Logo
Section 1 / 7

单元测试

了解单元测试、其背后的设计思想,以及各自的优缺点。

导入

单元测试

了解单元测试、其背后的设计思想,以及各自的优缺点。

单元测试概述

我们从单元测试开始。下面通过一个例子直观地说明它的含义。

项目的代码是由一系列称为函数或类方法的“变换”构成的复杂网络,这些变换将某些输入转化为输出。其中一些变换是确定性的,另一些则具有随机性。

为什么不逐个检查这些变换,验证每一步的结果是否符合预期(无论是完全匹配,还是满足某些条件)呢?这种针对单个函数进行的简单测试就被称为单元测试,这里的“单元”指的就是一个函数。

代码示例

来看一些单元测试的例子。在下面的代码中,我们定义了一个 average 函数,用于计算一组数值的平均值,并通过传入输入来测试其输出是否符合预期。

1def average(x): 2 return sum(x) / len(x) 3 4def test_average(): 5 x = [0, 1, 5] 6 assert average(x) == 2

再看一个测试失败的例子。这里有一个 nanmax 函数,它返回一组数值中的最大值,即使其中包含 None 值。

1def nanmax(x): 2 max_value = None 3 for value in x: 4 if value: 5 max_value = max(value, max_value) if max_value else value 6 return max_value 7 8def test_nanmax(): 9 x = [5, None, 2, -3] 10 assert nanmax(x) == 5 11 12def test_nanmax_with_zero(): 13 x = [None, -5, 0, None, -2, -1] 14 assert nanmax(x) == 0

单元测试的优点

  • 验证代码正确性:尽管单元测试只检查底层逻辑,但它们能在可能的情况下确保代码正常且可预测地运行。换句话说,至少能验证一部分功能,如果幸运的话,还是关键部分。

  • 作为代码文档:在某种意义上,单元测试就是代码的实际使用案例。它们减少了我们为类或函数编写行为说明文档的需求——只需展示用法:输入是什么,输出是什么。

  • 促进代码模块化:单元测试的另一个好处是促使我们写出更清晰、更整洁、更具模块化的代码(即易于测试的代码,而非混乱的“意大利面条式”代码)。

  • 提升代码质量:单元测试有助于发现开发过程中可能遗漏的问题,从而提高代码质量,减少生产环境中的缺陷。

  • 快速且易于执行:我们可以在开发过程中的任何时间运行单元测试,以确保代码按预期工作,而无需手动逐一验证。长期来看,这能节省大量时间和精力。

有一种开发模式叫做测试驱动开发(Test-Driven Development),其核心思想是:先写测试,再写能让测试通过的代码。这种方法有助于逐步构建代码库,并增强其模块性。

单元测试的缺点

单元测试也存在一些不足之处:

  • 不直接为最终用户创造价值:虽然单元测试有助于确保代码各部分正确运行,但它们并不直接增加软件的功能或可用性。因此,若非从项目初期就将测试纳入开发流程,很难证明投入时间和资源进行单元测试的合理性。

  • 编写耗时:尤其是对已有项目补充单元测试(以及为此准备代码结构)时尤为明显。即便是在重构代码时同步更新测试,也需要花费相当多的时间。

  • 复杂性较高:编写单元测试本身可能很复杂,特别是当需要模拟(mock)或桩定(stub)外部依赖时,这会使测试更难理解和维护。

  • 产生虚假安全感:尽管单元测试让我们对代码正确性更有信心,但也可能带来错觉。因为它们仅验证独立的代码单元,无法检测这些单元之间的交互问题,或与外部依赖的集成问题。

  • 覆盖范围有限:单元测试难以涵盖所有可能的情况和边界条件。即使所有测试都通过了,代码仍可能存在缺陷。

  • 依赖具体实现:单元测试可能很脆弱,一旦底层代码实现发生变化,测试就容易失败。这使得在不破坏测试的前提下修改代码变得困难。

总体而言,尽管单元测试在发现和修复代码缺陷方面非常有用,但它应作为更全面测试策略的一部分,还需结合集成测试和端到端测试共同使用。

可靠机器学习简介

pytest 入门