偶尔我会遇到一个不断言任何东西的单元测试。我今天早上遇到的特定示例是测试在满足条件时写入日志文件。假设是,如果没有抛出错误,则测试通过。
我个人对此没有任何问题,但是编写一个没有任何相关断言的单元测试似乎有点“代码味道”。
只是想知道人们对此有何看法?
偶尔我会遇到一个不断言任何东西的单元测试。我今天早上遇到的特定示例是测试在满足条件时写入日志文件。假设是,如果没有抛出错误,则测试通过。
我个人对此没有任何问题,但是编写一个没有任何相关断言的单元测试似乎有点“代码味道”。
只是想知道人们对此有何看法?
这只是一个非常简单的测试,应该这样记录。它只验证它在运行时不会爆炸。像这样的测试最糟糕的部分是它们呈现出一种错误的安全感。你的代码覆盖率会上升,但这是虚幻的。非常难闻的气味。
这将是官方的做法:
// Act
Exception ex = Record.Exception(() => someCode());
// Assert
Assert.Null(ex);
如果没有断言,则不是测试。
别再偷懒了——可能需要一点时间来弄清楚如何在那里获得断言,但是知道它做了你期望它做的事情是值得的。
这样的测试有味道。它应该检查文件是否被写入,至少修改时间可能已更新。
我见过很多以这种方式编写的测试,但最终根本没有测试任何东西,即代码不起作用,但它也没有崩溃。
如果您有一些明确的要求,即被测代码不会引发异常,并且您想明确指出这一事实(测试作为需求文档),那么我会做这样的事情:
try
{
unitUnderTest.DoWork()
}
catch
{
Assert.Fail("code should never throw exceptions but failed with ...")
}
...但这对我来说仍然有点味道,可能是因为它试图证明是负面的。
这些被称为烟雾测试并且很常见。它们是基本的健全性检查。但它们不应该是你唯一的测试类型。您仍然需要在另一个测试中进行某种验证。
这可能是一个很好的务实解决方案,特别是如果替代方案根本没有测试。
问题是,如果所有调用的函数都是无操作的,那么测试就会通过。但有时验证副作用是否符合您的预期是不可行的。在理想的世界里,有足够的时间为每个测试写检查......但我不住在那里。
我使用此模式的另一个地方是在单元测试中嵌入一些性能测试,因为这是让它们在每次构建时运行的简单方法。测试不断言任何东西,但测量测试花费了多长时间并记录下来。
从某种意义上说,您正在做出隐式断言 - 代码不会引发异常。当然,实际抓取文件并找到合适的行会更有价值,但我认为有总比没有好。
一般来说,我在集成测试中看到了这种情况,只是成功完成的事情就足够了。在这种情况下,我很冷静。
我想如果我在单元测试中一遍又一遍地看到它,我会很好奇这些测试到底有多有用。
编辑:在 OP 给出的示例中,有一些可测试的结果(日志文件结果),因此假设如果没有抛出错误,那么它的工作是惰性的。
我们一直这样做。我们使用 JMock 模拟我们的依赖关系,所以我猜在某种意义上 JMock 框架正在为我们做断言......但它是这样的。我们有一个要测试的控制器:
Class Controller {
private Validator validator;
public void control(){
validator.validate;
}
public setValidator(Validator validator){ this.validator = validator; }
}
现在,当我们测试 Controller 时,我们不想测试 Validator,因为它有自己的测试。所以我们使用 JMock 进行了测试,以确保我们调用了 validate:
public void testControlShouldCallValidate(){
mockValidator.expects(once()).method("validate");
controller.control;
}
仅此而已,没有“断言”可看,但是当您调用控制并且未调用“验证”方法时,JMock 框架会向您抛出异常(类似于“未调用预期方法”之类的东西)。
我们到处都有。这有点倒退,因为您基本上设置了断言然后调用测试方法。
我以前见过这样的事情,我认为这样做只是为了支持代码覆盖率。它可能并没有真正测试代码行为。无论如何,为了清楚起见,我同意它(意图)应该记录在测试中。
我有时会使用我选择的单元测试框架 (NUnit) 来构建充当我代码特定部分的入口点的方法。这些方法对于分析代码子集的性能、内存消耗和资源消耗非常有用。
这些方法绝对不是单元测试(即使它们被标记为[Test]属性),并且在它们被检入源代码控制时总是被标记为被忽略并明确记录。
我也偶尔将这些方法用作 Visual Studio 调试器的入口点。我使用 Resharper 直接进入测试,然后进入我想要调试的代码。这些方法要么不能达到源代码控制,要么它们获得了自己的断言。
我的“真实”单元测试是在正常的 TDD 周期中构建的,它们总是断言一些东西,尽管并不总是直接的——有时断言是模拟框架的一部分,有时我能够将类似的断言重构为一个方法。这些重构方法的名称总是以前缀“Assert”开头,以使其对我来说很明显。
测试的名称应记录这一点。
void TestLogDoesNotThrowException(void) {
log("blah blah");
}
测试如何验证日志是否在没有断言的情况下写入?
我不得不承认我从来没有写过一个单元测试来验证我的日志记录是正确的。但我确实考虑过,并遇到了关于如何使用 JUnit 和 Log4J 完成它的讨论。它不太漂亮,但看起来它会起作用。
测试应该总是断言一些东西,否则你要证明什么,你如何能一致地重现你的代码有效的证据?
我会说没有断言的测试表明以下两件事之一:
这个线程中的大多数评论都是关于第 1 件事的,我同意如果被测代码有任何重要的行为,那么应该可以编写测试来断言该行为,或者通过
如果被测代码具有重要的行为,但没有断言该行为的正确性,那么测试是有缺陷的。
您的问题似乎属于此类别。被测代码应该在满足条件时记录。所以至少有两个测试:
测试需要一种方法来安排代码的状态,以便满足或不满足条件,并且需要一种方法来确认是否发生了日志记录,可能有一些日志记录“测试替身”刚刚记录了日志调用(人们经常为此使用模拟框架。)
那么那些缺少断言的其他测试怎么样,但这是因为被测代码没有做任何重要的事情?我会说需要一个判断电话。在具有高代码速度(每天许多提交)和许多同时贡献者的大型代码库中,有必要在小提交中增量地交付代码。这是这样的:
在这些情况下,我添加了“占位符”类,它们没有做任何有趣的事情,但为接下来的实现提供了结构。现在添加这个类,甚至在其他类中使用它,可以帮助向审阅者展示这些部分如何组合在一起,即使新类的重要行为尚未实现。
那么,如果我们假设这些占位符适合添加,我们应该测试它们吗?这取决于。至少,您需要确认该类在语法上是有效的,并且可能它的任何附带行为都不会导致未捕获的异常。
举些例子:
我个人认为,这种推理支持在代码库中临时包含无断言测试。也就是说,这种情况应该是暂时的,占位符类应该很快就会收到更完整的实现,或者应该将其删除。
最后一点,我认为仅仅为了满足“所有测试都必须有断言”的形式主义而包含关于偶然行为的断言不是一个好主意。您或其他作者可能会忘记删除这些形式主义的断言,然后它们会用非必要行为的断言使测试变得混乱,从而分散对重要断言的关注。我们中的许多人可能都熟悉当你遇到一个测试的情况,你看到一些看起来不属于它的东西,我们说,“我真的很想删除它......但这没有任何意义为什么它在那里。所以它可能因为一些潜在的模糊和重要原因而存在,原作者忘记记录。我可能应该留下它,以便我 1)尊重原作者的意图,以及 2)不要切斯特顿的栅栏。)