如何在没有模拟断言的情况下对此函数进行单元测试?

时间:2016-06-09 12:04:49

标签: unit-testing

我很好奇有人会如何对下面的伪编码函数进行单元测试,甚至重构以便更容易测试不同的部分。

首先,我们有一个庞大的代码库,在较高的层次上,它被分解为以下项目:

Orchestrations -> Services -> Repositories -> Database  
                           -> Behaviors

我正在使用的当前示例是在业务流程级别,有如下函数:

FUNCTION Process (Options)

     IF Options.Option1 THEN

          IF Service1.HasAnyItems THEN

                Service1.DoSomethingWithThoseItems

          FI

      FI

     IF Options.Option2 THEN

          IF Service2.HasAnyItems THEN

                Service2.DoSomethingWithThoseItems

          FI

      FI

     IF Options.Option1 OR Options.Option2 THEN

          Orchestration2.DoSomething

     FI

END FUNCTION

我立即看到4种不同的测试场景会产生不同的输出:

  1. 选项1为真,选项2为假
  2. 选项2为真,选项1为假
  3. 选项1为真,选项2为真
  4. 选项1为false,选项2为false
  5. 目前,该函数不返回任何内容,因为调用各种事物(单独测试)的服务和编排。为了进一步增加挑战,业务流程调用的结果可能会根据内部提取的设置产生不同的副作用。

    以前,我已经完成了通过模拟服务和编排并断言函数被“调用”来测试这样的函数。但是,由于内部功能的变化很容易打破测试,所以我不是很喜欢这个,因为模拟很乏味而且测试非常脆弱。

2 个答案:

答案 0 :(得分:3)

依赖注入和模拟是 为单元测试做好准备的基本技术。

如果您没有使用模拟,那么您正在查看集成测试而不是单元测试。它们基本上以相同的方式编写(使用您喜欢的任何测试框架),但它们不能通过检查单个函数的作用来工作。

相反,您的测试应该调用系统中的某个入口点(可能是处理Web请求的内容,对UI的某些按钮单击做出反应的部分,无论如何),即用户交互或类似之间的点触发器和需要发生的工作。

假设您的Process (options)函数确实是这样的入口点,您现在有四个要测试的场景(选项1和选项2的四种可能组合)。因此,您可以通过检查需要检查的内容(文件系统,数据库,事件......)来调用Process (options)并检查每个服务和业务流程的功能。如果你不想嘲笑你的服务,别无他法。

  

嘲笑很乏味

也许是这样,但世界上的一切有时都很乏味。谁说节目一直很有趣又充满挑战?好消息是,你可以做一次而不再考虑它,至少如果你写了适当的测试装置。如果您仍然需要对很多依赖项进行随机播放,那么您就不能正确设计系统。

  

内部功能变化很容易打破测试

这是测试的内容之一!它会让你仔细检查你正在做有意义的事情,它们可以帮助你捕捉逻辑错误。此外,测试的“可破坏性”决定了它是否写得好。如果它在您的应用程序逻辑上执行相同的操作时中断,那么这不是一个好的测试。另一方面,如果输出发生变化而你的测试没有中断,那么你也不会这样做。

您可能希望选择一本关于您正在使用的任何语言的单元测试的书。

答案 1 :(得分:1)

问题

我得到你的担忧。验证内部行为而不是输入与输出将测试与实现细节相结合,并在进行重构时使其变脆。 Fowler在他的文章Mocks aren't Stubs中创造了这种测试风格 mockist 测试,他详细解释并比较了mockist和经典测试。

哪种测试方式更合适取决于所使用的编程语言,系统架构和个人偏好。

我更像是一个经典的测试人员,虽然我有时也非常依赖于嘲笑它是否会使测试更简单。

解决方案

话虽如此,问题的解决方案可能是逆转Process()与其客户端之间的控制:不是直接将工作委托给服务,而是让它收集需要的任务要完成并归还它。这样,您就可以对Process的返回值执行常规断言。

在伪代码中:

FUNCTION AssembleProcessingActions (Options) : List OF Action
    actions := NEW List OF Action

    IF Options.Option1 THEN
        actions.Add(Service1.DoSomethingWithItems)
    FI

    IF Options.Option2 THEN
        actions.Add(Service2.DoSomethingWithItems)
    FI

    IF Options.Option1 OR Options.Option2 THEN
        actions.Add(Orchestration2.DoSomething)
    FI

    RETURN actions
END FUNCTION

请注意,我已将支票移至HasAnyItems。我认为它们属于DoSomethingWithItems()方法。

结论

一般来说,如果您的系统设计是功能而不是面向对象,那么它将更容易用于经典测试。

这当然并不意味着您不能再在对象上拥有方法,并且一切都应该是静态实用程序类中的静态函数。 AssembleProcessingActions()可以而且应该是Options类型的方法。关键是它不应该更改Options实例或其依赖项的状态