跳转到内容

软件工程/测试/单元测试简介

来自维基教科书,开放的世界开放的书籍

在计算机编程中,单元测试是一种方法,通过该方法对源代码的各个单元进行测试,以确定它们是否适合使用。单元是应用程序中最小的可测试部分。在过程式编程中,单元可以是单个函数或过程。单元测试由程序员创建,有时也由白盒测试人员创建。

理想情况下,每个测试用例都是独立的:可以使用方法存根、模拟对象、[1] 假象和测试工具来帮助隔离测试模块。单元测试通常由软件开发人员编写和运行,以确保代码满足其设计并按预期运行。它的实现可以从非常手动(纸笔)到作为构建自动化的正式部分。

好处

[edit | edit source]

单元测试的目的是隔离程序的每个部分,并证明各个部分是正确的。[2] 单元测试提供了一个严格的书面契约,代码必须满足该契约。因此,它提供了几个好处。单元测试在开发周期的早期发现问题。

促进变化

[edit | edit source]

单元测试允许程序员在以后重构代码,并确保模块仍然正常工作(例如,在回归测试中)。该过程是对所有函数和方法编写测试用例,以便每当更改导致故障时,都可以快速识别和修复该故障。

随时可用的单元测试使程序员能够轻松地检查代码是否仍然正常运行。

在持续的单元测试环境中,通过持续维护的固有实践,单元测试将继续准确地反映可执行文件和代码的预期用途,以应对任何更改。根据既定的开发实践和单元测试覆盖范围,可以保持最新准确性。在单元测试中,我们分别测试每个模块。

简化集成

[edit | edit source]

单元测试可以减少单元本身的不确定性,并且可以用于自下而上的测试风格方法。通过首先测试程序的各个部分,然后测试其各部分的总和,集成测试变得更加容易。

单元测试的复杂层次结构并不等于集成测试。与外围单元的集成应包含在集成测试中,但不包含在单元测试中。[需要引用] 集成测试通常仍然严重依赖人工手动测试;高级或全局范围的测试可能难以自动化,因此手动测试通常看起来更快更便宜。[需要引用]

文档

[edit | edit source]

单元测试提供了一种关于系统现状的文档。希望了解单元提供的功能以及如何使用它的开发人员可以查看单元测试,以了解单元 API 的基本知识。

单元测试用例体现了对单元成功至关重要的特性。这些特性可以表明单元的适当/不当使用,以及单元要捕获的负面行为。单元测试用例本身记录了这些关键特性,尽管许多软件开发环境并不仅仅依赖于代码来记录正在开发的产品。

相比之下,普通的叙述性文档更容易偏离程序的实现,从而变得过时(例如,设计变更、功能蔓延、保持文档最新的松懈做法)。

设计

[edit | edit source]

当使用测试驱动方法开发软件时,单元测试可以代替正式的设计。每个单元测试都可以被视为一个设计元素,指定类、方法和可观察的行为。以下 Java 示例将有助于说明这一点。

这是一个测试类,它指定了实现中的许多元素。首先,必须有一个名为 Adder 的接口,以及一个名为 AdderImpl 的带有零参数构造函数的实现类。它继续断言 Adder 接口应该有一个名为 add 的方法,该方法具有两个整型参数,并返回另一个整数。它还指定了此方法对于一小部分值的行為。

public class TestAdder {
    public void testSum() {
        Adder adder = new AdderImpl();
        assert(adder.add(1, 1) == 2);
        assert(adder.add(1, 2) == 3);
        assert(adder.add(2, 2) == 4);
        assert(adder.add(0, 0) == 0);
        assert(adder.add(-1, -2) == -3);
        assert(adder.add(-1, 1) == 0);
        assert(adder.add(1234, 988) == 2222);
    }
}

在这种情况下,单元测试先于编写,充当一个设计文档,指定了所需解决方案的形式和行为,而不是实现细节,这些细节留给程序员。遵循“执行最简单可能的有效操作”的实践,通过使测试通过的最简单的解决方案如下所示。

interface Adder {
    int add(int a, int b);
}
class AdderImpl implements Adder {
    int add(int a, int b) {
        return a + b;
    }
}

与其他基于图的设计方法不同,使用单元测试作为设计具有一项重大优势。设计文档(单元测试本身)可用于验证实现是否符合设计。使用单元测试设计方法,如果开发人员没有根据设计实现解决方案,则测试将永远无法通过。

单元测试确实缺乏图表的某些可访问性,但 UML 图表现在可以通过免费工具(通常作为 IDE 的扩展提供)轻松地为大多数现代语言生成。基于 xUnit 框架的免费工具将图形呈现视图以供人类消费,外包到另一个系统。

分离接口和实现

[edit | edit source]

由于某些类可能引用其他类,测试一个类经常会蔓延到测试另一个类。一个常见的例子是依赖数据库的类:为了测试该类,测试人员经常编写与数据库交互的代码。这是一个错误,因为单元测试通常不应该超出其自身的类边界,尤其是不要跨越这样的进程/网络边界,因为这会导致单元测试套件出现不可接受的性能问题。跨越这样的单元边界会将单元测试变成集成测试,并且当测试用例失败时,会难以确定哪个组件导致了失败。另见模拟、模拟和集成测试

相反,软件开发人员应该围绕数据库查询创建一个抽象接口,然后使用自己的模拟对象实现该接口。通过将这种必要的附加关系从代码中抽象出来(暂时降低了净有效耦合),可以比以前更彻底地测试独立单元。这将导致一个质量更高、更易于维护的单元。

单元测试局限性

[edit | edit source]

不能指望测试能发现程序中的所有错误:除了最简单的程序之外,几乎不可能评估所有执行路径。单元测试也是如此。此外,根据定义,单元测试仅测试单元本身的功能。因此,它不会发现集成错误或更广泛的系统级错误(例如跨多个单元执行的功能或非功能测试区域,如性能)。单元测试应该与其他软件测试活动一起进行。像所有形式的软件测试一样,单元测试只能表明错误的存在;它们不能表明错误不存在。

软件测试是一个组合问题。例如,每个布尔决策语句至少需要两个测试:一个结果为“真”,一个结果为“假”。因此,对于编写的每一行代码,程序员通常需要 3 到 5 行测试代码。[3] 这显然需要时间,并且其投资可能不值得付出努力。还有很多问题根本无法轻松测试——例如那些是非确定性的或涉及多个线程的问题。此外,编写单元测试代码与测试的代码一样可能出现错误。弗雷德·布鲁克斯在《人月神话》中引用道:永远不要带两块计时器出海。总是带一块或三块。 意思是,如果两个计时器矛盾,你怎么知道哪个是正确的?

为了从单元测试中获得预期的收益,在整个软件开发过程中需要严格的纪律。除了要仔细记录已执行的测试之外,还需要记录对软件的该单元或任何其他单元的源代码所做的所有更改。使用版本控制系统至关重要。如果单元的后期版本无法通过它以前通过的特定测试,版本控制软件可以提供自那时起应用于单元的源代码更改(如果有)的列表。

同样重要的是要实施一个可持续的流程,以确保每天审查测试用例失败并立即解决。[4] 如果没有实施这样的流程并将其融入团队的工作流程,应用程序将随着单元测试套件的演变而失去同步,从而增加误报并降低测试套件的有效性。

应用

[edit | edit source]

极限编程

[edit | edit source]

单元测试是极限编程的基石,它依赖于自动化的单元测试框架。这个自动化的单元测试框架可以是第三方,例如 xUnit,也可以是开发团队内部创建的。

极限编程使用创建单元测试进行测试驱动开发。开发人员编写一个单元测试,以暴露软件需求或缺陷。此测试将失败,因为需求尚未实现,或者因为它故意暴露了现有代码中的缺陷。然后,开发人员编写最简单的代码以使测试(以及其他测试)通过。

系统中的大多数代码都经过单元测试,但并非一定经过所有代码路径测试。极限编程要求“测试所有可能出错的东西”的策略,而不是传统的“测试所有执行路径”方法。这导致开发人员比传统方法开发的测试更少,但这并不是什么问题,更多的是对事实的重述,因为传统方法很少被系统地遵循,以至于所有执行路径都经过了彻底的测试。[需要引用] 极限编程仅仅承认测试很少是详尽的(因为它通常过于昂贵和耗时,在经济上不可行),并提供有关如何有效地集中有限资源的指导。

至关重要的是,测试代码被认为是头等项目工件,这意味着它与实现代码保持相同的质量,并且消除了所有重复。开发人员与测试的代码一起将单元测试代码发布到代码库中。极限编程的彻底单元测试带来了上述优势,例如更简单、更自信的代码开发和重构、简化的代码集成、准确的文档以及更模块化的设计。这些单元测试也作为一种回归测试不断运行。

技术

[edit | edit source]

单元测试通常是自动化的,但仍然可以手动执行。IEEE 并不偏袒一方。[5] 手动单元测试方法可以使用逐步说明性文档。然而,单元测试的目标是隔离单元并验证其正确性。自动化对于实现这一点是有效的,并且可以实现本文中列出的许多好处。相反,如果没有仔细计划,粗心大意的手动单元测试用例可能会作为涉及许多软件组件的集成测试用例执行,从而无法实现大多数(如果不是全部)为单元测试制定的目标。

为了在使用自动化方法时充分实现隔离的效果,被测试的单元或代码体在框架内执行,该框架位于其自然环境之外。换句话说,它是在其最初创建的产品或调用上下文之外执行的。以这种隔离的方式进行测试揭示了被测试代码与产品中的其他单元或数据空间之间的不必要的依赖关系。然后可以消除这些依赖关系。

使用自动化框架,开发人员将标准编码到测试中以验证单元的正确性。在测试用例执行期间,框架记录任何标准都失败的测试。许多框架还将自动标记这些失败的测试用例并将其汇总报告。根据失败的严重程度,框架可能会停止后续测试。

因此,单元测试传统上是程序员创建解耦和内聚代码体的动力。这种实践促进了软件开发中的健康习惯。设计模式、单元测试和重构经常协同工作,从而可能出现最佳解决方案。

单元测试框架

[edit | edit source]

单元测试框架通常是第三方产品,不会作为编译器套件的一部分进行分发。它们有助于简化单元测试过程,因为它们是为各种语言开发的。测试框架的示例包括开源解决方案,如各种代码驱动测试框架(统称为 xUnit),以及专有/商业解决方案,如 TBrun、Testwell CTA++ 和 VectorCAST/C++。

通常可以在没有特定框架支持的情况下执行单元测试,方法是编写练习被测试单元的客户端代码,并使用断言、异常处理或其他控制流机制来发出失败信号。没有框架的单元测试很有价值,因为采用单元测试的门槛很高;很少的单元测试几乎比没有单元测试好不了多少,而一旦框架到位,添加单元测试就变得相对容易。[6] 在某些框架中,许多高级单元测试功能缺失或必须手动编码。

语言级单元测试支持

[edit | edit source]

一些编程语言直接支持单元测试。它们的语法允许直接声明单元测试,而无需导入库(无论是第三方库还是标准库)。此外,单元测试的布尔条件可以用与非单元测试代码中使用的布尔表达式相同的语法来表达,例如用于ifwhile语句的语法。

直接支持单元测试的语言包括

  • Cobra
  • D
  1. Fowler, Martin (2007-01-02). "模拟不是存根". 检索于 2008-04-01.
  2. Kolawa, Adam (2007). 自动化缺陷预防:软件管理最佳实践. Wiley-IEEE 计算机协会出版社. p. 75. ISBN 0470042125. {{cite book}}: |pages= 和 |page= 指定了多个 (帮助); 未知参数 |coauthors= 被忽略了 (|author= 建议) (帮助)
  3. Cramblitt, Bob (2007-09-20). "Alberto Savoia 赞扬软件测试". 检索于 2007-11-29.
  4. daVeiga, Nada (2008-02-06). "无需恐惧地更改代码:利用回归安全网". 检索于 2008-02-08.
  5. IEEE 标准委员会, "IEEE 软件单元测试标准:美国国家标准,ANSI/IEEE Std 1008-1987"IEEE 标准:软件工程,第二卷:过程标准;1999 版;由电气与电子工程师协会出版 IEEE 计算机学会软件工程技术委员会.
  6. Bullseye 测试技术 (2006–2008). "中等覆盖率目标". 检索于 2009 年 3 月 24 日. {{cite web}}: 检查 |date= 中的日期值 (帮助); 文本“publication-place”被忽略 (帮助)
[编辑 | 编辑源代码]
华夏公益教科书