4. 编写第一个MATLAB单元测试

在 MATLAB 中编写单元测试,核心是围绕 matlab.unittest 框架展开。一个可运行的测试通常由三部分组成:测试类(组织测试的容器)、测试方法(具体验证逻辑)、断言(判断实际结果是否符合预期)。测试运行器则负责发现、执行并汇总这些测试。理解这四者的分工,是写出第一个可维护测试的前提。

测试类:用 classdef 组织测试

MATLAB 单元测试以类文件形式存在,文件名与类名一致,通常以 Test 结尾。测试类需要继承 matlab.unittest.TestCase,这是框架识别测试的入口。一个最小测试类结构如下:

classdef CalculatorTest < matlab.unittest.TestCase
    methods (Test)
        function testAddPositive(testCase)
            actual = add(2, 3);
            testCase.verifyEqual(actual, 5);
        end
    end
end

其中 methods (Test) 块是关键:只有放在该块中的方法才会被测试运行器识别为测试方法。普通方法块中的函数不会自动执行。类名、文件名、方法名都应具备描述性,便于失败时快速定位。

测试方法:命名与结构约定

测试方法通常以 test 开头,接收 testCase 作为第一个参数。这个参数不是普通输入,而是断言和测试夹具的调用入口。方法内部一般遵循“准备输入—调用被测代码—断言结果”的顺序,但不必写成固定三段式,关键是让每个测试只验证一个明确行为。

例如测试一个除法函数时,可以分别编写正常路径和异常路径:

methods (Test)
    function testDivideNormal(testCase)
        actual = divide(10, 2);
        testCase.verifyEqual(actual, 5);
    end

    function testDivideByZero(testCase)
        testCase.verifyError(@() divide(1, 0), 'MATLAB:divideByZero');
    end
end

这里 verifyError 验证的是“调用是否抛出指定标识符的错误”,而不是返回值。异常路径和正常路径应分开成独立测试方法,避免一个方法里混合多种判断,导致失败原因不清晰。

断言类型:verify、assert、fatalAssert 与 assume

MATLAB 提供多组断言,差别在于失败后的行为和是否继续执行。常见的有:

  • verifyEqual、verifyTrue、verifyError 等 verify*:失败时记录失败,但继续执行当前测试方法中的后续断言,适合收集多个问题。
  • assertEqual、assertTrue 等 assert*:失败时立即终止当前测试方法,适合前置条件不满足时不再继续。
  • fatalAssert*:失败时终止整个测试运行,通常用于环境损坏等严重情况。
  • assume*:条件不满足时标记为“过滤”而非失败,适合依赖特定平台或工具箱的测试。

选择原则可以概括为:普通结果校验优先用 verify;关键前置条件用 assert;环境级问题用 fatalAssert;条件不满足时用 assume。例如验证数值结果时,verifyEqual 支持容差参数:

testCase.verifyEqual(actual, expected, 'AbsTol', 1e-9);

浮点计算中直接比较相等往往不可靠,显式给出绝对或相对容差是更稳妥的做法。

测试运行器:发现与执行

写好测试类后,用 runtests 执行是最直接的方式:

results = runtests('CalculatorTest');
disp(results);

runtests 会返回一个结果数组,包含每个测试的通过、失败或未完成状态。若需要更细粒度控制,可以使用 matlab.unittest.TestSuite 和 matlab.unittest.TestRunner:

suite = testsuite('CalculatorTest');
runner = testrunner('textoutput');
results = runner.run(suite);

这种方式便于添加插件,例如生成 JUnit 风格报告、覆盖率报告或自定义输出。对于日常开发,runtests 已足够;当测试需要集成到持续集成流水线时,再引入 runner 和插件更合适。

MATLAB 单元测试四要素与执行链路 测试类 CalculatorTest.m classdef ... < TestCase methods (Test) 容器:组织测试方法 测试方法 testAddPositive testDivideByZero function testXxx(testCase) 单元:一个行为一个方法 断言 verifyEqual / verifyError assertEqual / assumeTrue fatalAssert* 判断:结果是否符合预期 测试运行器 runtests('CalculatorTest') testsuite + testrunner + 插件 发现 → 执行 → 汇总结果 结果:通过 / 失败 / 未完成 失败时定位到具体测试方法与断言 关键约定 文件名 = 类名 方法在 (Test) 块中 方法名以 test 开头 断言选择 verify:继续执行 assert:立即终止 assume:过滤跳过

从零到可运行:一个完整示例

假设有一个简单函数 add.m,返回两个数之和。测试类可以写成:

classdef AddTest < matlab.unittest.TestCase
    methods (Test)
        function testPositiveNumbers(testCase)
            testCase.verifyEqual(add(2, 3), 5);
        end

        function testNegativeNumbers(testCase)
            testCase.verifyEqual(add(-2, -3), -5);
        end

        function testZero(testCase)
            testCase.verifyEqual(add(0, 0), 0);
        end
    end
end

在命令行执行 runtests('AddTest'),若三个方法全部通过,结果会显示类似“3 Passed, 0 Failed”的汇总。若某个断言失败,输出会指出失败的方法名、期望值与实际值,便于快速定位。这里每个测试方法只验证一种输入情形,失败时不会互相干扰。

测试方法名应描述“被测行为 + 条件”,例如 testAddPositiveNumbers 比 test1 更有信息量。当测试数量增长时,命名质量直接决定维护成本。

常见误区与检查点

  • 方法未放入 (Test) 块:运行器不会发现,测试数量为 0。
  • 忘记继承 TestCase:类无法被识别为测试类。
  • 浮点结果直接用 verifyEqual 无容差:可能因微小误差误报失败,应加 'AbsTol' 或 'RelTol'。
  • 一个方法里塞入过多断言:失败原因不清晰,建议按行为拆分。
  • 异常路径未单独测试:只测正常路径会遗漏错误处理逻辑。