Delphi StopExpectingException 后的 7 个 Dunit 检查没有像我预期的那样工作

Delphi 7 Dunit checks after StopExpectingException are not working as I expect

下面的代码工作正常,calc... 生成异常,将其注释掉或更改 calc... 不抛出异常并且测试失败。

  StartExpectingException(exception);
  calcMembersPIPEndDate(EncodeDate(2005,01,01),true);
  StopExpectingException('calcMembersPIPEndDate - 1st after aDay');

我的问题是我在此测试方法中放入的任何检查都不会执行。
所以

  checkEquals(1,0);
  StartExpectingException(exception);
  calcMembersPIPEndDate(EncodeDate(2005,01,01),true);
  StopExpectingException('calcMembersPIPEndDate - 1st after aDay');

第一次 checkEquals 失败

  StartExpectingException(exception);
  calcMembersPIPEndDate(EncodeDate(2005,01,01),true);
  StopExpectingException('calcMembersPIPEndDate - 1st after aDay');
  checkEquals(1,0);

通过 - 为什么?

我试图找出我使用的 Dunit 版本:

testframework.pas has the following - which didn't seem to 
rcs_id: string = '#(@)$Id: TestFramework.pas,v 1.117 2006/07/19 02:45:55
rcs_version : string = '$Revision: 1.117 $';
versioninfo.inc
ReleaseNo : array[1..3] of Integer
          = (9,2,1);
ReleaseStr     = '9.2.1';
ReleaseWhen : array[1..6] of Integer
          = (2005,09,25,17,30,00);

StartExpectingExceptionStopExpectingException 这两个方法不能直接调用。

相反,您应该使用 ExpectedException 属性。当您设置此 属性 时,将调用 StartExpectingException。虽然您可以调用 StartExpectingException,但我相信您的预期用途是分配给 ExpectedException.

至于StopExpectingException,你就不叫了。框架调用它。它在执行您的测试方法的框架代码 TTestCase.RunTest 中这样做。

因此您的测试用例代码可能如下所示:

ExpectedException := ESomeException;
raise ESomeException.Create(...);

当您声明您期待异常时,您的意思是您的测试方法将引发该异常。由于引发异常会改变控制流,因此引发异常后出现的代码将不会执行。异常向上传播调用堆栈,直到它们被捕获。该框架将捕获 TTestCase.RunTest 中的异常。如果您已指出捕获的异常是预期的,那么测试将通过,否则将记录失败。

所有这一切的最终结果是,如果测试方法的最终行为是引发预期的异常,则可以使用 ExpectedException 机制。如果您想在引发异常后执行进一步测试,则 ExpectedException 机制根本没有用。如果你想这样做,那么你应该:

  1. 在您的测试方法中编写您自己的异常处理代码,检查异常是否按设计引发。
  2. 使用CheckException.

StopExpectingException无法按您期望的方式工作。了解异常状态下的执行流程以了解原因很重要。

考虑以下代码:

procedure InnerStep(ARaiseException);
begin
  Writeln('Begin');
  if ARaiseException then
    raise Exception.Create('Watch what happens now');
  Writeln('End');
end;

procedure OuterStep;
begin
  try
    InnerStep(False); //1
    InnerStep(True);  //2
    InnerStep(False); //3
  except
    //Do something because of exception
    raise;
  end;
end;

当您在上面调用 OuterStep 时,第 //2 行将在 InnerStep 中引发异常。现在每当引发异常时:

  • 指令指针跳出每个方法(有点像goto)到第一个除了finally 在调用堆栈中找到块。
  • Writeln('End');不会被调用。
  • //3 行将不会被调用。
  • 接下来执行 OuterStepexcept 块中的任何代码。
  • 最后当调用raise;时,再次引发异常,指令指针跳转到下一个exceptfinally 阻止。
  • 另请注意,像raise; except 块内的任何其他异常也会跳出,(有效地隐藏了第一个异常) .

所以当你写的时候:

StartExpectingException(...);
DoSomething();
StopExpectingException(...);

有两种可能:

  1. DoSomething 引发异常并且 StopExpectingException 永远不会被调用。
  2. DoSomething 不会引发异常,当 StopExpectingException 被调用时 也没有异常。

DUnit 框架为您调用 StopExpectingException。但是您可能想知道如何处理您的测试用例来检查多个异常场景。

选项 1

编写较小的测试。
你知道那是每个人都说你在任何情况下都应该做的,对吗? :)
例如

procedure MyTests.TestBadCase1;
begin
  ExpectedException := ESomethingBadHappened;
  DoSomething('Bad1');
  //Nothing to do. Exception should be raised, so any more code would
  //be pointless.
  //If exception is NOT raised, test will exit 'normally', and
  //framework will fail the test when it detects that the expected
  //exception was not raised.
end;

procedure MyTests.TestBadCase2;
begin
  ExpectedException := ESomethingBadHappened;
  DoSomething('Bad2');
end;

procedure MyTests.TestGoodCase;
begin
  DoSomething('Good');
  //Good case does not (or should not) raise an exception.
  //So now you can check results or expected state change.
end;

选项 2

正如 David 所建议的,您可以在测试中编写自己的异常处理。但您会注意到它可能会变得有点混乱,并且在大多数情况下您可能更喜欢选项 1。尤其是当您拥有明确命名的测试的额外好处时,可以更轻松地准确识别出问题所在。

procedure MyTests.TestMultipleBadCasesInTheSameTest;
begin
  try
    DoSomething('Bad1');
    //This time, although you're expecting an exception and lines
    //here shouldn't be executed:
    //**You've taken on the responsibility** of checking that an
    //exception is raised. So **if** the next line is called, the
    //expected exception **DID NOT HAPPEN**!
    Fail('Expected exception for case 1 not raised');
  except
    //Swallow the expected exception only!
    on ESomethingBadHappened do;
    //One of the few times doing nothing and simply swallowing an
    //exception is the right thing to do.
    //NOTE: Any other exception will escape the test and be reported
    //as an error by DUnit
  end;

  try    
    DoSomething('Bad2');
    Fail('Expected exception for case 2 not raised');
  except
    on E: ESomethingBadHappened do
      CheckEquals('ExpectedErrorMessage', E.Message);
      //One advantage of the manual checking is that you can check
      //specific attributes of the exception object.
      //You could also check objects used in the DoSomething method
      //e.g. to ensure state is rolled back correctly as a result of
      //the error.
  end;
end;

NB! NB! Something very important to note in option 2. You need to be careful about what exception class you swallow. DUnit's Fail() method raises an ETestFailure exception to report to the framework that the test failed. And you wouldn't want to accidentally swallow the exception that's going to trigger the test failure for expected exception.

与异常测试相关的微妙问题使得重要的是:首先进行测试,确保您有正确的失败,然后才实施生产代码更改以获得通过。该过程将大大减少无效测试的机会。