5

这是场景:

  1. 存储过程sproc_a调用sproc_b。然后sproc_b调用sproc_c。典型的嵌套过程。
  2. Sproc_a 执行了 SET XACT_ABORT ON;并使用命名事务。
  3. Sproc_c 引发错误。
  4. tSQLt.ExpectException 未能确认错误。测试应该成功,但失败了。

下面是复制场景的代码。

create procedure sproc_c
as
    RAISERROR('An error is found', 11, 1)
go

create procedure sproc_b
as 
    exec dbo.sproc_c;
go

create procedure sproc_a 
as 
SET QUOTED_IDENTIFIER OFF  
SET ANSI_NULLS ON  
SET NOCOUNT ON  
SET XACT_ABORT ON  
SET ANSI_WARNINGS OFF  

    declare @transactionName as varchar(50) = '[POC]';

    begin tran @transactionName
    save tran @transactionName

    exec dbo.sproc_b;
    commit tran @transactionName
go

CREATE PROCEDURE [test sproc_a]
AS
    -- Assert
    BEGIN 
        EXEC tSQLt.ExpectException
            @ExpectedMessage = 'An error is found'
    END 

    -- Act
    BEGIN 
        EXEC dbo.sproc_a
    END 
GO 

EXEC tSQLt.Run '[test sproc_a]'

当我删除 SET XACT_ABORT ON 时,单元测试是成功的,但它会出现错误:Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 0, current count = 1.

这更像是一个错误报告。好吧,我想也许问题是:有谁知道如何解决它?:)

4

2 回答 2

3

在使用 tSQLt 进行测试时应用 How to ROLLBACK a transaction 中的逻辑添加一个 TRY CATCH 来检查是否仍然需要 ROLLBACK。

create procedure sproc_c
as
    RAISERROR('An error is found', 11, 1)
go

create procedure sproc_b
as 
    exec dbo.sproc_c;
go

create procedure sproc_a 
as 
SET QUOTED_IDENTIFIER OFF  
SET ANSI_NULLS ON  
SET NOCOUNT ON  
SET XACT_ABORT ON  
SET ANSI_WARNINGS OFF  

    declare @transactionName as varchar(50) = '[POC]';
    BEGIN TRY    
        begin tran @transactionName
        save tran @transactionName

        exec dbo.sproc_b;

        commit tran @transactionName
    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('An error is found', 11, 1);
    END CATCH
go

CREATE PROCEDURE [test sproc_a]
AS
    -- Assert
    BEGIN 
        EXEC tSQLt.ExpectException
            @ExpectedMessage = 'An error is found'
    END 

    -- Act
    BEGIN 
        EXEC dbo.sproc_a
    END 
GO 

EXEC tSQLt.Run '[test sproc_a]'
于 2015-06-08T02:05:14.463 回答
2

我正在单独发表评论以回答我自己的问题。我调查了 tSQLt.Private_RunTest。事实证明:

  1. Private_RunTest 有一个 BEGIN TRAN。然后它将保存一个命名事务。
  2. 紧随其后的是一个 TRY-CATCH,它将执行一个 exec(@cmd)。这基本上执行您的单元测试。例如:“EXEC tSQLt.Run '[test sproc_a]'”。
  3. 当 sproc_a 引发错误时,Private_RunTest 将尝试执行 ROLLBACK TRAN @TranName。这将失败,因为它不会回滚 sproc_a 中的命名事务。

作为解决方案,我破解了 tSQLt.Private_RunTest 并替换了代码“ROLLBACK TRAN @TranName;” 用“回滚转;”。

我还在提交时添加了一个条件。

我不确定进行此更改后会产生什么影响。我们会看看情况如何。以下是我的更改:

CREATE PROCEDURE tSQLt.Private_RunTest
   @TestName NVARCHAR(MAX),
   @SetUp NVARCHAR(MAX) = NULL
AS
BEGIN
DECLARE @Msg NVARCHAR(MAX); SET @Msg = '';
DECLARE @Msg2 NVARCHAR(MAX); SET @Msg2 = '';
DECLARE @Cmd NVARCHAR(MAX); SET @Cmd = '';
DECLARE @TestClassName NVARCHAR(MAX); SET @TestClassName = '';
DECLARE @TestProcName NVARCHAR(MAX); SET @TestProcName = '';
DECLARE @Result NVARCHAR(MAX); SET @Result = 'Success';
DECLARE @TranName CHAR(32); EXEC tSQLt.GetNewTranName @TranName OUT;
DECLARE @TestResultId INT;
DECLARE @PreExecTrancount INT;

TRUNCATE TABLE tSQLt.CaptureOutputLog;
CREATE TABLE #ExpectException(ExpectException INT,ExpectedMessage NVARCHAR(MAX), ExpectedSeverity INT, ExpectedState INT, ExpectedMessagePattern NVARCHAR(MAX), ExpectedErrorNumber INT, FailMessage NVARCHAR(MAX));

IF EXISTS (SELECT 1 FROM sys.extended_properties WHERE name = N'SetFakeViewOnTrigger')
BEGIN
  RAISERROR('Test system is in an invalid state. SetFakeViewOff must be called if SetFakeViewOn was called. Call SetFakeViewOff after creating all test case procedures.', 16, 10) WITH NOWAIT;
  RETURN -1;
END;

SELECT @Cmd = 'EXEC ' + @TestName;

SELECT @TestClassName = OBJECT_SCHEMA_NAME(OBJECT_ID(@TestName)), --tSQLt.Private_GetCleanSchemaName('', @TestName),
       @TestProcName = tSQLt.Private_GetCleanObjectName(@TestName);

INSERT INTO tSQLt.TestResult(Class, TestCase, TranName, Result) 
    SELECT @TestClassName, @TestProcName, @TranName, 'A severe error happened during test execution. Test did not finish.'
    OPTION(MAXDOP 1);
SELECT @TestResultId = SCOPE_IDENTITY();


BEGIN TRAN;
SAVE TRAN @TranName;

SET @PreExecTrancount = @@TRANCOUNT;

TRUNCATE TABLE tSQLt.TestMessage;

DECLARE @TmpMsg NVARCHAR(MAX);
BEGIN TRY
    IF (@SetUp IS NOT NULL) EXEC @SetUp;
    EXEC (@Cmd);
    IF(EXISTS(SELECT 1 FROM #ExpectException WHERE ExpectException = 1))
    BEGIN
      SET @TmpMsg = COALESCE((SELECT FailMessage FROM #ExpectException)+' ','')+'Expected an error to be raised.';
      EXEC tSQLt.Fail @TmpMsg;
    END
END TRY
BEGIN CATCH
    IF ERROR_MESSAGE() LIKE '%tSQLt.Failure%'
    BEGIN
        SELECT @Msg = Msg FROM tSQLt.TestMessage;
        SET @Result = 'Failure';
    END
    ELSE
    BEGIN
      DECLARE @ErrorInfo NVARCHAR(MAX);
      SELECT @ErrorInfo = 
        COALESCE(ERROR_MESSAGE(), '<ERROR_MESSAGE() is NULL>') + 
        '[' +COALESCE(LTRIM(STR(ERROR_SEVERITY())), '<ERROR_SEVERITY() is NULL>') + ','+COALESCE(LTRIM(STR(ERROR_STATE())), '<ERROR_STATE() is NULL>') + ']' +
        '{' + COALESCE(ERROR_PROCEDURE(), '<ERROR_PROCEDURE() is NULL>') + ',' + COALESCE(CAST(ERROR_LINE() AS NVARCHAR), '<ERROR_LINE() is NULL>') + '}';

      IF(EXISTS(SELECT 1 FROM #ExpectException))
      BEGIN
        DECLARE @ExpectException INT;
        DECLARE @ExpectedMessage NVARCHAR(MAX);
        DECLARE @ExpectedMessagePattern NVARCHAR(MAX);
        DECLARE @ExpectedSeverity INT;
        DECLARE @ExpectedState INT;
        DECLARE @ExpectedErrorNumber INT;
        DECLARE @FailMessage NVARCHAR(MAX);
        SELECT @ExpectException = ExpectException,
               @ExpectedMessage = ExpectedMessage, 
               @ExpectedSeverity = ExpectedSeverity,
               @ExpectedState = ExpectedState,
               @ExpectedMessagePattern = ExpectedMessagePattern,
               @ExpectedErrorNumber = ExpectedErrorNumber,
               @FailMessage = FailMessage
          FROM #ExpectException;

        IF(@ExpectException = 1)
        BEGIN
          SET @Result = 'Success';
          SET @TmpMsg = COALESCE(@FailMessage+' ','')+'Exception did not match expectation!';
          IF(ERROR_MESSAGE() <> @ExpectedMessage)
          BEGIN
            SET @TmpMsg = @TmpMsg +CHAR(13)+CHAR(10)+
                       'Expected Message: <'+@ExpectedMessage+'>'+CHAR(13)+CHAR(10)+
                       'Actual Message  : <'+ERROR_MESSAGE()+'>';
            SET @Result = 'Failure';
          END
          IF(ERROR_MESSAGE() NOT LIKE @ExpectedMessagePattern)
          BEGIN
            SET @TmpMsg = @TmpMsg +CHAR(13)+CHAR(10)+
                       'Expected Message to be like <'+@ExpectedMessagePattern+'>'+CHAR(13)+CHAR(10)+
                       'Actual Message            : <'+ERROR_MESSAGE()+'>';
            SET @Result = 'Failure';
          END
          IF(ERROR_NUMBER() <> @ExpectedErrorNumber)
          BEGIN
            SET @TmpMsg = @TmpMsg +CHAR(13)+CHAR(10)+
                       'Expected Error Number: '+CAST(@ExpectedErrorNumber AS NVARCHAR(MAX))+CHAR(13)+CHAR(10)+
                       'Actual Error Number  : '+CAST(ERROR_NUMBER() AS NVARCHAR(MAX));
            SET @Result = 'Failure';
          END
          IF(ERROR_SEVERITY() <> @ExpectedSeverity)
          BEGIN
            SET @TmpMsg = @TmpMsg +CHAR(13)+CHAR(10)+
                       'Expected Severity: '+CAST(@ExpectedSeverity AS NVARCHAR(MAX))+CHAR(13)+CHAR(10)+
                       'Actual Severity  : '+CAST(ERROR_SEVERITY() AS NVARCHAR(MAX));
            SET @Result = 'Failure';
          END
          IF(ERROR_STATE() <> @ExpectedState)
          BEGIN
            SET @TmpMsg = @TmpMsg +CHAR(13)+CHAR(10)+
                       'Expected State: '+CAST(@ExpectedState AS NVARCHAR(MAX))+CHAR(13)+CHAR(10)+
                       'Actual State  : '+CAST(ERROR_STATE() AS NVARCHAR(MAX));
            SET @Result = 'Failure';
          END
          IF(@Result = 'Failure')
          BEGIN
            SET @Msg = @TmpMsg;
          END
        END 
        ELSE
        BEGIN
            SET @Result = 'Failure';
            SET @Msg = 
              COALESCE(@FailMessage+' ','')+
              'Expected no error to be raised. Instead this error was encountered:'+
              CHAR(13)+CHAR(10)+
              @ErrorInfo;
        END
      END
      ELSE
      BEGIN
        SET @Result = 'Error';
        SET @Msg = @ErrorInfo;
      END  
    END;
END CATCH

BEGIN TRY
    -- Replaced "ROLLBACK TRAN @TranName;" with "ROLLBACK TRAN;". The prior approach can't handle nested named transactions. 
    --ROLLBACK TRAN @TranName;

    ROLLBACK TRAN;
END TRY
BEGIN CATCH
    DECLARE @PostExecTrancount INT;
    SET @PostExecTrancount = @PreExecTrancount - @@TRANCOUNT;
    IF (@@TRANCOUNT > 0) ROLLBACK;
    BEGIN TRAN;
    IF(   @Result <> 'Success'
       OR @PostExecTrancount <> 0
      )
    BEGIN
      SELECT @Msg = COALESCE(@Msg, '<NULL>') + ' (There was also a ROLLBACK ERROR --> ' + COALESCE(ERROR_MESSAGE(), '<ERROR_MESSAGE() is NULL>') + '{' + COALESCE(ERROR_PROCEDURE(), '<ERROR_PROCEDURE() is NULL>') + ',' + COALESCE(CAST(ERROR_LINE() AS NVARCHAR), '<ERROR_LINE() is NULL>') + '})';
      SET @Result = 'Error';
    END
END CATCH    

If(@Result <> 'Success') 
BEGIN
  SET @Msg2 = @TestName + ' failed: (' + @Result + ') ' + @Msg;
  EXEC tSQLt.Private_Print @Message = @Msg2, @Severity = 0;
END

IF EXISTS(SELECT 1 FROM tSQLt.TestResult WHERE Id = @TestResultId)
BEGIN
    UPDATE tSQLt.TestResult SET
        Result = @Result,
        Msg = @Msg
     WHERE Id = @TestResultId;
END
ELSE
BEGIN
    INSERT tSQLt.TestResult(Class, TestCase, TranName, Result, Msg)
    SELECT @TestClassName, 
           @TestProcName,  
           '?', 
           'Error', 
           'TestResult entry is missing; Original outcome: ' + @Result + ', ' + @Msg;
END    

-- Add "IF (@@TRANCOUNT > 0)" so that it will only do the commit if there is a transaction. 
IF (@@TRANCOUNT > 0)
COMMIT;

END;
于 2015-10-20T07:10:05.130 回答