【问题标题】:Is there a way to unit test against side effects?有没有办法针对副作用进行单元测试?
【发布时间】:2011-03-23 05:30:52
【问题描述】:

任何代码都可能产生副作用。大多数时候,副作用可能是糟糕设计和/或需要重构的标志,但在单元测试时,我发现很难进行测试。考虑以下示例:

[Test]
public void TrimAll_Removes_All_Spaces()
{
    // Arrange
    var testSubject = "A    string  with     lots   of     space";
    var expectedResult = "Astringwithlotsofspace";

    // Act
    var result = testSubject.TrimAll();

    // Assert
    Assert.AreEqual(expectedResult, result);
}

测试以下扩展:

public static string TrimAll(this string str)
{
    PokeAround();

    return str.Replace(" ", "");
}

测试会通过,但没有防范副作用。调用PokeAround 的影响将完全被忽视。

鉴于您不知道 PokeAround 是什么 - 它可以是任何东西! - 你如何编写一个防止它的测试?有可能吗?

说明: 有几个关于 PokeAround 的 cmets 完全未知这是一个非常不可能的情况,因为我们在编写测试时有源代码。不过,我问这个问题的原因是想找到一种方法来防止稍后添加的副作用。也就是说,当我编写测试时,我的扩展方法可能是这样的:

public static string TrimAll(this string str)
{
    return str.Replace(" ", "");
}

测试通过,一切正常。然后,一个月后,当我休假时,一位同事添加了PokeAround 电话。我希望我已经写的测试失败,因为他失败了。

【问题讨论】:

  • 这个问题很荒谬。您无法对无权访问的代码进行单元测试。 “在计算机编程中,单元测试是一种测试各个源代码单元以确定它们是否适合使用的方法。单元是应用程序的最小可测试部分。在过程编程中,单元可以是单独的功能或程序。单元测试由程序员或偶尔由白盒测试人员创建。”
  • @WOPR:查看我的更新,了解与此相关的真实场景。
  • 这仍然是荒谬的。现在,您正试图扩展单元测试的概念,以防止其他程序员出现白痴。您的“未来同事”也应该对他的更改进行单元测试。您不能使用单元测试来测试未来的更改...他/她也可以删除所有代码并将其更改为删除主引导记录。
  • 我想有一种方法 - 对代码文件设置文件权限,以阻止其他编码人员搞乱它。

标签: unit-testing side-effects


【解决方案1】:

这就是Working Effectively With Legacy Code 中所谓的传感。即感知调用被测方法的效果。

鉴于您不知道 PokeAround 是什么 - 它可以是任何东西!

既然我们在谈论单元测试,这应该不是真的 - 单元测试是 whitebox 测试,并且代码(应该)在那里供您检查。除非它在某些封闭源代码的第 3 方库中,在这种情况下,您 不需要测试它 不能按定义对其进行单元测试(也许您需要功能/验收测试,但这是一个完全不同的事情...)。

更新:所以您想确保将来对您的单元测试方法的更改永远不会产生任何意想不到的副作用?我想你

  1. 不能,
  2. 不应该。

您不能,因为没有明智的方法来检测现实生活(非平凡)程序中的方法调用是否缺少副作用。您正在寻找的是检查整个宇宙的状态除了这个和这个小东西之外没有改变。即使从一个不起眼的程序的角度来看,那个宇宙也是浩瀚。方法调用可以创建/更新/删除任意数量的本地对象(其中许多您甚至无法从单元测试环境中看到),在可用的本地/网络文件系统上触摸文件,执行数据库请求,进行远程过程调用。 ..

您不应该这样做,因为由您的同事做出未来的更改来负责对他/她的更改进行单元测试。如果您不相信这会发生,那么您遇到的是人员或流程问题,而不是单元测试问题。

【讨论】:

  • @Péter Török,实际上您确实需要测试第 3 方库。图书馆可以而且很可能会在某个时候改变。通过编写测试来确认您对其工作方式的假设,您可以安全地升级库,并立即知道预期行为是否有任何变化,而不是在用户开始调用时将其投入生产。
  • 我认为最后一句话应该改写为:“除非它在某个 3rd 方库中,否则您不应该需要对其进行测试 :-)” (在一个完美的世界中)。
  • 查看我的更新,了解与此相关的真实场景。
  • @Chad,@strager,很公平,可能需要测试第 3 方库。但这完全超出了 this 具体问题的范围,也不是单元测试。我改写了我的句子,希望能更清楚:-)
  • 当然,如果有一种方法可以在语言中将函数标记为纯函数(并在它变得不纯时进行标记),那么答案会简单得多。 =]
【解决方案2】:

请记住,单元测试只是验证和检查库中的一种工具,其中一些可能会抓住您同事的“PokeAround”:

  1. 单元测试
  2. 代码检查/审查
  3. 合同设计
  4. 断言
  5. FindBugsChecker Framework 等静态分析工具(@NonNull、@Pure 和 @ReadOnly 都很棒!)

其他人?

测试通过,一切正常。然后,一个月后,当我休假时,一位同事添加了 PokeAround 电话。我希望我已经写的测试失败,因为他失败了。

是什么让您认为您的同事也不会更改测试?

【讨论】:

  • 推测是同事无能,没有恶意。
【解决方案3】:

我没有第一手经验,但你可能会感兴趣:代码合同

http://research.microsoft.com/en-us/projects/contracts/

有一项规定,可以将 [pure] 属性添加到方法中,并通过运行时或编译时检查来强制无副作用。它还允许指定和执行一组令人印象深刻的其他约束。

【讨论】:

    【解决方案4】:

    鉴于您不知道 PokeAround 是什么 - 它可以是任何东西! - 你如何编写一个防止它的测试?有可能吗?

    这个问题似是而非。这种情况在现实世界中不太可能发生。

    1. 你总是知道 PokeAround 是什么。这是单元测试。你有来源。

      如果 - 由于某种组织上的邪恶 - 您被禁止阅读源代码,那么您遇到的是组织问题,而不是技术问题。

      如果您不知道 PokeAround 是什么,那么您就会发现有些人特别邪恶并阻碍了成功。他们需要新的工作。或者你做。

    2. 您必须为此 PokeAround 使用 Mocks,以便观察副作用。

    “防止后来添加的副作用。”

    这不是一段神秘代码的例子。你仍然知道 PokeAround 是什么。你总是知道 PokeAround 是什么。

    这就是我们进行回归测试的原因。 http://en.wikipedia.org/wiki/Regression_testing

    它仍然是单元测试。

    • 您仍然使用独立的单元测试来测试 PokeAround。

    • 然后您通过模拟 PokeAround 来测试使用 PokeAround 的东西。

    【讨论】:

    • “你总是知道 PokeAround 是什么。它是单元测试。你有源代码。” -- 如果我一直知道 PokeAround(以及我编写的任何其他函数)是什么这样做我根本没有理由编写单元测试。我只知道他们应该做什么
    • @Luther Blissett:“如果我一直知道 PokeAround(以及我编写的任何其他函数)在做什么,那么我就没有理由编写单元测试”什么?测试不是发现。测试证明它确实做到了它所声称的。测试不是为了发现代码的内在奥秘而做的事情。
    • 查看我的更新,了解与此相关的真实场景。
    • 如果我知道 'X' 是 'Y' 那么我不需要测试这个,因为这意味着我有一个证明(代码属性可以statically 证明,我不需要编写一个单元测试来验证除法例程,如果该例程具有来自定理求解器的正式证明)。如果例程通过了它的测试,那只意味着我不知道“X”是否有参数,而“X”没有“Y”。
    • @Luther Blissett:我不知道您为什么要谈论内置方法(例如除法),除非我们编写除法例程,否则我们不会进行单元测试。你为什么提到这个?许多人使用单元测试来确认他们的静态证明。
    【解决方案5】:

    我不会用这种语言编程,但以下是测试原始字符串是否被修改的一种方法:

    [Test]
    public void TrimAll_Removes_All_Spaces()
    {
        // Arrange
        var testSubject = "A    string  with     lots   of     space";
        var testSubjectCopy = "A    string  with     lots   of     space";
        var expectedResult = "Astringwithlotsofspace";
    
        // Act
        var result = testSubject.TrimAll();
    
        // Assert
        Assert.AreEqual(expectedResult, result);
    
        // Check for side effects
        Assert.AreEqual(testSubjectCopy, testSubject);
    }
    

    【讨论】:

      【解决方案6】:

      您可以换一种方式看待它,通过为此代码编写单元测试,您是在强迫自己承认代码中的问题(副作用)。然后,您可以以可测试的方式重新编写/重组代码,可能通过将 PokeAround 移动到它自己的函数中,或者只是将它从您尝试测试的代码中重新定位,如果它需要更多输入/状态可测试。

      【讨论】:

        【解决方案7】:

        我不确定你能做到。毕竟是预期效果还是副作用,还是要看设计者的意图。

        我建议你断言“副作用”代码没有改变。

        此外,Jester 之类的工具可能会有所帮助,但我认为它不会对您的示例产生影响。

        【讨论】:

          【解决方案8】:

          取决于你所说的副作用 - 我的意思是,也许 PokeAround() 做了一些需要做的重要事情。您如何对副作用进行分类?

          无论如何,我知道在单个单元测试中没有特定的技术/技巧可以防止副作用,但只要您的所有代码都有测试覆盖率,任何不需要的副作用都有望得到至少通过一项测试。

          BDD/集成测试工具也有助于解决这个问题,因为它们(通常)测试更大范围的功能,而不仅仅是单个类/方法。

          您可能想看看Design By Contract (DBC)。这使您可以指定前置条件和后置条件,以及不变量,以便如果使用无效参数调用方法、返回无效值或对象进入无效状态,则会引发某种错误。

          【讨论】:

            【解决方案9】:

            没有是不可能的。在任何测试阶段都很难测试副作用。它在不同的项目(产品开发、工具开发、业务应用程序开发、游戏开发等)中有所不同。

            如果没有完整的回归测试,就无法找到副作用。

            在一个典型的项目中(我经历过),“此更改是否有任何副作用”的问题通常会在项目结束时(即将上线)或当有人想要在已生产系统。我发现如果没有回归测试,唯一(仍然有风险的)质量控制措施就是代码审查。

            希望对您有所帮助。

            【讨论】:

              【解决方案10】:

              一种技术是随机化单元测试的顺序。如果您的测试套件相当全面,那么随机化测试的顺序可能会发现意外的副作用、错误地依赖于先前测试状态的测试等等。

              Google Test(对于C++)可以do this;我不知道其他框架有这个功能。

              【讨论】:

                猜你喜欢
                • 2010-11-13
                • 1970-01-01
                • 2017-03-11
                • 1970-01-01
                • 2022-01-24
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多