猿问

什么单元测试

我有点困惑我应该在单元测试上投入多少。


假设我有一个简单的功能,例如:


appendRepeats(StringBuilder strB, char c, int repeats)

[此函数会将 char c 重复次数附加到 strB。例如:


strB = "hello"

c = "h"

repeats = 5

// result

strB = "hellohhhhh"

]


对于这个功能的单元测试,我觉得已经有很多可能性了:


AppendRepeats_ZeroRepeats_DontAppend

AppendRepeats_NegativeRepeats_DontAppend

AppendRepeats_PositiveRepeats_Append

AppendRepeats_NullStrBZeroRepeats_DontAppend

AppendRepeats_NullStrBNegativeRepeats_DontAppend

AppendRepeats_NullStrBPositiveRepeats_Append

AppendRepeats_EmptyStrBZeroRepeats_DontAppend

AppendRepeats_EmptyStrBNegativeRepeats_DontAppend

AppendRepeats_EmptyStrBPositiveRepeats_Append

等等等等。 strB 可以为 null 或为空或具有值。c 可以为 null 或具有值 repeats 可以为负数、正数或零

那似乎已经有 3 * 2 * 3 = 18 个测试方法。如果这些函数还需要测试特殊字符、Integer.MIN_VALUE、Integer.MAX_VALUE 等,那么其他函数可能会更多。我的停止线应该是什么?出于我自己的程序的目的,我是否应该假设:strB 只能为空或具有值 c 具有值 repeats 只能为空或正


抱歉打扰了。只是真的很困惑我应该如何偏执地进行一般的单元测试。我应该留在我的假设范围内还是那是不好的做法,我应该为每个潜在案例都有一个方法,在这种情况下,单元测试方法的数量将以指数方式快速扩展。


长风秋雁
浏览 115回答 4
4回答

开满天机

没有正确答案,这是个人意见和感受的问题。但是,我认为有些事情是普遍的:如果您采用测试驱动开发,除非您首先编写了一个失败的单元测试,否则您永远不会编写任何非测试代码,这将指导您编写的测试数量。有了一些 TDD 经验,您就会对此有所了解,因此即使您需要为未经过 TDD 的旧代码编写单元测试,您也可以像经过 TDD 一样编写测试。如果一个类的单元测试太多,则表明该类做了太多事情。然而,“太多”很难量化。但是当感觉太多时,尝试将班级分成更多的班级,每个班级的职责更少。模拟是单元测试的基础——如果不模拟协作者,您测试的不仅仅是“单元”。因此,学习使用模拟框架。检查空值并测试这些检查可能会增加很多代码。如果您采用一种从不生成 null 的样式,那么您的代码就永远不需要处理 null,也不需要测试在那种情况下会发生什么。这也有例外,例如,如果您提供库代码,并希望向调用者提供友好的无效参数错误对于某些方法,属性测试可能是一种通过大量测试来命中代码的可行方法。jUnit@Theory就是其中的一种实现。它允许您测试断言,例如 ' plus(x,y) 为任何正 x 和正 y 返回正数'

慕姐8265434

您开发的测试用例集是黑盒测试设计方法的结果,实际上它们看起来就像您应用了分类树方法一样。虽然在进行单元测试时暂时采用黑盒视角是完全可以的,但将自己局限于黑盒测试可能会产生一些不良影响:首先,正如您所观察到的,您最终可能会得到所有每个输入的可能场景,其次,您可能仍然找不到特定于所选实现的错误。通过(也)采用玻璃盒(又名白盒)视角,您可以避免创建无用的测试:知道您的代码作为第一步处理重复次数为负的特殊情况意味着您不必将此场景与所有其他场景相乘。当然,这意味着您正在利用您对实现细节的了解:如果您稍后更改您的代码,以便在多个地方进行针对负重复的检查,那么您最好也调整您的测试套件。由于似乎对测试实现细节有广泛的关注:单元测试是关于测试实现的。不同的实现有不同的潜在错误。如果您不使用单元测试来查找这些错误,那么任何其他测试级别(集成、子系统、系统)肯定不太适合系统地查找它们——并且在更大的项目中您不希望实现级别的错误逃逸到后期的开发阶段甚至到现场。附带说明一下,覆盖率分析意味着您采用玻璃盒视角,而 TDD 也是如此。然而,测试套件或单个测试不应不必要地依赖于实现细节是正确的——但这与说你根本不应该依赖实现细节是不同的说法。因此,一种看似合理的方法是,进行一组从黑盒角度来看有意义的测试,以及旨在捕获那些特定于实现的错误的测试。后者需要在您更改代码时进行调整,但可以通过各种方式减少工作量,例如使用测试辅助方法等。在您的情况下,从玻璃盒的角度来看,可能会将具有负重复的测试数量减少到一个,还有 null char 情况,也可能是 NullStrB 情况(假设您通过用空字符串替换 null 来尽早处理),等等。

回首忆惘然

一般的经验法则通常是代码中的每个“分支”都应该进行测试,这意味着您应该涵盖所有可能的边缘情况。例如,如果您有以下代码:if (x != null) {  if (x.length > 100) {    // do something    } else {    // do something else  }} else {  // do something completely else}您应该有三个测试用例——一个为空,一个为小于 100 的值,一个为更长的值。这是如果你很严格并且想要 100% 覆盖。无论是不同的测试还是参数化都不那么重要,这更多的是风格问题,你可以选择任何一种方式。我认为更重要的是涵盖所有情况。

森林海

首先,使用代码覆盖工具。这将向您显示测试执行了哪些代码行。IDE 具有用于代码覆盖工具的插件,因此您可以运行测试并查看执行了哪些行。尝试覆盖每一行,这在某些情况下可能很难,但对于这种实用程序来说,它是非常可行的。使用代码覆盖工具可以使未发现的边缘情况脱颖而出。对于难以实现的测试,代码覆盖率会向您显示您的测试执行了哪些行,因此如果您的测试中出现错误,您可以看到它进行了多远。接下来,了解没有测试涵盖所有内容。总会有您不测试的值。因此,选择感兴趣的代表性输入,避免那些看起来多余的输入。例如,传递一个空的 StringBuilder 真的是您关心的事情吗?它不会影响代码的行为。有一些特殊值可能会导致问题,例如 null。如果您正在测试二分查找,您需要覆盖数组非常大的情况,以查看中点计算是否溢出。寻找重要的案例类型。如果您预先验证并剔除麻烦的值,您就不必进行那么多的工作测试。一项针对 null StringBuilder 的测试是为了验证您抛出了 IllegalArgumentException,一项针对负重复值的测试是为了验证您为此抛出了一些东西。最后,测试是针对开发人员的。做对你有用的事。
随时随地看视频慕课网APP

相关分类

Java
我要回答