单元测试应该如何记录文档?

29

我正在尝试提高Python项目中测试用例的数量和质量。随着测试用例数量的增加,我遇到的一个困难是知道每个测试用例的作用以及它如何帮助检测问题。我知道跟踪测试用例的一部分是更好的单元测试名称(已在其他地方解决),但我也对了解文档和单元测试如何结合感兴趣。

如何记录单元测试,以在将来这些测试失败时提高其效用?具体而言,什么样的单元测试文档字符串才是好的?

我期待有描述性的答案和具有优秀文档描述的单元测试示例。虽然我只使用Python,但我可以接受其他语言的做法。

5个回答

17

我主要使用方法名来记录我的单元测试:

testInitializeSetsUpChessBoardCorrectly()
testSuccessfulPromotionAddsCorrectPiece()

对于近乎100%的我的测试用例,这清楚地解释了单元测试正在验证什么,这就是我使用的全部内容。然而,在一些更复杂的测试用例中,我会在方法中添加一些注释来解释几行代码正在做什么。

我曾经见过一个工具(我相信它是针对Ruby的),它通过解析项目中所有测试用例的名称生成文档文件,但我不记得它的名称了。如果您有一个棋后类的测试用例:

testCanMoveStraightUpWhenNotBlocked()
testCanMoveStraightLeftWhenNotBlocked()

该工具将生成一个类似以下内容的HTML文档:
Queen requirements:
 - can move straight up when not blocked.
 - can move straight left when not blocked.

你的函数名字怎么了?我猜你把测试命名为“testFunctionName”,这还可以,但是你居然有一个叫做InitializeSetsUpChessBoardCorrectly的函数?我认为“setUpChessboard”就足够了。 - Catharsis
13
不,方法名称已经清楚地解释了它正在测试什么——那个测试用例验证initalize()函数是否正确设置了棋盘。这就是自动文档化。 - Kaleb Brasee
哈哈,是的,开头的“test”只是来自JUnit的旧日子,我的大脑还停留在那个时候。我可以将其命名为initalizeSetsUpChessBoardCorrectly()并使用@Test注释。 - Kaleb Brasee
@Kaleb:我也是这样做的。:) “setUpChessboard” 应该是被测试类中的一个方法。 - Bill the Lizard
Python的单元测试框架本质上是Python版本的JUnit,这就是为什么我们仍然使用“test”前缀来表示测试方法的原因。但即使它没有意义,我也可能会这样做 :) - Chris Lacasse
我也会在所有测试方法前加上 test_ 前缀。不过我使用的是 nosetest,它依赖于方法名中出现单词 'test' 来进行单元测试发现。http://somethingaboutorange.com/mrl/projects/nose/0.11.1/ - Mike Mazur

15
也许问题不在于如何最好地编写测试文档字符串,而是如何编写测试本身?重构测试以使其自我记录可以大有裨益,当代码发生变化时,您的文档字符串将不会过时。
有几件事情可以做,使测试更清晰:
- 清晰且具有描述性的测试方法名称(已经提到) - 测试主体应清晰简明(自我记录) - 在方法中抽象出复杂的设置/拆卸等。 - 更多?
例如,如果您有这样的一个测试:
def test_widget_run_returns_0():
    widget = Widget(param1, param2, "another param")
    widget.set_option(true)
    widget.set_temp_dir("/tmp/widget_tmp")
    widget.destination_ip = "10.10.10.99"

    return_value = widget.run()

    assert return_value == 0
    assert widget.response == "My expected response"
    assert widget.errors == None

你可以用方法调用来替换设置语句:
def test_widget_run_returns_0():
    widget = create_basic_widget()
    return_value = widget.run()
    assert return_value == 0
    assert_basic_widget(widget)

def create_basic_widget():
    widget = Widget(param1, param2, "another param")
    widget.set_option(true)
    widget.set_temp_dir("/tmp/widget_tmp")
    widget.destination_ip = "10.10.10.99"
    return widget

def assert_basic_widget():
    assert widget.response == "My expected response"
    assert widget.errors == None

请注意,您的测试方法现在由一系列具有意图明确名称的方法调用组成,这是一种特定于测试的DSL。像这样的测试仍然需要文档吗?
另一个要注意的问题是,您的测试方法主要在一个抽象级别上。阅读测试方法的人将看到算法是:
- 创建小部件 - 在小部件上运行 - 断言代码执行了我们期望的操作
他们对测试方法的理解不会被设置小部件的细节所混淆,这比测试方法低一级。
测试方法的第一个版本遵循内联设置模式。第二个版本遵循创建方法委托设置模式。
通常我反对注释,除非它们解释了代码的“为什么”。阅读 Uncle Bob Martin 的Clean Code使我信服。有一章关于注释,还有一章关于测试。我推荐一下。
有关自动化测试最佳实践的更多信息,请查看xUnit模式

1
谢谢您提供的额外资源,帮助我理解如何简化测试本身。我一定会在这个主题上再多阅读一些。再次感谢! - ddbeck

4
测试方法的名称应该准确描述你正在测试的内容。文档应该说明什么会导致测试失败。

0

在编写文档字符串时,应该使用描述性的方法名称和注释的组合。一个好的方法是在文档字符串中包含基本的过程和验证步骤。然后,如果您从某种自动化运行测试并收集结果的测试框架中运行这些测试,您可以让框架记录每个测试方法的文档字符串内容以及其stdout+stderr。

以下是一个基本示例:

class SimpelTestCase(unittest.TestCase):
    def testSomething(self):
        """ Procedure:
            1. Print something
            2. Print something else
            ---------
            Verification:
            3. Verify no errors occurred
        """
        print "something"
        print "something else"

在测试过程中使用该程序可以更轻松地弄清楚测试的内容。如果您将文档字符串与测试输出一起包含,那么在稍后查看结果时确定出错原因就会变得更加容易。我之前工作的地方也是这样做的,当出现故障时效果非常好。我们自动在每次提交时运行单元测试,使用CruiseControl。


3
这篇文章的主旨是:代码告诉你如何实现,而注释告诉你为什么要这样做。作者认为,好的代码应该清晰易读,并且可以自我解释。然而,即使代码看起来很简单,也有可能存在深奥的原因,需要通过注释来解释。相比之下,糟糕的注释会让代码更加难以理解和维护,因此需要谨慎编写注释。最后,作者提出了一些编写好的注释的建议,例如注释应该准确明了地表达意图,而不是描述代码本身。 - Russia Must Remove Putin

0

当测试失败时(应该在它通过之前),您应该看到错误消息并能够知道出了什么问题。这只有在您计划的情况下才会发生。

这完全取决于测试类、测试方法和断言消息的命名方式。当测试失败时,如果您无法从这三个线索中知道出了什么问题,则重命名一些内容或拆分一些测试类。

如果夹具的名称是ClassXTests,测试的名称是TestMethodX,错误消息是“期望为true,返回为false”,那么这就是测试编写不规范的迹象。

大多数情况下,您不必阅读测试或任何注释就可以知道发生了什么。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接