14

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

[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认为完全未知是一种非常不可能的情况,因为我们在编写测试时有来源。不过,我问这个问题的原因是想找到一种方法来防止后来添加的副作用。也就是说,当我编写测试时,我的 exension 方法可能如下所示:

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

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

4

10 回答 10

14

这就是《有效使用遗留代码》中所谓的感知。也就是感知调用被测方法的效果。

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

由于我们在谈论单元测试,这几乎不应该是真的——单元测试是白盒测试,代码(应该)在那里供您检查。除非它在某些封闭源代码的第 3 方库中,在这种情况下,您不需要测试它,不能根据定义对其进行单元测试(也许您需要功能/验收测试,但那是完全不同的事情......) .

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

  1. 不能,
  2. 不应该。

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

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

于 2010-07-30T12:32:25.620 回答
4

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

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

其他的?

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

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

于 2010-12-10T03:08:16.897 回答
2

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

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

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

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

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

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

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

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

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

它仍然是单元测试。

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

  • 你用 PokeAround 的模拟来测试使用 PokeAround 的东西。

于 2010-07-30T12:41:39.310 回答
2

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

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

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

于 2010-07-30T12:50:37.427 回答
1

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

[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);
}
于 2010-12-10T02:48:02.320 回答
0

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

于 2010-07-30T12:33:25.603 回答
0

我不确定你能不能。毕竟,这是预期效果还是副作用,取决于设计者的意图。

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

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

于 2010-07-30T12:34:31.917 回答
0

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

无论如何,我知道在单个单元测试中没有特定的技术/技术来防止副作用,但是只要您对所有代码都有测试覆盖,任何不需要的副作用都有望被拾取至少一项测试。

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

您可能想要查看的一件事是按合同设计(DBC)。这使您可以指定前置条件和后置条件以及不变量,以便如果使用无效参数调用方法、返回无效值或对象进入无效状态,则会引发某种错误。

于 2010-07-30T12:35:04.767 回答
0

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

没有完整的回归测试就找不到副作用。

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

希望能帮助到你。

于 2010-07-30T12:35:31.397 回答
0

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

Google Test (for C++) 可以做到这一点;我不知道其他具有此功能的框架。

于 2010-07-30T12:43:32.037 回答