【问题标题】:How to unit test this function without mock asserts?如何在没有模拟断言的情况下对这个函数进行单元测试?
【发布时间】:2016-06-09 12:04:48
【问题描述】:

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

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

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 为假,选项 2 为假

目前该函数不返回任何内容,因为调用了各种事物(单独测试)的服务和编排。为了增加更多挑战,编排调用的结果可能会根据它将在内部获取的设置产生不同的副作用。

以前,我通过模拟服务和编排并断言该函数被“调用”来完成对这样一个函数的测试。但是,我不是这个的忠实粉丝,因为模拟很乏味并且测试非常脆弱,因为内部函数更改很容易破坏测试。

【问题讨论】:

  • “我在问单元测试是如何工作的,但我基本上不想进行单元测试,因为它很乏味,需要根据实际代码进行更改”

标签: unit-testing


【解决方案1】:

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

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

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

假设您的Process (options) 函数确实是这样的入口点,您现在有四个场景要测试(选项1 和选项2 的四种可能组合)。因此,您调用Process (options) 并通过检查您需要检查的任何内容(文件系统、数据库、事件......)来检查您的每个服务和编排都做了什么。如果您不想模拟您的服务,则没有其他方法。

模拟很乏味

也许是这样,但世界上的一切有时都很乏味。谁说编程一直都是有趣且具有挑战性的?好消息是,您可以这样做一次,而无需再考虑它,至少如果您编写了正确的测试夹具。如果您仍然需要大量调整依赖项,则说明您的系统设计不正确。

内部函数更改很容易破坏测试

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

您可能想拿起一本关于您使用的任何语言的单元测试的书。

【讨论】:

    【解决方案2】:

    问题

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

    哪种测试风格更适合取决于使用的编程语言、系统架构和个人喜好。

    我也更像是一个经典的测试人员,虽然我有时也会严重依赖模拟来简化测试。

    解决方案

    话虽如此,您的问题的解决方案可能是在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 实例或其依赖项的状态

    【讨论】:

    • 感谢详细的回答。这是我和我的团队认为的一个潜在解决方案。
    猜你喜欢
    • 2022-11-17
    • 1970-01-01
    • 2014-07-29
    • 2022-10-19
    • 1970-01-01
    • 1970-01-01
    • 2013-04-08
    • 1970-01-01
    • 2010-12-08
    相关资源
    最近更新 更多