代码测试简介
本节阅读量:现在,您已经编写了一个程序,它可以编译,甚至看起来可以工作!现在怎么办?
嗯,这要看情况。如果您编写了只运行一次并丢弃的程序,那么工作就算基本完成了。在这种情况下,程序不能在每个情况下都工作可能并不重要——如果它在您需要的一个情况下工作,并且只运行一次,那么事情就完成了。
如果您的程序是完全线性的(没有条件判断,例如If语句或switch语句),不接受输入,并产生正确的答案。在这种情况下,您已经通过运行整个程序并验证输出来测试了它。但您可能希望在几个不同的系统上编译和运行程序,以确保其行为一致(如果不一致,可能是您的代码产生了未定义的行为,而这些行为恰好在您的初始系统上工作)。
但最有可能的是,您编写了一个打算多次运行的程序,该程序使用循环和条件判断逻辑,并接受某种类型的用户输入。可能也编写了可以在其他未来程序中重用的函数。或者是有一些新的需求,向程序添加了一些最初没有计划的新功能。也许甚至您打算将这个程序分发给其他人(他们可能会将程序用于你没有想到的事情上)。在这种情况下,您确实应该验证您的程序在各种条件下是否像您认为的那样工作——这需要一些主动测试。
仅仅因为程序对一组输入有效,并不意味着它在所有情况下都将正确工作。
软件测试(也称为软件验证)是确定软件是否实际按预期工作的过程。
测试所面临的挑战
在讨论测试代码的一些实用方法之前,先来谈谈为什么全面测试程序是困难的。
考虑这个简单的程序:
|
|
假设输入是一个4字节的整数,使用每个可能的输入组合,显式的测试该程序将需要运行程序18446744073709551616(~1.8千亿亿)次。显然,这不是一项可行的任务!
每次用户输入,或在代码中具有条件时,程序可能的输入都会按指数级增长。除了最简单的程序之外的所有程序,几乎立即不可能显式测试每个输入组合。
现在,直觉应该告诉我们,真的不需要运行上面的程序1.8千亿亿次来确保它工作。可以合理地得出结论,如果情况1适用于x>y的一对x和y值,则它应该适用于x>y的任何一对x和y。考虑到这一点,很明显,确实只需要运行该程序大约三次(每次来验证函数compare()中三种情况中的每一种),以获得高置信度,可以确保它可以按预期工作。我们还可以使用其他类似的技巧来显著减少我们必须测试某些东西的次数,以便使测试易于管理。
关于测试方法有许多可以写的东西——事实上可以写一整章来讨论它。但由于它不是C++特定的主题,因此这里只做一个简短的非正式介绍,从开发人员测试自己的代码的角度进行讲解。在接下来的几个小节中,将讨论在测试代码时应该考虑的一些实际问题。
在小代码段中测试程序
考虑一家正在制造定制概念车的汽车制造商。你认为他们会做以下哪一项?a) 在安装之前,单独构建(或购买)和测试每个汽车组件。一旦组件被证明可以工作,将其集成到汽车中并重新测试,以确保集成工作正常。最后,测试整个汽车,作为最后的验证,一切似乎都很好。b) 一次性用所有部件制造一辆汽车,然后再第一次测试整个汽车。
很明显,选项a是一个更好的选择。然而,许多新程序员编写的代码类似于选项b!
在案例b中,如果任何汽车部件不能按预期工作,将不得不诊断整个汽车,以确定是什么问题——但问题可能在任何地方。相同的症状可能有许多原因——例如,汽车是否由于火花塞、蓄电池、燃油泵或其他故障而无法启动?这会导致浪费大量时间去试图准确地确定问题所在,以及如何处理它们。发现问题的后果可能是灾难性的——一个地方的变化可能会在多个其他地方造成“连锁反应”(变化)。例如,燃油泵太小可能导致发动机重新设计,从而导致车架重新设计。在最坏的情况下,可能最终会重新设计汽车的很大一部分,只是为了适应最初的小问题!
在案例a中,公司在运行过程中进行测试。如果任何部件在出厂时就坏了,就会立即知道并可以修复/更换它。在证明其自身工作之前,没有任何东西集成到汽车中。确定正常后,再将该部件集成到汽车后立即重新测试。这样,任何意外问题都会尽早发现,而它们仍然是很容易修复的小问题。
当开始组装整个汽车时,应该有合理的信心相信汽车会工作——毕竟,所有部件都已经在独自和最初集成时进行了测试。此时仍然可能会发现意外的问题,但通过所有先前的测试,风险被最小化了。
上面的类比也适用于程序,尽管出于某种原因,新程序员通常没有意识到这一点。最好编写小函数(或类),然后立即编译和测试它们。这样,如果你犯了一个错误,你就会知道它一定是在你上次编译/测试以来修改的少量代码中。这意味着需要查看的地方更少,调试所花费的时间也少得多。
单独测试代码的一小部分以确保代码的“单元”是正确的,这称为单元测试(unit testing)。每个单元测试都旨在确保单元的特定行为是正确的。
如果程序简短并接受用户输入,则尝试各种用户输入可能就足够了。但随着程序变得越来越长,这就变得不够了,在将单个函数或类集成到程序的其余部分之前测试它们更有价值。
那么我们如何在单元中测试代码呢?
最佳做法
用小的、定义良好的单元(函数或类)编写程序,经常编译,并在单元完成时测试代码。
非正式测试
测试代码的一种方法是在编写程序时进行非正式测试。在编写代码单元(函数、类或其他离散的代码“包”)后,可以编写一些代码来测试刚刚添加的单元,然后在测试通过后删除测试代码。例如,对于以下isLowerVowel()函数,可以编写以下代码:
|
|
如果结果返回为1和0,那么就可以基本认定isLowerVowel没有问题。您知道您的函数适用于一些基本情况,并且可以通过查看代码合理地推断,它将适用于未测试的情况(“e”、“i”、“o”和“u”)。因此,可以删除该临时测试代码,并继续编程。
保留测试代码
尽管编写临时测试是测试某些代码的快速而简单的方法。但它没有考虑到这样一个事实,即在某个时刻,您可能希望稍后再次测试相同的代码。也许您修改了一个函数来添加新的功能,并希望确保没有破坏任何已经工作的功能。由于这个原因,保存测试更合理,以便将来可以再次运行它们。例如,您可以将测试移动到testVowel()函数中,而不是删除临时测试代码:
|
|
更有更多的测试样例时,您可以简单地将它们添加到testVowel()函数中。
自动化测试函数
上述测试函数的一个问题是,它依赖于您在运行时手动验证结果。这需要您记住每个预期的输出结果,并手动将实际结果与预期结果进行比较。
通过编写一个包含测试和预期答案的测试函数,并对它们进行比较,我们可以做得更好,因此不必这样做。
|
|
现在,您可以随时调用testVowel()来重新证明您没有破坏任何东西,测试函数将为您完成所有工作,返回“一切正常”信号(返回值0)或未通过的测试编号。当测试未通过时,您可以明确的知道原因。当返回并修改旧代码时,这特别有用,可以确保您没有意外地破坏任何东西!
对于高级读者
更好的方法是使用断言,如果任何测试失败,它将导致程序中止并显示错误消息。
|
|
我们在后续介绍assert——assert和static_assert。
单元测试框架
由于编写函数来执行其他函数是如此常见和有用,因此有一些完整的框架(称为单元测试框架)旨在帮助简化编写、维护和执行单元测试的过程。由于这些涉及第三方软件,因此不会在这里介绍它们,但您应该知道它们的存在。
集成测试
一旦隔离测试了每个单元,就可以将它们集成到程序中并重新测试,以确保它们正确集成。这称为集成测试。集成测试往往更复杂——现在,运行程序几次并抽查集成后的行为就足够了。
