実稼働コードで SQL Server の tSQLt 単体テストを使用し始めています。現在、私はErland Sommarskog のSQL Server のエラー処理パターンを使用しています。
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 は、常にXACT_ABORT を ON に設定することを推奨しています。これは、SQL Server が (ほぼ) 一貫した方法でエラーを処理するためです。
ただし、これは tSQLt を使用する場合に問題を引き起こします。tSQLt は、明示的なトランザクション内ですべてのテストを実行します。テストが完了すると、トランザクション全体がロールバックされます。これにより、テスト アーティファクトのクリーンアップが完全に簡単になります。ただし、XACT_ABORT を ON にすると、TRY ブロック内でエラーがスローされると、そのトランザクションはすぐに破滅します。トランザクションは完全にロールバックする必要があります。コミットすることも、セーブ ポイントにロールバックすることもできません。実際、トランザクションがロールバックされるまで、そのセッション内のトランザクション ログには何も書き込むことができません。ただし、テストの終了時にトランザクションが開いていない限り、tSQLt はテスト結果を適切に追跡できません。tSQLt は実行を停止し、運命の ROLLBACK ERROR をスローしますトランザクション。失敗したテストは (成功または失敗ではなく) エラーのステータスを示し、後続のテストは実行されません。
tSQLt の作成者である Sebastian Meine は、別のエラー処理パターンを推奨しています。
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 は、別のトランザクション内で実行されると、トランザクション カウンターから 1 を減算するだけです。(ネストされたトランザクションは実際には SQL Server には存在しません。)
Sebastian は非常にきちんとしたパターンを作成しました。実行チェーン内の各プロシージャは、独自のトランザクションをクリーンアップします。残念ながら、このパターンには大きな問題があります: 破滅的なトランザクションです。失敗したトランザクションは、セーブ ポイントまたはコミットにロールバックできないため、このパターンを破ります。完全にロールバックすることしかできません。もちろん、これは、TRY-CATCH ブロックを使用するときに XACT_ABORT ON を設定できないことを意味します (常に TRY-CATCH ブロックを使用する必要があります)。 . さらに、セーブ ポイントは分散トランザクションでは機能しません。
どうすればこれを回避できますか? 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
編集:さらにテストした後、変更したロールバックも機能しないことがわかりました。プロシージャがエラーをスローすると、ロールバックもコミットもせずに終了します。プロシージャが終了するときの @@TRANCOUNT がプロシージャが開始するときのカウントと一致しないため、tSQLt はエラーをスローします。いくつかの試行錯誤の後、テストで機能する回避策を見つけました。2 つのエラー処理アプローチを組み合わせて、エラー処理をより複雑にし、一部のコード パスをテストできません。より良い解決策を見つけたいと思います。
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