2016-09-22 6 views
1

Я начинаю работать с модульными тестами tSQLt для SQL Server в моем производственном коде. В настоящее время я использую шаблон обработки ошибок Erland Sommarskog's для SQL Server.Использование XACT_ABORT и TRY CATCH вместе в SQL Server break tSQLt Откат

USE TempDB; 

SET ANSI_NULLS, QUOTED_IDENTIFIER ON; 
GO 

IF OBJECT_ID('dbo.SommarskogRollback') IS NOT NULL 
    DROP PROCEDURE dbo.SommarskogRollback; 
GO 

CREATE PROCEDURE dbo.SommarskogRollback 
AS 
BEGIN; /*Stored Procedure*/ 

    SET XACT_ABORT, NOCOUNT ON; 

    BEGIN TRY; 

    BEGIN TRANSACTION; 

     RAISERROR('This is just a test. Had this been an actual error, we would have given you some cryptic gobbledygook.', 16, 1); 

    COMMIT TRANSACTION; 

    END TRY 
    BEGIN CATCH; 

    IF @@TRANCOUNT > 0 
     ROLLBACK TRANSACTION; 

    THROW; 

    END CATCH; 

END; /*Stored Procedure*/ 
GO 

Erland Sommarskog рекомендует нам всегда SET XACT_ABORT ON, потому что только тогда ошибки ручки SQL Server в (главным образом) последовательным образом.

Это создает проблему при использовании tSQLt. tSQLt выполняет все тесты внутри явной транзакции. Когда тесты завершены, вся транзакция возвращается. Это делает очистку тестовых артефактов абсолютно безболезненными. Однако с XACT_ABORT ON любая ошибка, выброшенная внутри блока TRY, немедленно dooms эта сделка. Сделка должна полностью откат. Он не может совершить, и он не может вернуться к точке сохранения. На самом деле ничто не может записывать в журнал транзакций внутри этого сеанса, пока транзакция не вернется. Тем не менее, tSQLt не может правильно отслеживать результаты теста, если транзакция не открыта, когда тесты заканчиваются. tSQLt прекращает выполнение и выдает ROLLBACK ERROR для обреченных транзакций. Сбой теста показывает статус ошибки (а не успех или отказ), а последующие тесты не выполняются.

Себастьян Майне, создатель tSQLt, рекомендует другой error handling pattern.

USE TempDB; 

SET ANSI_NULLS, QUOTED_IDENTIFIER ON; 
GO 

IF OBJECT_ID('dbo.MeineRollback') IS NOT NULL 
    DROP PROCEDURE dbo.MeineRollback; 
GO 

CREATE PROCEDURE dbo.MeineRollback 
AS 
BEGIN /*Stored Procedure*/ 

    SET NOCOUNT ON; 

    /* We declare the error variables here, populate them inside the CATCH 
    * block and then do our error handling after exiting the CATCH block 
    */ 
    DECLARE @ErrorNumber  INT 
     ,@MessageTemplate NVARCHAR(4000) 
     ,@ErrorMessage  NVARCHAR(4000) 
     ,@ErrorProcedure NVARCHAR(126) 
     ,@ErrorLine  INT 
     ,@ErrorSeverity INT 
     ,@ErrorState  INT 
     ,@RaisErrorState INT 
     ,@ErrorLineFeed NCHAR(1) = CHAR(10) 
     ,@ErrorStatus  INT = 0 
     ,@SavepointName VARCHAR(32) = REPLACE((CAST(NEWID() AS VARCHAR(36))), '-', ''); 
     /*Savepoint names are 32 characters and must be unique. UNIQUEIDs are 36, four of which are dashes.*/ 

    BEGIN TRANSACTION; /*If a transaction is already in progress, this just increments the transaction count*/ 

    SAVE TRANSACTION @SavepointName; 

    BEGIN TRY; 

    RAISERROR('This is a test. Had this been an actual error, Sebastian would have given you a meaningful error message.', 16, 1); 

    END TRY 
    BEGIN CATCH; 

    /* Build a message string with placeholders for the original error information 
    * Note: "%d" & "%s" are placeholders (substitution parameters) which capture 
    *  the values from the argument list of the original error message. 
    */ 
    SET @MessageTemplate = N': Error %d, Severity %d, State %d, ' + @ErrorLineFeed 
         + N'Procedure %s, Line %d, '    + @ErrorLineFeed 
         + N', Message: %s'; 

    SELECT @ErrorStatus = 1 
      ,@ErrorMessage = ERROR_MESSAGE() 
      ,@ErrorNumber = ERROR_NUMBER() 
      ,@ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-') 
      ,@ErrorLine  = ERROR_LINE() 
      ,@ErrorSeverity = ERROR_SEVERITY() 
      ,@ErrorState  = ERROR_STATE() 
      ,@RaisErrorState = CASE ERROR_STATE() 
           WHEN 0 /*RAISERROR Can't generate errors with State = 0*/ 
           THEN 1 
           ELSE ERROR_STATE() 
          END; 

    END CATCH; 

    /*Rollback to savepoint if error occurred. This does not affect the transaction count.*/ 
    IF @ErrorStatus <> 0 
    ROLLBACK TRANSACTION @SavepointName; 

    /*If this procedure executed inside a transaction, then the commit just subtracts one from the transaction count.*/ 
    COMMIT TRANSACTION; 

    IF @ErrorStatus = 0 
    RETURN 0; 

    ELSE 
    BEGIN; /*Re-throw error*/ 

     /*Rethrow the error. The msg_str parameter will contain the original error information*/ 
     RAISERROR(@MessageTemplate /*msg_str parameter as message format template*/ 
       ,@ErrorSeverity /*severity parameter*/ 
       ,@RaisErrorState /*state parameter*/ 
       ,@ErrorNumber  /*argument: original error number*/ 
       ,@ErrorSeverity /*argument: original error severity*/ 
       ,@ErrorState  /*argument: original error state*/ 
       ,@ErrorProcedure /*argument: original error procedure name*/ 
       ,@ErrorLine  /*argument: original error line number*/ 
       ,@ErrorMessage  /*argument: original error message*/ 
       ); 

     RETURN -1; 

    END; /*Re-throw error*/ 

END /*Stored Procedure*/ 
GO 

Он объявляет переменные ошибки, начинает транзакцию, устанавливает точку сохранения, а затем выполняет код процедуры внутри TRY блока. Если блок TRY выдает ошибку, выполнение переходит к блоку CATCH, который заполняет переменные ошибки. Затем выполнение переходит из блока TRY CATCH. При ошибке транзакция возвращается к точке сохранения, установленной в начале процедуры. Затем транзакция завершается. Из-за того, как SQL Server обрабатывает вложенные транзакции, этот COMMIT просто вычитает один из счетчика транзакций при выполнении внутри другой транзакции. (Вложенные транзакции действительно не существуют в SQL Server.)

Себастьян создал очень аккуратный и аккуратный рисунок. Каждая процедура в цепочке выполнения очищает свои транзакции. К сожалению, у этой модели есть большая проблема: обречены на сделки. Обреченные транзакции ломают этот шаблон, потому что они не могут вернуться к точке сохранения или совершить. Они могут полностью откатываться. Это, конечно, означает, что вы не можете установить XACT_ABORT ON при использовании блоков TRY-CATCH (и вы всегда должны использовать блоки TRY-CATCH.) Даже при XACT_ABORT OFF многие ошибки, такие как ошибки компиляции, в любом случае обрекают транзакцию , Кроме того, сохранение точек не будет работать с распределенными транзакциями.

Как я могу обойти это? Мне нужен шаблон обработки ошибок, который будет работать в рамках тестовой системы tSQLt, а также обеспечит последовательную корректную обработку ошибок в процессе производства. Я мог бы проверить среду во время выполнения и соответствующим образом настроить поведение. (См. Пример ниже). Однако мне это не нравится. Мне кажется, что это взломать. Это требует, чтобы среды разработки были настроены последовательно. Хуже того, я не тестирую свой фактический производственный код. У кого-нибудь есть блестящее решение?

USE TempDB; 

SET ANSI_NULLS, QUOTED_IDENTIFIER ON; 
GO 

IF OBJECT_ID('dbo.ModifiedRollback') IS NOT NULL 
    DROP PROCEDURE dbo.ModifiedRollback; 
GO 

CREATE PROCEDURE dbo.ModifiedRollback 
AS 
BEGIN; /*Stored Procedure*/ 

    SET NOCOUNT ON; 

    IF RIGHT(@@SERVERNAME, 9) = '\LOCALDEV' 
    SET XACT_ABORT OFF; 

    ELSE 
    SET XACT_ABORT ON; 

    BEGIN TRY; 

    BEGIN TRANSACTION; 

     RAISERROR('This is just a test. Had this been an actual error, we would have given you some cryptic gobbledygook.', 16, 1); 

    COMMIT TRANSACTION; 

    END TRY 
    BEGIN CATCH; 

    IF @@TRANCOUNT > 0 AND RIGHT(@@SERVERNAME,9) <> '\LOCALDEV' 
     ROLLBACK TRANSACTION; 

    THROW; 

    END CATCH; 

END; /*Stored Procedure*/ 
GO 

EDIT: После дальнейших испытаний я обнаружил, что мой измененный откат тоже не работает. Когда процедура выдает ошибку, она выходит, не откатываясь или не совершая никаких действий. tSQLt выдает сообщение об ошибке, потому что @@ TRANCOUNT, когда процедура выходит, не соответствует счету при запуске процедуры. После некоторых проб и ошибок я нашел обходное решение, которое работает в моих тестах. Он сочетает в себе два подхода к обработке ошибок: упрощение обработки ошибок, а некоторые пути кода не могут быть протестированы.Мне бы хотелось найти лучшее решение.

USE TempDB; 

SET ANSI_NULLS, QUOTED_IDENTIFIER ON; 
GO 

IF OBJECT_ID('dbo.TestedRollback') IS NOT NULL 
    DROP PROCEDURE dbo.TestedRollback; 
GO 

CREATE PROCEDURE dbo.TestedRollback 
AS 
BEGIN /*Stored Procedure*/ 

    SET NOCOUNT ON; 

    /* Due to the way tSQLt uses transactions and the way SQL Server handles errors, we declare our error-handling 
    * variables here, populate them inside the CATCH block and then do our error-handling after exiting 
    */ 
    DECLARE @ErrorStatus  BIT 
     ,@ErrorNumber  INT 
     ,@MessageTemplate NVARCHAR(4000) 
     ,@ErrorMessage  NVARCHAR(4000) 
     ,@ErrorProcedure NVARCHAR(126) 
     ,@ErrorLine   INT 
     ,@ErrorSeverity  INT 
     ,@ErrorState  INT 
     ,@RaisErrorState INT 
     ,@ErrorLineFeed  NCHAR(1) = CHAR(10) 
     ,@FALSE    BIT = CAST(0 AS BIT) 
     ,@TRUE    BIT = CAST(1 AS BIT) 
     ,@tSQLtEnvironment BIT 
     ,@SavepointName  VARCHAR(32) = REPLACE((CAST(NEWID() AS VARCHAR(36))), '-', ''); 
     /*Savepoint names are 32 characters long and must be unique. UNIQUEIDs are 36, four of which are dashes*/ 

    /* The tSQLt Unit Testing Framework we use in our local development environments must maintain open transactions during testing. So, 
    * we don't roll back transactions during testing. Also, doomed transactions can't stay open, so we SET XACT_ABORT OFF while testing. 
    */ 
    IF RIGHT(@@SERVERNAME, 9) = '\LOCALDEV' 
    SET @tSQLtEnvironment = @TRUE 

    ELSE 
    SET @tSQLtEnvironment = @FALSE; 


    IF @tSQLtEnvironment = @TRUE 
    SET XACT_ABORT OFF; 

    ELSE 
    SET XACT_ABORT ON; 

    BEGIN TRY; 

    SET ROWCOUNT 0; /*The ROWCOUNT setting can be updated outside the procedure and changes its behavior. This sets it to the default.*/ 

    SET @ErrorStatus = @FALSE; 

    BEGIN TRANSACTION; 

     /*We need a save point to roll back to in the tSQLt Environment.*/ 
     IF @tSQLtEnvironment = @TRUE 
     SAVE TRANSACTION @SavepointName; 

     RAISERROR('Cryptic gobbledygook.', 16, 1); 

    COMMIT TRANSACTION; 

    RETURN 0; 

    END TRY 
    BEGIN CATCH; 

    SET @ErrorStatus = @TRUE; 

    /* Build a message string with placeholders for the original error information 
    * Note: "%d" & "%s" are placeholders (substitution parameters) which capture 
    *  the values from the argument list of the original error message. 
    */ 
    SET @MessageTemplate = N': Error %d, Severity %d, State %d, ' + @ErrorLineFeed 
         + N'Procedure %s, Line %d, '    + @ErrorLineFeed 
         + N', Message: %s'; 

    SELECT @ErrorMessage = ERROR_MESSAGE() 
      ,@ErrorNumber = ERROR_NUMBER() 
      ,@ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-') 
      ,@ErrorLine  = ERROR_LINE() 
      ,@ErrorSeverity = ERROR_SEVERITY() 
      ,@ErrorState  = ERROR_STATE() 
      ,@RaisErrorState = CASE ERROR_STATE() 
           WHEN 0 /*RAISERROR Can't generate errors with State = 0*/ 
           THEN 1 
           ELSE ERROR_STATE() 
          END; 
    END CATCH; 

    /* Due to the way the tSQLt test framework uses transactions, we use two different error-handling schemes: 
    * one for unit-testing and the other for our main Test/Staging/Production environments. In those environments 
    * we roll back transactions in the CATCH block in the event of an error. In unit-testing, on the other hand, 
    * we begin a transaction and set a save point. If an error occurs we roll back to the save point and then 
    * commit the transaction. Since tSQLt executes all test in a single explicit transaction, starting a 
    * transaction at the beginning of this stored procedure just adds one to @@TRANCOUNT. Committing the 
    * transaction subtracts one from @@TRANCOUNT. Rolling back to a save point does not affect @@TRANCOUNT. 
    */ 
    IF @ErrorStatus = @TRUE 
    BEGIN; /*Error Handling*/ 

     IF @tSQLtEnvironment = @TRUE 
     BEGIN; /*tSQLt Error Handling*/ 
      ROLLBACK TRANSACTION @SavepointName; /*Rolls back to save point but does not affect @@TRANCOUNT*/ 

      COMMIT TRANSACTION; /*Subtracts one from @@TRANCOUNT*/ 
     END; /*tSQLt Error Handling*/ 

     ELSE IF @@TRANCOUNT > 0 
     ROLLBACK TRANSACTION; 

     /*Rethrow the error. The msg_str parameter will contain the original error information*/ 
     RAISERROR(@MessageTemplate /*msg_str parameter as message format template*/ 
       ,@ErrorSeverity /*severity parameter*/ 
       ,@RaisErrorState /*state parameter*/ 
       ,@ErrorNumber  /*argument: original error number*/ 
       ,@ErrorSeverity /*argument: original error severity*/ 
       ,@ErrorState  /*argument: original error state*/ 
       ,@ErrorProcedure /*argument: original error procedure name*/ 
       ,@ErrorLine  /*argument: original error line number*/ 
       ,@ErrorMessage  /*argument: original error message*/ 
       ); 

    END; /*Error Handling*/ 

END /*Stored Procedure*/ 
GO 
+0

Wow. это огромное количество шаблонов, которые нужно добавить в proc. Удачи найти лучшее обходное решение. – JJS

ответ

0

Я тестирую исправление для этого, что modificaties процедуры рамочного tSQLt.Private_RunTest. В основном, в первичном блоке CATCH, в котором он пытается сделать именованный Откат (Line 1448 для меня), я замена

ROLLBACK TRAN @TranName; 

с

IF XACT_STATE() = 1 -- transaction is active 
     ROLLBACK TRAN @TranName; -- execute original code 
    ELSE IF XACT_STATE() = -1 -- transaction is doomed; cannot be partially rolled back 
     ROLLBACK; -- fully roll back 

    IF (@@TRANCOUNT = 0) 
     BEGIN TRAN; -- restart transaction to fulfill expectations below 

Предварительное тестирование выглядит хорошо. Будьте на связи. (Я отправлю git после того, как я получу еще больше уверенности в этом предлагаемом редактировании.)