【问题标题】:Unit testing, is it a good idea to verify methods are not called单元测试,验证方法不被调用是否是个好主意
【发布时间】:2019-03-20 23:37:50
【问题描述】:

假设您有以下方法要测试:

public void foo(object myObject, bool myBool)
{
     if(myBool)
        repositoryA.save(myObject)
     else
        repositoryB.save(myObject)
}

对此类函数进行单元测试的最佳方法是什么?如果你写 2 个测试 当 myBool 为 true 时调用 assert repositoryA,当 myBool 为 false 时调用 repositoryB,那么对函数的以下更改仍会使测试通过,但可能会破坏应用程序的功能:

public void foo(object myObject, bool myBool)
{
    repositoryA.save(myObject)
    repositoryB.save(myObject)
}

另一方面,如果您断言当 myBool 为 true 时调用 repositoryA 并且不调用 repositoryB,这让您更有信心对函数的任何更改都不会引入错误,但是您有一个测试取决于实现细节。最好的方法是什么?

如果您使用 TDD,您会编写哪些测试以达到所需的功能?

【问题讨论】:

  • 如果您测试其中一个存储库被调用并发现正常,那么调用存储库不是实现细节。因此,测试不调用存储库并不是测试实现细节。也就是说:测试不应该涵盖某人可以在代码中做的所有潜在的奇怪事情。如果您发现潜在的破损有合理的可能性发生,则添加该断言。否则,不要。但请记住,破坏测试的开发人员也可能会更改测试代码。 bast 调用可能有两种方法,而不是一种。
  • 谢谢,有道理
  • 也许不是你的情况,因为你的例子有点“不完整”;我的意思是:有一个更好的命名可以帮助这篇论文。无论如何:您的实现似乎隐藏了多态性(我指的是改变内部行为的布尔参数)。所以,如果你问我“如果你使用 TDD”,我会回答你说有这样的合同有点奇怪。我认为您可以对同一个 Repository 接口有两种不同的实现。但是,我真的不知道“myBool”是什么,所以这可能是一个合理的案例。
  • 我无法修改合同,在我的情况下,我实际上有 9-10 个参数,每个参数都会影响我需要做的事情,坚持 repoA,坚持 repoB,调用 web api,什么都不做,因为它的逻辑实际上真的很复杂。我用一个简化的例子问了这个问题。
  • 测试行为而不是实现。这样你就可以在不改变测试的情况下改变实现——自信地重构。 TDD 将帮助您以可测试、行为驱动的方式设计代码。

标签: unit-testing testing tdd


【解决方案1】:

对此类函数进行单元测试的最佳方法是什么?

从重新设计开始?

首先开发测试的部分原因是声称可测试接口也“更好”;更容易消费,更容易维护。

所以你(正确地)提出的关于测试这种方法的副作用的观点是一种“设计气味”——它暗示这个代码设计可能不是你的用例所需要的。

两种可能:

一个是代码试图告诉您您有遥测需求;您应该能够查询被测系统并了解每个存储库中保存了多少对象,或者每个存储库中保存的最后一个对象是什么,或者类似的东西。

然后你可以利用遥测来编写你的测试

Given:
    telemetry reports that repository B has stored 7 objects
    and myBool is true
When:
    foo()
Then:
    telemetry reports that repository B has stored 7 objects

这基本上将问题分为两部分;确保遥测准确报告存储库保存对象的次数的测试集合,然后是假设遥测正常工作的foo() 测试。

第二种选择:测试试图告诉您您希望能够评估程序中发生的副作用。因此,让这些效果成为您设计中的一等公民,并编写测试来检查效果。

List<Effect> foo () {
    if (myBool) {
        return List.of(SaveInRepositoryA);
    } else {
        return List.of(SaveInRepositoryB);
    }
}

现在您的断言更容易了 - 您只需确保 List 中包含正确数量的元素和正确的元素。

重要提示:这些设计正在做的是在您的逻辑和副作用之间创建一个接缝。想象一个边界,边界的一侧是易于测试的复杂逻辑,因为它是对内存中数据的所有操作;边界的另一边是难以测试的代码,但它非常简单直接,显然没有任何错误。

【讨论】:

  • 我真的很喜欢你的第二种方法。如果您的逻辑变得更复杂,它似乎也可以很好地扩展。谢谢!
【解决方案2】:

坚持你的问题(请阅读我的评论,一般来说可能更有价值),我会写这样的东西:(使用 JMock,一个 Java 模拟库)

// with myBool == true

context.checking(new Expectations(){{
    oneOf(repositoryA).save(object);
    never(repositoryB);
}});

underTest.foo(object, true);




// with myBool == false

context.checking(new Expectations(){{
    never(repositoryA);
    oneOf(repositoryB).save(object);
}});

underTest.foo(object, false);

【讨论】:

    猜你喜欢
    • 2018-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-18
    • 2013-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多