How to ROLLBACK a transaction when testing using tSQLt

余生颓废 提交于 2019-12-30 09:39:29

问题


I recently was calling a procedure that contained a rasierror in the code. The raiserror was in a try catch block. Also a BEGIN TRAN was in the same try catch block after the raiserror. The Catch block is designed to ROLLBACK the transaction if the error occurred in the transaction. The way it does this is to check the @@TRANCOUNT if it is greater that 0 I know that it had started a transaction and needs to ROLLBACK. When testing with tSQLt the @@TRANCOUNT is always >0 so if it ever hits the CATCH Block the ROLLBACK is executed and tSQLt fails (because tSQLt is running in a transaction). When I rasie an error and the CATCH block is run tSQLt always fails the test. I have no way to test for the correct handling of the raiserror. How would you create a test case that can potentially ROLLBACK a transaction?


回答1:


As you mentioned, tSQLt runs every test in its own transaction. To keep track of what is going on is relies on that same transaction to be still open when the test finishes. SQL Server does not support nested transactions, so your procedure rolls back everything, including the status information the framework stored for the current test. At that point tSQLt can only assume that something really bad happened. It therefore marks the test as errored.

SQL Server itself discourages a rollback inside a procedure, by throwing an error if that procedure was called within an open transaction. For ways to deal with this situation and some additional info check out my blog post about how to rollback in procedures.




回答2:


As I'm just reading up on tSQLt this was one of the first questions that came to mind when I've learned each test ran in a transactions. As some of my stored procedures do start transaction, some even use nested transactions, this can become challenging. What I've learned about nested transactions, if you apply the following rules you can keep your code clean of constant error checking and still handle errors gracefully.

  • Always use a TRY/CATCH block when opening a transactions
  • Always commit the transactions unless an error was raised
  • Always rollback the transaction when an error is raised unless @@TRANCOUNT = 0
  • Always reraise the error unless you're absolutely sure there was no transaction open at the start of the stored procedure.

Keeping those rules in mind here is an example of a proc implementation and test code to test it.

ALTER PROC testProc
    @IshouldFail BIT
AS
BEGIN TRY
    BEGIN TRAN

    IF @IshouldFail = 1
        RAISERROR('failure', 16, 1);

    COMMIT
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK;

    -- Do some exception handling

    -- You'll need to reraise the error to prevent exceptions about inconsistent 
    -- @@TRANCOUNT before / after execution of the stored proc.
    RAISERROR('failure', 16, 1);
END CATCH
GO


--EXEC tSQLt.NewTestClass 'tSQLt.experiments';
--GO
ALTER PROCEDURE [tSQLt.experiments].[test testProc nested transaction fails]
AS
BEGIN
    --Assemble
    DECLARE @CatchWasHit CHAR(1) = 'N';

    --Act
    BEGIN TRY
        EXEC dbo.testProc 1
    END TRY
    BEGIN CATCH 
        IF @@TRANCOUNT = 0
            BEGIN TRAN --reopen an transaction
        SET @CatchWasHit = 'Y';
    END CATCH

    --Assert
    EXEC tSQLt.AssertEqualsString @Expected = N'Y', @Actual = @CatchWasHit, @Message = N'Exception was expected'

END;
GO

ALTER PROCEDURE [tSQLt.experiments].[test testProc nested transaction succeeds]
AS
BEGIN
    --Act
    EXEC dbo.testProc 0

END;
GO

EXEC tSQLt.Run @TestName = N'tSQLt.experiments'



回答3:


Better to use a BEGIN TRY block after BEGIN TRANSACTION. I did this when I had a similar problem. This is more logical, because in CATCH block I checked IF @@TRANCOUNT > 0 ROLLBACK. This condition doesn't need to be checked if another error is raised before BEGIN TRANSACTION. And in this case you can test your RAISERROR functionality.




回答4:


+1 to both the above answers.

However, if you don't want to use TRY .. CATCH, please try the following code. The part between the lines ----- represents the test, and above and below that represents tSQLt, before and after it calls your test. As you can see, the transaction started by tSQLt before calling the test, is still in place, as it expects, whether or not the error occurs. @@TRANSCOUNT is still 1

You can comment out the RAISERROR to try it with and without the exception being raised.

SET NOCOUNT ON

BEGIN TRANSACTION  -- DONE BY tSQLt
PRINT 'Inside tSQLt before calling the test: @@TRANCOUNT = ' + CONVERT (VARCHAR, @@TRANCOUNT)

    ---------------------------------
    PRINT '  Start of test ---------------------------'

    SAVE TRANSACTION Savepoint
    PRINT '  Inside the test: @@TRANCOUNT = ' + CONVERT (VARCHAR, @@TRANCOUNT)


    BEGIN TRANSACTION -- PART OF THE TEST
    PRINT '    Transaction in the test: @@TRANCOUNT = ' + CONVERT (VARCHAR, @@TRANCOUNT)

        RAISERROR ('A very nice error', 16, 0)

        PRINT '  @@ERROR = ' + CONVERT(VARCHAR,@@ERROR)


    -- PART OF THE TEST - CLEAN-UP
    IF @@ERROR <> 0 ROLLBACK TRANSACTION Savepoint   -- Not all the way, just tothe save point
    ELSE COMMIT TRANSACTION

    PRINT '  About to finish the test: @@TRANCOUNT = ' + CONVERT (VARCHAR, @@TRANCOUNT)

    PRINT '  End of test ---------------------------'

    ---------------------------------

ROLLBACK TRANSACTION   -- DONE BY tSQLt
PRINT 'Inside tSQLt after finishing the test: @@TRANCOUNT = ' + CONVERT (VARCHAR, @@TRANCOUNT)

With acknowledgement to information and code at http://www.blackwasp.co.uk/SQLSavepoints.aspx



来源:https://stackoverflow.com/questions/8973138/how-to-rollback-a-transaction-when-testing-using-tsqlt

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!