在inputParser章节中,我们通过不断改进getArea函数对输入参数的处理方法,引入这样一个观点:一个可靠的科学工程计算项目必须有一套测试系统,才能防止开发的过程中算法退化,工程项目的推进必须在算法开发和算法测试之间不断迭代完。在inputParser章节的最后,还根据直觉提出了一个测试系统所应该有的基本功能。在本章中,我们将学习MATLAB提供的测试解决方案:MATLAB单元测试(MATLAB Unit Test)。
% testmainfunc.m
function tests = testmainfunc
tests = functiontests(localfunctions); % 主测试函数中必须要有这个命令
end
...
其中 localfunctions 是一个MATLAB函数,用来返回所有局部函数的函数句柄。
局部函数的命名必须以 test 开头,局部函数只接受一个输入参数,即测试对象,即下面例子中的形参 testCase
% testmainfunc.m
...
function testPoint1(testCase) % 只接受一个输入参数
testCase.verifyEqual(.....);
end
function testPoint2(testCase) % 只接受一个输入参数
testCase.verifyEqual(.....);
end
...
% 第一版的getArea函数
function a = getArea(wd,ht)
p = inputParser;
p.addRequired('width', @isnumeric); % 检查输入必须是数值型的
p.addRequired('height',@isnumeric);
p.parse(wd,ht);
a = p.Results.width*p.Results.height; % 从Results处取结果
end
% testGetArea.m
function tests = testGetArea
tests = functiontests(localfunctions);
end
% 添加了第一个测试点
function testTwoInputs(testCase)
testCase.verifyTrue(getArea(10,22)==220,'!=220');
testCase.verifyTrue(getArea(3,4)==12,'!=12');
end
% testGetArea.m
function tests = testGetArea
tests = functiontests(localfunctions);
end
function testTwoInputs(testCase)
testCase.verifyTrue(getArea(10,22)==0,'Just A Test'); % 故意让验证失败
end
Framework将给出详尽的错误报告, 其中 Test Diagnostic 栏目中报告的就是verifyTrue函数中的第二个参数所提供的诊断信息。
% command line
>> results =runtests('testGetArea')
Running testGetArea
================================================================================
Verification failed in testGetArea/testTwoInputs. % 验证失败
----------------
Test Diagnostic: % 诊断信息
----------------
Just A Test
---------------------
Framework Diagnostic:
---------------------
verifyTrue failed. % 验证函数verifyTrue出错
--> The value must evaluate to "true".
% 验证的表达式getArea(10,22)==0的值应该为true
Actual logical:
0 % 表达式的实际值为false
------------------
Stack Information:
------------------
In testGetArea.m (testTwoInputs) at 6 % 测试点testTwoPoints出错
================================================================================
.
Done testGetArea
_________
Failure Summary: % 测试简报
Name Failed Incomplete Reason(s)
========================================================================
testGetArea/testTwoInputs X Failed by verification.
% 出错的测试点名称
results =
TestResult with properties:
Name: 'testGetArea/testTwoInputs'
Passed: 0 % 零个测试点通过
Failed: 1 % 一个测试点出错
Incomplete: 0
Duration: 0.0342
Totals:
0 Passed, 1 Failed, 0 Incomplete.
0.03422 seconds testing time.
我们再添加一个负面测试,回忆第一版的函数getArea不支持单个参数,如下:
% command line
>> getArea(10) % 如预期报错 调用少一个参数
Error using getArea
Not enough input arguments.
>> [a b] = lasterr % 调用lasterr得到error ID
a =
Error using getArea1 (line 6)
Not enough input arguments.
b =
MATLAB:minrhs
% testGetArea.m
function tests = testGetArea
tests = functiontests(localfunctions);
end
function testTwoInputs(testCase)
testCase.verifyTrue(getArea1(10,22)==220,'!=220');
testCase.verifyTrue(getArea1(3,4)==12,'!=12');
end
% 添加了第2个测试点
function testTwoInputsInvalid(testCase)
testCase.verifyError(@()getArea1(10),'MATLAB:minrhs');
end
% 在命令行中得到Error ID
>> getArea1('10',22)
Error using getArea1 (line 6)
The value of 'width' is invalid. It must satisfy the function: isnumeric.
>> [a b] = lasterr
a =
Error using getArea1 (line 6)
The value of 'width' is invalid. It must satisfy the function: isnumeric.
b =
MATLAB:InputParser:ArgumentFailedValidation % 这个Error ID是我们需要的
然后再把这个负面测试添加到testGetArea中去
% testGetArea.m
function tests = testGetArea
tests = functiontests(localfunctions);
end
function testTwoInputs(testCase)
testCase.verifyTrue(getArea1(10,22)==220,'!=220');
testCase.verifyTrue(getArea1(3,4)==12,'!=12');
end
function testTwoInputsInvalid(testCase)
testCase.verifyError(@()getArea1(10),'MATLAB:minrhs');
testCase.verifyError(@()getArea1('10',22),... % 新增的test
'MATLAB:InputParser:ArgumentFailedValidation')
end
运行一遍,一个正面测试,一个负面测试都全部通过。
% command line
>> runtests('testGetArea')
Running testGetArea
..
Done testGetArea
_________
ans =
1x2 TestResult array with properties:
Name
Passed
Failed
Incomplete
Duration
Totals:
2 Passed, 0 Failed, 0 Incomplete.
0.0094501 seconds testing time.
% command line
>> getArea(10) % 正确处理了单个参数的情况
ans =
100
>> getArea(10,0) % 如预期检查出第二个参数的错误,并给出提示
Error using getArea (line 37)
The value of 'height' is invalid. Expected input number 2, height, to be nonzero.
>> getArea(0,22) % 如预期检查出第一个参数的错误,并给出提示
Error using getArea (line 37)
The value of 'width' is invalid. Expected input number 1, width, to be nonzero.
% 确保单一参数计算正确
function tests = testGetArea
...从略
function testOneInput(testCase)
testCase.verifyTrue(getArea2(10) ==100,'!=100');
testCase.verifyTrue(getArea2(22) ==484,'!=484');
end
再添加一个Negative测试点,确保getArea函数会处理输入是零的情况
% 保证不接受零输入
function tests = testGetArea
...从略
function testTwoInputsZero(testCase)
testCase.verifyError(@()getArea(10,0),'MATLAB:expectedNonZero');
testCase.verifyError(@()getArea(0,22),'MATLAB:expectedNonZero');
end
% testImgProcess
function tests = testImgProcess( )
tests = functiontests(localfunctions);
end
function testOp1(testCase)
img = imread('testimg.tif'); % 载入图像
Op1(img);
% ... rest of the work
end
function testOp2(testCase)
img = imread('testimg.tif'); % 载入图像
Op2(img);
% ... rest of the work
end
% 使用setup和teardown
function tests = testImgProcess( )
tests = functiontests(localfunctions);
end
function setup(testCase)
testCase.TestData.img = imread('corn.tif');
% 其它的准备工作
end
function teardown(testCase)
% 其他清理工作
end
function testOp1(testCase)
newImg = Op1(testCase.TestData.img); % 直接使用对象testCase的属性TestData
% ... rest of the work
end
function testOp2(testCase)
newImg = Op2(TestCase.TestData.img);h
% ... rest of the work
end
% 使用setupOnce teardownOnce来管理对数据库的连接
function tests = testAlgo( )
tests = functiontests(localfunctions);
end
function setupOnce(testCase)
testCase.TestData.conn = connect_DB('testdb'); %一个假想的连接数据库的函数
end
function teardownOnce(testCase)
disconnect_DB();
end
function testAlgo1(testCase)
% retrieve data and do testing
end
function testAlgo2(testCase)
% retrieve data and do testing
end
% testGetArea.m
function tests = testGetArea
tests = functiontests(localfunctions);
end
% 添加了第一个测试点
function testTwoInputs(testCase)
testCase.verifyTrue(getArea(10,22)==220,'!=220');
testCase.verifyTrue(getArea(3,4)==12,'!=12');
end
% tFoo.m
function tests = tFoo
tests = functiontests(localfunctions);
end
function testSomething_PC(testCase)
testCase.assumeTrue(ispc,'only run in PC'); % 如果这个测试点在其它平台运行,
% 则显示Incomplete
% ....
end
如果我们在MAC下运行这个测试,则显示
>> runtests('tFoo')
Running tFoo
================================================================================
tFoo/testSomething_PC was filtered.
Test Diagnostic: only run in PC
Details
================================================================================
.
Done tFoo
__________
Failure Summary:
Name Failed Incomplete Reason(s)
====================================================================
tFoo/testSomething_PC X Filtered by assumption.
该测试被过滤掉了
ans =
TestResult with properties:
Name: 'tFoo/testSomething_PC'
Passed: 0
Failed: 0
Incomplete: 1
Duration: 0.0466
Totals:
0 Passed, 0 Failed, 1 Incomplete.
0.046577 seconds testing time.
function tests = tFoo
tests = functiontests(localfunctions);
end
function testA(testCase)
testCase.assertTrue(isConnected(),'database must be connected!')
% 其它测试内容
end
function testB(testCase)
testCase.verifyTrue(1==1,'');
end
运行这个测试,显示如下
% command line
>> runtests('tFoo')
Running tFoo
================================================================================
Assertion failed in tFoo/testA and it did not run to completion.
----------------
Test Diagnostic:
----------------
database must be connected!
---------------------
Framework Diagnostic:
---------------------
assertTrue failed.
--> The value must evaluate to "true".
Actual logical:
0
------------------
Stack Information:
------------------
In /Users/iamxuxiao/Documents/MATLAB/tFoo.m (testA) at 6
================================================================================
..
Done tFoo
__________
Failure Summary:
Name Failed Incomplete Reason(s)
======================================================
tFoo/testA X X Failed by assertion.
Totals:
1 Passed, 1 Failed, 1 Incomplete.
0.036008 seconds testing time.
% 错误提示信息其实是getArea文档的一部分
...
function testTwoInputs(testCase)
testCase.verifyTrue(getArea(10,22)==220,'given width and height, ...
should return 10*22=220');
...
end
...