我阅读了很多关于(单元)测试的文章,并且我尝试在我的日常工作流程中尽可能多地实现,但不知何故我觉得我做错了什么。
假设我有一个函数,它接受一条路径,并根据这条路径的某些元素为新日志文件创建一个名称。路径可以是 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)
现在我可以编写一堆这样的测试来检查所有不同类型的输入是否符合我的预期。
但是,如果在某一时刻我决定我选择的日志文件的命名约定不再是我想要的:我将更改函数以使其实现我的新要求,并且所有测试都会失败。
这只是一个简单的例子,但我感觉经常是这样。当我在编程时,我想出了一种做某事的新方法,然后我的测试失败了。
我在这里遗漏了什么,我测试错了吗?如果是这样,您将如何处理这种情况?或者这就是它的方式,我应该接受吗?
最佳答案
Is there something I am missing here?
几件事。
一个是您不一定需要所有测试来指定主题的确切行为。断言两个表示彼此完全相等是一个很好的起点,从可能可行的最简单的事情
的意义上来说,但这不是您拥有的唯一选择。拥有一组满足某些约束条件的测试可能同样有效——然后当您对预期行为进行小的更改时,您只需要在测试之间进行小的更改。
另一个是模块的设计;参见 [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,Testing Without Mocks ,了解更多关于这个想法的信息。
在测试驱动的世界中经常发生的事情是,在发现我们需要改变一些行为后,我们将重构以围绕我们将要改变的决定创建一个模块,并引入一个我们可以配置的路径哪个模块参与了我们的系统——所有这些更改都可以在不破坏任何现有测试的情况下进行。然后我们开始引入新的测试来描述替换模块,以及它如何与您的解决方案的其余部分交互。
如果您看上面,您会发现我通过引入 applyTemplate
来暗示这一点,您只有 convertPathToLogFileName
- 我重构 convertPath 以生成 applyTemplate 函数,现在我可以以本地包含的方式更改系统的行为。
当我们有大量 overfitted 时,这并不能拯救我们。测试;我们不会永远被特定的测试实现所束缚。相反,我们会查看使更改实现变得困难的测试,并考虑我们如何修改测试设计以使将来的更改更容易。
也就是说,预计会有一定数量的返工——我们将来需要更改哪些代码的最佳证据是我们现在需要更改哪些代码。不改变的功能将适用于过度拟合测试。我们希望将设计资金投入到我们定期修改的代码部分。
关于unit-testing - 如何对具体实现进行单元测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58289696/