How to ROLLBACK a transaction when testing using tSQLt

前端 未结 4 1408
离开以前
离开以前 2021-01-12 15:05

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

相关标签:
4条回答
  • 2021-01-12 15:14

    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'
    
    0 讨论(0)
  • 2021-01-12 15:29

    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.

    0 讨论(0)
  • 2021-01-12 15:29

    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.

    0 讨论(0)
  • 2021-01-12 15:31

    +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

    0 讨论(0)
提交回复
热议问题