Shortcuts

单元测试指南

单元测试的重要性

在软件工程领域,单元测试是一种测试方法。通过这种方法,对一个或多个计算机程序模块的源代码集及其相关控制数据、使用程序和操作程序的每个单元进行测试,以确定它们是否可以正确运行(来自Wikipedia - 单元测试)。

在实际开发中,单元测试的意义如下:

  • 当代码更新时,您可以运行单元测试以确保不会发生回归错误。

  • 通过细粒度的单元测试设计,您可以在单元测试期间快速准确地定位错误的来源。

  • 将单元测试与代码覆盖率结合可以确保所有代码和分支都经过了测试。

  • 在发现错误后,您可以将能够重现错误的测试用例添加到单元测试中,以不断提高代码功能的完善性。

  • 另一个重要的点——对于一个模块来说,阅读单元测试代码也是理解其功能和用法的非常有效的方式

单元测试的类型

在DI-engine项目中,我们将单元测试分为以下几个部分:

  • unittest – 在一般意义上进行功能单元测试,以确保工程代码的正常功能和算法代码在简单用例上的收敛性。

  • algotest – 用于算法代码的单元测试,以确保算法代码能够在特定用例上满足使用要求。

  • cudatest – 用于CUDA依赖功能的单元测试,确保这些功能在带有CUDA的操作环境中正常运行。

  • envpooltest – 用于测试依赖envpool高性能并行计算的功能,以确保这些功能正常运行。

  • platformtest – 跨平台代码的单元测试,确保DI-engine的核心功能在MacOS和Windows平台上仍能正常运行。

  • benchmark – 算法或架构的性能测试,主要对相关内容进行速度测量,以确保其性能符合要求。

如何构建单元测试

在DI-engine中,我们使用pytest来构建单元测试。

对于单元测试的编写,您可以参考代码路径下各级的tests文件夹作为一个整体,例如ding/envs/env_manager/tests

命名约定

对于单元测试,我们通常以类或函数为单位构建,其名称应符合一定的规范,具体如下:

  • 对于函数形式的单元测试,函数名称需要以test_开头。

  • 对于类表单的单元测试,类名需要以Test开头,所有用于测试的方法名称应以test_开头。

断言

在测试案例中,我们使用assert(断言)来检查原型结果。如果断言不成立,将显示非常详细的信息,如下图所示

../_images/pytest_assert.png

此外,`` pytest`` 还支持对抛出异常的断言,如下所示

import pytest

@pytest.mark.unittest
def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

此外,对于实数的测试,由于实数的存储原理,可能会导致由细微误差引起的误判。因此,可以使用近似函数approx进行近似判断。它支持数值类型、list类型、dict类型和numpy类型(numpy. Ndarray)。

../_images/pytest_approx.png

Fixture 和 Conftest

Fixture 是 pytest 中一个非常重要的机制。它可以初始化测试所需的资源,并将它们作为测试函数的参数传递以供使用。不仅如此,还可以实现操作资源的恢复,以确保后续操作不会受到影响。此外,通过定义作用域,可以轻松实现代码的复用。

这个fixture教程写得非常详细,可以作为参考。在DI-engine的现有代码中,你可以参考ding/league/tests/test_player.py

Fixture 通常用于单个文件中,即在当前文件下定义 fixture 后使用。如果需要跨文件使用 fixture,可以使用 conftestconfig of test 的缩写)机制。在测试文件中无需显式导入,pytest 框架会自动完成加载。你可以参考这个 教程,在现有代码中,可以参考 ding/league/tests/conftest.py

测试标记

为了区分测试的类型(例如,参考:ref-test-types),你可以添加pytest.mark("MARK-NAME")装饰器,让测试按类别执行,并使用pytest –m MARK-NAME在运行时执行选定的测试类型。

../_images/pytest_mark.png

参数化

在某些情况下,我们需要重用相同的测试逻辑并对不同的输入数据进行测试。此时,我们可以使用参数配置 @pytest.mark.paramtrize(argsnames, argsvalues, ids=None) 实现多组测试的参数配置。其中:

  • argsnames : 表示参数名称,类型为 str。如果需要表示多个参数名称,请使用逗号分隔。

  • argsvalues : 表示参数值,如果类型是list,则是由参数组成的列表。列表中的元素是分配给参数的值。如果在argsnames中设置了多个参数,将使用tuple类型,并且值将按顺序与名称一一对应。

例如:

  • 如果使用装饰器 @pytest.mark.paramtrize('data', [1, 2, 3]),那么 ``data`` 变量将分别被赋值为 1、2 和 3 进行测试。

  • 如果使用装饰器 @pytest.mark.paramtrize('var1, var2', [(1, 2), (2, 3), (3, 4)])(var1, var2) 变量将被赋值为 (1, 2)(2, 3)(3, 4) 进行测试。

你可以参考ding/utils/data/tests/test_dataloader.py中的写法。

如何运行单元测试

在DI-engine中,我们使用pytest来启动单元测试。对于非常简单的情况,你可以直接使用命令:

pytest -sv ./ding

当您需要了解单元测试覆盖率和具体的覆盖率分布时,需要使用以下命令:

pytest -sv ./ding -m unittest --cov-report term-missing --cov=./ding

每个参数的含义如下:

  • -m : 选择要测试的标记类型。

  • -s : 输出内容未被捕获,这是 --capture=no 选项的缩写。

  • -v : 选择输出内容的复杂度级别。当前选择的是较低的复杂度级别。如果需要输出更详细的信息,可以使用 -vv 来增加复杂度,依此类推。

  • --cov-report term-missing :选择以term-missing的形式显示覆盖率报告,这指的是“显示未覆盖的具体区域”。

  • --cov : 选择要覆盖的代码区域。

注意

更推荐的方法是使用封装在Makefile中的脚本进行快速启动,例如:

make unittest  # Full unit testing
make unittest RANGR_DIR=./ding/xxx  # Test for specific sub modules
make algotest
make cudatest
make envpooltest
make platformtext