单元测试的最佳实践 class 主要负责调用依赖项的方法,但也包含逻辑

Best practice for Unit Testing class which is mostly responsible to call methods of dependencies, but contains logic as well

让我们假设我有 StartCommandHandler 负责创建一些包含所需文件的文件。但是为了做到这一点,我必须给他一组子职责,比如:

作为该命令处理程序的结果,我们正在创建包含所有必需文件的文件夹。现在该文件夹已准备好进行另一项操作。

我刚读完"Art of the Unit testing"。并开始添加单元测试。我也遵循 SOLID 原则。特别是 SRPDIP,我认为它们是单元测试的先决条件。 所以,我上面提到的大部分事情都是通过特定接口完成的。因此,该命令处理程序 90% 的工作是调用依赖项的方法。而10%是这样的逻辑:

if(!_dependency1.IsAnySomething())
{
     _dependency2.Download();

      var isScriptNeeded = _dependency2.IsScriptNeeded();

      if(isScriptNeeded)
      {
          var res = _dependency3.ExecuteScript();
         _dependency4.SetScriptResult(res.Info, res.Date, res.State);
      }

     _dependency3.Archive();

     _dependency5.DeleteTemp();
}

我已经测试了该命令处理程序的所有依赖项。但是,hat 命令处理程序还包括一些小逻辑,例如是否需要下载文件,或者删除或不删除临时文件等等...

我脑子里有很多问题,比如:

  1. 可能单元测试对这些单元没有意义?集成测试来拯救?因为,测试是否检查所有调用似乎是错误的,比如下载后是否调用DeleteTemp,或者脚本是否执行,或者脚本结果是否正确传递方法SetScriptResult方法。单元测试好吗?
  2. 有什么方法可以重构 class 使其可测试吗?

您也可以对这种方法使用单元测试。这可以通过模拟依赖关系来完成。原理如下:

您使用预定义的结果创建依赖项的模拟(通常通过实现依赖项的接口)。例如,您为 dependency2 提供的实现总是 returns trueIsScriptNeeded().

然后你监视 dependency3 并检查它是否被调用(应该是!)以及 dependency4.SetScriptResult(...) 的参数是否与结果匹配。

对于 C#,有 FakeItEasy 和 Moq 等库,这使得模拟变得轻而易举。使用 xUnit 和 FakeItEasy 的示例代码片段:

using System;
using FakeItEasy;
using Xunit;

public class MyTest
{
    [Fact]
    public void Test() 
    {
        // Create the mock
        var mock = A.Fake<IMyDependency>();
        A.CallTo(() => mock.DoSomething()).Returns(true);

        // Create the class to test
        var sut = new SystemUnderTest(mock);
        sut.DoSomethingElse();

        // Assert that mock.DoSomething() was called
        A.CallTo(() => mock.DoSomething()).MustHaveHappened();
    }
}

Is it GOOD Unit Test?

我担心你必须对这个话题做出自己的决定。

据我所知,并不是每个人都同意 UT 应该涵盖哪些内容。我会说这还取决于您的团队、您的公司以及您真正想要实现的目标。

  • 有些人盲目地测试一切,不管怎样,因为你永远不知道下一个错误会是什么。他们可能是对的。 在您的情况下,他们将为每个依赖项进行多个模拟,以测试通过和未通过的案例。

  • 有些人认为它太昂贵并且无法证明单一测试高层协调类,就像你在这里指出的那样。他们可能也是对的。

我非常有信心不这样做的唯一理由是成本。或者至少我不知道任何其他的。如果您的高级 UT 维护成本低,没有理由不这样做。

不幸的是,在很多情况下(在我看来,但我也同意它并不适用于所有地方)模拟很难维护并且使 UT 变得毫无价值。

例如(因为我不想被误解),如果您的依赖项使用低级对象,如套接字或文件,这会导致竞争条件 and/or 意外延迟(在多线程环境),你不能以它们帮助你检测错误的方式模拟它们:你应该模拟那些竞争条件和延迟。

你会尝试模仿它们吗,你会在模拟开发和维护上花费很多精力,你最好增加你的集成测试,以检测真实环境中的错误。

如果您不模拟它们,您的 UT 将不会提供任何安全性,因为可以肯定的是,这些错误将来自您没有模拟的那些竞争条件。

最后,我想指出,这个问题也有一个'teaching'方面。你可能想要实现那些高级 UT,使用模拟,即使它们不能阻止任何事情......只是因为你希望你的同事在允许他们自己打破它之前了解 UT 的一般规则是如何工作的。

我希望我有更多线索。 祝你好运。

更新;

既然有责任调用和协调其他东西, 您可以处理所有依赖项并验证它们是否以正确的顺序被调用,并且根据需要多次调用。类似于 Moq 在 mock 类.

上提供的 Verify 方法
        // Explicitly verify each expectation...
        mockSomeClass.Verify(s => s. SetScriptResult(It.IsAny<YourType>()/* or whatever value parameters you may setup snd ecpect it to be called*/), Times.Once());

我相信你在这里所做的和提到的对于单元测试来说已经足够了,因为单元测试的职责是在假设其他互连单元正常工作的情况下测试一个单元,此外单元测试应该在几分之一秒内完成,这样当你或者你的队友做了一些改动的时候,可以快速的验证这个改动是不是破坏了现有的功能,所以快点离开是一个UT的严肃要求。

话虽如此,我想你想做的是确保所有这些单元都能很好地协同工作。这提示您可能需要一些集成测试来检查此功能是否正常工作。

取决于系统的复杂性、并行用户的数量、要求等等,您可以决定是否需要某种负载测试。

我应该强调我说的是单元测试作为一种技术,您可以使用单元测试工具编写集成和负载测试作为 tool/medium。我建议看一下像 Specflow 这样的 BDD 框架,恕我直言,这将有助于更好地描述和演示实际中的问题和解决方案。 (再次将 BDD 框架作为媒体而不是过程。)

您有两个 if 语句。您需要根据 if 语句测试您的代码是否调用了正确的依赖方法。

测试顺序方法调用似乎是错误的。对于那种情况,我建议你涵盖更糟糕的情况。例如,如果 _dependency2.Download(); 抛出异常怎么办?那么

_dependency3.Archive();
_dependency5.DeleteTemp();

方法不应该被调用对吧?这是您需要测试的案例。

那些异常的回滚操作是什么?如果您有回滚操作,那么您需要测试它们是否被调用,以防抛出异常。

关于参数,你不应该在这里测试它们。您需要测试 _dependency3.ExecuteScript() 方法是否 returns 正确的参数。

单元测试应该测试代码的行为,而不是代码的实现。

考虑单元测试如何增加价值是有帮助的:它们传达代码的预期行为,并验证预期行为是否由实现生成。它们在您的项目生命周期中两次增加价值:第一次是代码最初实现时,第二次是代码重构时。

但是,如果单元测试与特定实现紧密相关,则它们无法在重构时增加价值。

这从来都不是一门完美的科学,但是了解您是在测试行为还是在测试实现的一种方法是询问"will this unit test break if I refactor?"如果重构会破坏测试,那么它就不是一个好的单元测试。

编写单元测试以简单地确保调用方法 A,然后是方法 B,然后是方法 C(或其他)通常没有帮助。那只是要测试你的实现是否是你的实现,它很可能会阻碍而不是帮助下一个想要重构代码的开发人员。

相反,请考虑行为以及您的代码如何与其他对象交互。尝试将这些行为中的每一个梳理成单独的对象,并单独测试这些对象。

例如,您可以将上述代码分解为三种不同的行为:

  1. 检查值是否不存在然后调用工厂创建它的缓存对象,
  2. 创建空目录的工厂对象,调用生成器对象填充它,然后压缩和删除它
  3. 一个构建器对象,它将文件下载到一个目录并运行它在那里找到的脚本。

其中每个对象都有单独可测试的行为:

class Cache {
    Cache(ValueStore store, ValueFactory factory) { ... }

    object GetValue(object key) {
        if (!store.HasValue(key))
            factory.CreateValue(key);
        return store.GetValue(key);
    }
}

class CacheTest {
   void GetValue_CallsFactory_WhenValueNotInStore() {
      // arrange
      var store = Mock.Of<VaueStore>(_ => _.HasValue() == false);
      var factory = Mock.Of<ValueFactory>();
      var cache = new Cache(store, factory);

      // act
      cache.getValue();

      // assert
      Mock.Get(factory).Verify(_ => _.CreateValue(), Times.Once());
   }
}

你可以对工厂和建造者进行类似的分解,并分别测试他们的行为。