首页 > 解决方案 > 如何对具体实现进行单元测试

问题描述

我正在阅读很多有关(单元)测试的内容,并且我尝试在我的日常工作流程中尽可能多地实施,但不知何故我觉得我做错了什么。

假设我有一个函数,它采用路径并根据该路径的某些元素为新的日志文件创建一个名称。路径可能是C:/my_project/dir_1/message01并且应该将其转换为dir_1_log_01.txt. 该函数的名称是convertPathToLogfileName

如果我想为这个函数编写一个单元测试,它可能看起来像:

def test_convertPathToLogfileName():   
    path = "C:/my_project/dir_1/message01"

    expected = "dir_1_log_01.txt"
    actual = convertPathToLogFileName(path)

    assertEqual(expected, actual)

现在我可以写一堆这样的测试来检查所有不同类型的输入,如果输出是我期望的。

但是,如果有一次我决定我选择的日志文件的命名约定不再是我想要的:我会更改函数以使其实现我的新要求,并且所有测试都会失败。

这只是一个简单的例子,但我觉得经常是这样。当我在编程时,我想出了一种新的做事方式,然后我的测试失败了。

这里有什么我遗漏的东西吗,我是在测试错误的东西吗?如果是这样,您将如何处理这种情况?还是这就是现状,我应该接受吗?

标签: unit-testingtestingtddsoftware-designsoftware-quality

解决方案


我在这里缺少什么吗?

几件事。

一是您不一定需要所有测试来指定主题的确切行为。从某种意义上说,断言两个表示完全相等是一个很好的起点simplest thing that could possibly work,但这不是您唯一的选择。拥有一组每个都满足某些约束的测试可能同样有效——然后,当您对预期行为进行小幅更改时,您只需在测试中进行小幅更改。

另一个是模块的设计;见 [Parnas 1971]。这里的基本思想是每个模块都以一个决策为模型,如果我们改变一个决策,我们就替换那个模块。模块边界就像改变的隔板。

在您的示例中,可能至少有两个模块

path = "C:/my_project/dir_1/message01"
expected = "dir_1_log_01.txt"

这看起来很像您需要一个parse函数来从路径中提取有趣的信息,以及一些apply to template函数来对提取的信息做一些有趣的事情。

这可能允许您编写一个断言,例如

assertEquals(
    applyTemplate("dir_1", "01"),
    convertPathToLogFileName(path)
)

然后你可能有的其他地方,比如

assertEquals(
    "dir_1_log_01.txt",
    applyTemplate("dir_1", "01")
)

当你以后决定你的拼写应该改变时,你只需要改变第二个断言。有关此想法的更多信息,请参阅 James Shore,Test without Mocks 。

在测试驱动的世界中经常发生的事情是,在发现我们需要改变一些行为之后,我们将围绕我们将要改变的决定进行重构以创建一个模块,并引入一条路径,我们可以通过该路径配置哪个模块参与在我们的系统中——所有这些更改都可以在不破坏任何现有测试的情况下进行。然后我们开始引入描述替换模块的新测试,以及它如何与您的解决方案的其余部分交互。

如果您看上面,您会发现我通过介绍applyTemplate您仅有的地方来暗示这一点convertPathToLogFileName- 我重构 convertPath 以生成 applyTemplate 函数,现在我可以以本地包含的方式更改系统的行为。

当我们有大量的过拟合测试时,这并不能拯救我们;我们不会永远被特定的测试实现所束缚。相反,我们会查看那些使更改实现变得困难的测试,并考虑如何修改测试设计以使未来的更改更容易。

也就是说,预计会有一定程度的返工——我们将来需要更改哪些代码的最好证据就是我们现在需要更改哪些代码。对于过度拟合的测试,不改变的函数会很好。我们希望将设计资金投入到我们定期修改的代码部分。


推荐阅读