有没有办法对副作用进行单元测试?

时间:2010-07-30 12:24:56

标签: unit-testing side-effects

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

[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电话。我希望我已经写过的测试失败了,因为他做了。

10 个答案:

答案 0 :(得分:12)

这就是Working Effectively With Legacy Code中所谓的传感。也就是说,感知调用测试方法的效果。

  

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

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

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

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

    你不应该这样做,因为你的同事要做出未来的改变来照顾单位测试他/她的变化。如果您不相信这种情况会发生,那么您就会遇到人员或流程问题,而不是单元测试问题。

答案 1 :(得分:3)

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

  1. 单元测试
  2. 代码检查/评论
  3. 按合同设计
  4. 断言
  5. 静态分析工具,如FindBugsChecker Framework(@NonNull,@ Pure和@ReadOnly全都摇滚!)
  6. 他人?

      

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

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

答案 2 :(得分:2)

  

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

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

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

    如果 - 通过一些组织上的邪恶 - 你被禁止阅读来源,你就会遇到组织问题,而不是技术问题。

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

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

  3.   

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

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

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

    它仍然是单元测试。

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

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

答案 3 :(得分:2)

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

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

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

答案 4 :(得分: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);
}

答案 5 :(得分:0)

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

答案 6 :(得分:0)

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

我建议你断言“副作用”代码不变。

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

答案 7 :(得分:0)

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

无论如何,在单个单元测试中没有特别的技术/技术可以防止副作用,但只要你对所有代码都有测试覆盖率,任何不需要的副作用都会有希望得到通过至少一次测试获得。

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

您可能想要关注的一件事是Design By Contract(DBC)。这允许您指定前置条件和后置条件,以及不变量,以便如果使用无效参数调用方法,返回无效值,或者对象进入无效状态,则会抛出某种错误。

答案 8 :(得分:0)

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

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

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

希望它有所帮助。

答案 9 :(得分:0)

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

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