标题: MATLAB单元测试 [打印本页] 作者: thinkfunny 时间: 2020-12-3 10:40 标题: MATLAB单元测试 1 `( q2 N. c: Q) r2 l9 E) M7 f
基于函数的(Function-Based)单元测试的构造 * l% m5 w) V" ?( `, y8 Q8 B$ a5 vMATLAB基于函数的单元测试构造很简单,如图Figure 1所示:用户通过一个主测试函数和若干局部测试函数(也叫做测试点) (Local Function)来组织各个测试。而测试的运行则交给MATLAB的单元测试架构(以下简称Framework)去完成。 5 ?6 e2 \& N& ~' I+ V7 H - b: a( I" U3 B" tFigure.1 单元测试Framework和测试函数' `# B, g/ z3 g2 a) ^1 d$ _
主测试函数和局部测试函数看上去和普通的MATLAB函数没有区别,其结构如图Figure 2 所示,只是命名上有一些规定而已,这些特殊的规定是为了Framework可以和测试函数契合而规定的。 * h& w& M9 O, p/ d; [7 R1 S. \ b Z
Figure.2 简单的主测试函数和若干局部的测试函数构成的一个单元测试 : Q# p; I7 a6 f) W/ |; @( {命名规则如下:6 Z2 d; X' h0 q1 K! s# m# A+ Q* x
主函数的名称由用户任意指定,和其他的MATLAB函数文件一样,该文件的名称需要和函数的名称的相同. (如果主函数的名称是 testmainfunc , 该文件名称则是testmainfunc.m )。! {- L; i4 B/ ], \% ^2 x( A
在主函数中,必须调用一个叫做 functiontests 的函数,搜集该函数中的所有局部函数, 产生一个包含这些局部函数的函数局部的测试矩阵并返回给Framework 2 K* | L1 F4 B! B# W* U$ e如下所示: , w3 M3 v* o6 y, U+ d* p% `3 l% Z% testmainfunc.m 0 s9 D% O& z% ~0 k( X* S function tests = testmainfunc$ V( @+ ^0 w x( R Q
tests = functiontests(localfunctions); % 主测试函数中必须要有这个命令 # v2 ?/ K$ _$ O end & r8 z1 g( t7 T, l h* H0 r ... ' m$ d$ s: _4 c, n: ?1 p% F1 \; N6 Q 5 U/ ?* L, l' ]+ O8 R# u" Y6 w
其中 localfunctions 是一个MATLAB函数,用来返回所有局部函数的函数句柄。 局部函数的命名必须以 test 开头,局部函数只接受一个输入参数,即测试对象,即下面例子中的形参 testCase 2 e) Z7 X; B j4 N2 @% testmainfunc.m a" E' S) i7 Y4 n/ Q$ o3 k9 @
... / h# F8 j( m3 ?9 V function testPoint1(testCase) % 只接受一个输入参数 6 J# e* _* x; H' ]/ x testCase.verifyEqual(.....); ( {& o: s" u M9 E/ x end . a# P6 ?* p* N- R
9 g* J+ l# V( c$ T5 @8 ~2 r
function testPoint2(testCase) % 只接受一个输入参数 ! @. x3 @7 X' `# S) s, c( ]' X. ^0 ~ testCase.verifyEqual(.....);9 A" @4 m* H* l( ?7 c7 L
end 9 [( y1 n( F. \! G9 m
...; W# p g! g; f6 z6 @# R3 f
% p" @4 i! ?9 L- B- b$ X! |# c8 N+ X其中testCase由单元测试Framework提供,即Framework将自动的调用该函数,并且提供testCase参数。 按照规定,要运行单元测试中的所有测试,必须调用runtests函数 : A; ~. l. B# v e( M% command line 3 c' H) l. W3 |5 J/ U( _ >> runtests('testmainfunc.m') / [4 _) l, z4 X) @6 F6 R. o
7 W) z5 m$ C; p& M; e& s
下面用我们用基于函数的单元测试来给getArea函数的构造其单元测试。 8 \- ?- P3 Q" Z: PgetArea函数的单元测试: 版本 I' k% R F& L: M+ J% U7 u1 S+ U: A
首先给主测试文件起个名字叫做testGetArea,该名字是任意的,为了便于理解名字里面通常包含test,并包含要测试的主要函数的名字: - U. d0 @2 L- p& [% testGetArea.m % f ]9 n+ o; E
function tests = testGetArea ( e* }" B/ |. j- k' z0 J tests = functiontests(localfunctions); ! Y) @# a! `7 Q end 8 I2 S! X% E2 b2 Y* X ( G9 t* A9 \& E7 q4 ?/ ~" v2 T
在该主函数中,localfunctions将搜集所有的局部函数,构造函数句柄数组并返回测试矩阵。这里自然会有一个问题,这个tests句柄数组将返回给谁,这就要了解Framework是如何和测试相互作用的。 如图Figure.3 所示,整个测试从 runtests('testmainfunc.m') 命令开始, 命令函数,Framework将首先调用testGetArea的主函数,得到所有的局部函数的函数句柄,如空心箭头线段所示,然后Framework再负责调用每一个测试局部函数,并且把testCase当做参数提供给每个局部函数,如虚线线段所示。我们可以把Framework想象成一个流水线,用户只需要通过 runtests('testmainfunc.m') 把"testmainfunc.m"放到流水线上并且打开开关"就可以了。它是MATLAB的类 matlab.unittest.FunctionTestCase 的对象。 9 q( C: D; {, X- O2 D3 f2 B; z8 I$ [( k
Figure.3 单元测试Framework和测试函数的相互作用 ; P _$ @5 ]8 `# r, ~2 l0 E/ u返回的testCase是类 matlab.unittest.FunctionTestCase 的对象,有很多成员验证方法可以提供给用户调用,我们的第一版的getArea函数如下, 要求函数接受两个参数,并且都是数值类型: ) q8 D& p; V. z1 Y% 第一版的getArea函数 7 n+ {. m- M" I* Q
function a = getArea(wd,ht)8 ~& Q* d, ]. j2 O' Q
0 r. S3 k# O! t. E
p = inputParser; ' F& U h5 i$ f& L" @- t+ } ; Q( M" }( B. V. T2 q( b
p.addRequired('width', @isnumeric); % 检查输入必须是数值型的 0 F G- D9 g8 Z( V$ } p.addRequired('height',@isnumeric);8 Y3 @( \0 A9 p8 O8 s
! }. ^( {. H7 s* q) }1 [
p.parse(wd,ht);% S: N8 S5 E) [0 A" m6 p& T
5 d- B# |+ [# l n! f: f8 T. O a = p.Results.width*p.Results.height; % 从Results处取结果* H$ ~ F- {+ t& [' ]% S/ m
end 9 r o0 K/ @$ g9 H+ i8 R 0 F% {" y) I- T0 Z" S* e* s
我们先给这个getArea写第一个测试点,确保测试getArea函数在接受两个参数的时候,能给出正确的答案 & J* T- w: E' @# K% testGetArea.m & M& Z6 r4 n9 R& J1 k
function tests = testGetArea0 Q( a/ m; h: l; a4 ` t
tests = functiontests(localfunctions); % `+ K2 D& K2 E4 H v1 q end1 O2 m7 E/ Y( U! T, r( O
% 添加了第一个测试点; |8 ]8 E" T3 S! E3 f! _9 W
function testTwoInputs(testCase)3 x% ]8 [- R& l2 C6 x2 g
testCase.verifyTrue(getArea(10,22)==220,'!=220'); - c" \$ Y' H+ r
testCase.verifyTrue(getArea(3,4)==12,'!=12'); 8 k4 Y% d, f& w6 @1 X end 0 a% i! Y: b. M J9 s. P1 h1 ]2 f
; X. P* q% Y1 \' X我们给testGetArea.m添加一个局部函数叫做testTwoInputs,按照规定,该局部函数的名字要以test开头,后面的名字要能够尽量反应该测试点的实际测试的内容。verifyTrue是一个testCase对象所支持的方法,它用来验证其第一个参数,作为一个表达式,是否为真。verifyTrue的第二个参数接受字符串,在测试失败时提供诊断提示。 一个很常见的问题是: getArea是一个极其简单的函数,内部的工作就是把两个输入相乘,在这里验证 getArea(10,22) == 220 真的有必要吗?请读者记住这个问题,它是理解单元测试的精要之一。 下面我们来运行这个测试: / Y' W8 b W- w1 Y% command line ( K* K4 o2 {6 c. ] >> results =runtests('testGetArea') , C& W. E: T" k Running testGetArea# [8 H/ k; L$ F* R* Z$ L
. ) ~+ S$ F, {5 h6 J Done testGetArea / i% N; I5 E6 F6 @, z. S9 Q, j __________ $ w& Q7 {8 ?0 Y- K# |, ]$ M results = % 测试返回matlab.unittest.TestResult对象 z3 z( x7 ^+ A5 J7 ?) s+ q* I. `0 A
TestResult with properties: : {8 W; }! P) Y Name: 'testGetArea/testTwoInputs' 9 L: R/ |- I# Z% T8 W Passed: 1 - U% ]+ S' Q# P
Failed: 0 ! w9 }3 `* g" ?5 g6 o2 n) e Incomplete: 0' t2 ^1 Y: @. k8 M3 x: W# _" v
Duration: 0.0018 5 q" `. Y6 v& H' q- y Totals:. Y9 A$ S0 t, Q3 n A4 o
1 Passed, 0 Failed, 0 Incomplete.3 ]: _5 x2 P4 I" ?% W* s5 t
0.0018203 seconds testing time.) R2 f' i7 a9 k& D) a7 f1 L
w6 P5 X/ s7 s+ {& J3 d7 O/ z( \
测试返回一个 matlab.unittest.TestResult 对象,其中包括运行测试的结果,不出意料我们的函数通过了这轮简单的测试。 如果函数没有通过测试,比如我们故意要验证一个错误的结果: getArea(10,22) ==0& C, c8 F3 X( J( \$ j6 R4 S5 y2 R
% testGetArea.m / a1 i! d7 j% D a' Z. {! P
function tests = testGetArea # q, \: b, ?& k9 n# L tests = functiontests(localfunctions); 0 ~, Y7 l1 G x: ?, f% _3 Y end, V: }; Z* ]* m, g" [5 o/ H
function testTwoInputs(testCase); @4 _; S: {! N2 l5 ^# _' L- a
testCase.verifyTrue(getArea(10,22)==0,'Just A Test'); % 故意让验证失败/ G2 O/ Z; n: X B4 w
end - L" i+ J5 _5 g9 i8 c# X! L
# X) `) E [( S2 SFramework将给出详尽的错误报告, 其中 Test Diagnostic 栏目中报告的就是verifyTrue函数中的第二个参数所提供的诊断信息。 $ c3 v% @4 Z9 r% l1 s! Z% command line $ K3 D" }8 N( }4 ~) ]5 C
>> results =runtests('testGetArea') + H* L0 ~1 f' V4 T$ s8 L V Running testGetArea& @( x5 B5 Y$ H9 e# v8 w
================================================================================. I9 n0 o8 @6 N9 b0 ]( t
Verification failed in testGetArea/testTwoInputs. % 验证失败2 N# H$ q3 `4 }2 b8 x' W; ]0 @
---------------- 7 Y [# S' @% s. V n Test Diagnostic: % 诊断信息 6 }2 D/ |7 x3 c3 b. k, ` c ---------------- 5 u! L5 P' t+ |: o9 C8 V Just A Test ' z% W7 e( r! C B6 z+ c% c * h* n1 j5 U- q --------------------- " N7 @3 K* Y' G& d6 U( Z8 N Framework Diagnostic: & e1 x9 f- L/ K7 x" X ---------------------! O4 R% }# q8 {- c
verifyTrue failed. % 验证函数verifyTrue出错 . p8 ?& h/ J6 O7 d1 d
--> The value must evaluate to "true". 5 N: F/ _5 E$ z' k: h
% 验证的表达式getArea(10,22)==0的值应该为true 0 X; V3 r! w& l/ @% U$ O Actual logical: 3 P& i, w# e" C6 X) v4 h6 Y* N$ m
0 % 表达式的实际值为false * S2 ]- o8 {6 U, T4 s4 { ------------------ ' q% X/ U/ E' v1 o8 Q Stack Information: 6 j$ h7 Y( I% s; j ------------------ : b& C1 D1 M3 q$ b9 f7 j In testGetArea.m (testTwoInputs) at 6 % 测试点testTwoPoints出错+ S; I Z. ~' D
================================================================================8 j% f: k" X( Q, o" a1 ~
. & `* X/ w s' F; p- ^) W Done testGetArea6 e6 J) ?; A6 n* R/ v$ f5 }2 O
_________! [6 v1 ]7 t$ @+ X
Failure Summary: % 测试简报 $ Z0 A, Y$ W: w* M6 q. ]* e
Name Failed Incomplete Reason(s) + A& y1 z: d, ? q4 }0 X, \. I ========================================================================/ q2 r5 O b# m- H
testGetArea/testTwoInputs X Failed by verification. : |0 [+ i% x" M" Z5 b+ r % 出错的测试点名称 0 d! ?& i# }" N1 e7 g results = 5 d$ p, ~) t( T5 a
TestResult with properties: 5 ]/ W8 i2 c! R0 _6 j7 \% M& e $ S! L# r6 P. T$ [
Name: 'testGetArea/testTwoInputs'( ?) K3 M# T' I& D( G
Passed: 0 % 零个测试点通过+ V$ q+ g- l2 W2 V7 r& k
Failed: 1 % 一个测试点出错 5 V2 `5 Q% [- ]( I. s Incomplete: 0+ `( {8 z$ D7 h5 s9 k) o3 W
Duration: 0.0342- v# @( o1 k4 k; D/ {0 t% l% P
Totals:! v |: m5 O& L
0 Passed, 1 Failed, 0 Incomplete.7 f' h% R6 E0 N. D Z9 s
0.03422 seconds testing time. 5 @# J9 I- p+ s2 a+ [# T4 x
- j; G6 `" W! a- g7 |
我们再添加一个负面测试,回忆第一版的函数getArea不支持单个参数,如下:. I; c9 C# X, L+ `3 I/ U' T" ]1 r/ i
% command line 4 p9 E8 l9 ^3 @5 a+ F >> getArea(10) % 如预期报错 调用少一个参数 , {7 A! `% ^. M- q7 f6 t! U Error using getArea - V% c3 ` j+ ~0 @7 B
Not enough input arguments. ~+ L @2 E$ B. o. @9 ~ >> [a b] = lasterr % 调用lasterr得到error ID9 q3 X i* x3 E" h& a
a =, S! g! l0 E9 A F, o
Error using getArea1 (line 6)% q/ a! D: F$ t$ }
Not enough input arguments.$ [$ C0 t3 g% B: E* u$ t. F0 x
b = 8 u1 [5 D$ o" q- h& b9 J MATLAB:minrhs( W; o- g6 ~ K( Y. ^1 `
9 X" h f6 F9 w* k6 E0 C M3 E
我们可以利用lasterr函数得到了这个错误的Error ID,这个Error ID将在负面测试中用到。 下面是这个负面测试,验证在只有一个输入的情况下,getArea函数能够如预期报错。我们给测试添加一个新的测试点,叫做 testTwoInputsInvalid , u, ^; _; [$ _* c2 m$ B% testGetArea.m 0 w2 G+ g0 Y5 {+ F
function tests = testGetArea" A& z1 I5 g7 U% s9 c
tests = functiontests(localfunctions);; F/ e+ x0 u8 \! Y9 P* J
end " W6 y' B' a% u$ k6 w& c' { 1 ~1 j& x) h1 n/ Q: h
function testTwoInputs(testCase) 5 \) x2 [* W+ M5 Q0 h testCase.verifyTrue(getArea1(10,22)==220,'!=220'); 1 W2 c0 v7 X5 M* M. a2 w testCase.verifyTrue(getArea1(3,4)==12,'!=12'); L+ u3 y9 w1 a/ L8 p1 D! \1 P
end , q2 s( K! { z; D6 U q1 e0 a % 添加了第2个测试点 ! g1 l3 F) ~. G7 ^8 i function testTwoInputsInvalid(testCase)5 E2 H( v2 o1 Q/ g
testCase.verifyError(@()getArea1(10),'MATLAB:minrhs'); 1 ]5 \& p* e! P4 O9 x4 C* L1 k; x/ b" q end - d) b+ {% F! X' x9 p' ^- P2 \( d 1 E! P f3 o2 v# ~" S3 p( x. z在 testTwoInputsInvalid 中, 我们使用了测试对象的verifyError成员函数,它的第一个参数是函数句柄,即要执行的语言(会出错的语句),第二个参数是要验证的MATLAB错误的Error ID, 就是我们前面用lasterr函数得到的信息。verifyError内部还有try和catch,可以运行函数句柄,捕捉到错误,并且把Error ID和第二个参数做比较。 再举一个例子,我们先在getArea函数中规定所有的输入必须是数值类型,所以如果输入的是字符串,getArea将报错,先再命令行中实验一下,以便得到Error ID: # k" D$ V0 z [& i; `! t% 在命令行中得到Error ID 4 y' R i8 E7 m) H9 O. I$ [6 M
>> getArea1('10',22) . t+ \) j! b+ }3 @/ S5 Q' L Error using getArea1 (line 6) ' G% }# ]0 z! W- I1 ^+ x2 A) M The value of 'width' is invalid. It must satisfy the function: isnumeric. 4 ^# s/ z$ G/ U" _, ^ >> [a b] = lasterr 5 v$ z& v5 i4 p n8 q a =1 B9 F3 h3 w' l. }; Q
Error using getArea1 (line 6) 5 c3 o6 I+ v+ I9 S% W: x. R The value of 'width' is invalid. It must satisfy the function: isnumeric. : v# i0 E) l {. _3 {1 l- z2 { b = ' _/ O# r! v2 h9 b& l MATLAB:InputParser:ArgumentFailedValidation % 这个Error ID是我们需要的' U, y' o% K6 {2 o
s. g& m v) L8 t+ _' ?( L
然后再把这个负面测试添加到testGetArea中去 : {7 U9 }- S2 [. c2 j4 {: }% testGetArea.m % K' B K- D" i* q3 D1 }' u8 s
function tests = testGetArea 8 u" H. Z! ]0 a tests = functiontests(localfunctions); : z0 ~$ x0 s K) ] end # p+ ?1 r: T# J! [" i6 F1 v! p * x3 Y3 W& H$ c
function testTwoInputs(testCase)( ]! B) @2 R( ?
testCase.verifyTrue(getArea1(10,22)==220,'!=220'); 1 f! R5 q- m8 \! h& h& g& A testCase.verifyTrue(getArea1(3,4)==12,'!=12'); 3 M5 H6 l# q4 ~ a& g
end" o- g( C' S4 z- \" ^5 H0 H
* r d& l0 c/ R* a8 [0 N4 s function testTwoInputsInvalid(testCase)* a# J* f7 o' _% ^3 {
testCase.verifyError(@()getArea1(10),'MATLAB:minrhs'); - W- ~2 ~' }8 k0 b1 o testCase.verifyError(@()getArea1('10',22),... % 新增的test9 s& p3 S/ ?/ n0 D$ D; M2 P
'MATLAB:InputParser:ArgumentFailedValidation') / X* B6 w6 X* F7 A F1 t3 b end ' R K2 J% Z) Q( ^# H4 p- i' h
2 \( H9 K4 L3 D: n* g3 E* @运行一遍,一个正面测试,一个负面测试都全部通过。 8 t6 k$ }& M4 E- X, g4 O7 G% command line 8 {) O p2 }, i# d! ~ >> runtests('testGetArea') % E- k3 Z; @$ g Running testGetArea 6 X$ O0 R- i* F4 J/ h. o9 b& U" O .. & i- D) Y* q: a, j3 ]) h+ T8 u Done testGetArea/ L5 Y- H: y. v( p+ O
_________ 0 c- P. r: P* g* D8 L7 I$ _ ans = $ r. A7 N4 {( i/ v! x 1x2 TestResult array with properties:7 l5 I0 X0 }2 P( V" S/ r) P
Name 8 ]0 {" l5 b6 u- [) e6 U Passed1 N1 M( {. i0 t$ q' b$ s
Failed/ l: r5 E6 e: a
Incomplete $ w) { L8 V u& z' ~1 b% L Duration I* X% U; ?$ M Totals:; E. [0 T+ y. Y, V, t1 P3 ]
2 Passed, 0 Failed, 0 Incomplete. $ Q+ Q5 y- F) v. ~0 W! |: N 0.0094501 seconds testing time. ) ^: d% }$ ?/ L+ N( a
* P% b, x8 l& V+ [! w- J5 wgetArea函数的单元测试: 版本II & III, K9 V' z P% c: S8 e) M
回忆getArea函数的开发,第二个版本我们给getArea添加了可以处理单个参数的能力,并且把inputParser和validateAttributes联合起来使用。新的函数在原来的基础上可以应付如下的新的情况 & D& K( H7 U7 S: D/ b# L% command line " q& I7 w6 z3 ]* E0 J1 T6 J( X >> getArea(10) % 正确处理了单个参数的情况 , _- g3 ^; S' e r$ z- v4 ` ans = o: d0 [7 Q0 `2 Z) e4 J
100 . Y" |4 ], K& ?. A; B! ~ 9 @. ?+ K6 g9 ~) a- Z2 m >> getArea(10,0) % 如预期检查出第二个参数的错误,并给出提示$ j3 J6 c( ?; |, f& |
Error using getArea (line 37)6 W( _5 E% w" L; x
The value of 'height' is invalid. Expected input number 2, height, to be nonzero.: }5 w& i3 L# a6 A( Z
1 i0 c* c" M: ^- y( K) O >> getArea(0,22) % 如预期检查出第一个参数的错误,并给出提示7 L8 W3 \" a% E5 k" B+ ^3 j( p6 }4 l) L
Error using getArea (line 37) : v9 a: z7 K( U9 N' C The value of 'width' is invalid. Expected input number 1, width, to be nonzero. ( x! _, u' j* |7 T0 r% @: H) _$ q ; k T3 @) F+ l( W$ [8 q
在开发完这第二个版本的函数之后,我们首先运行了一下已经有的testGetArea测试,发现之前添加的一个测试点,验证函数在接受一个参数时会报错的情况已不再适用,因为我们已经开始支持单参数的功能了,所以要去掉它,随着程序算法的不断开发,修改或删除已有的测试是很常见的2 r* U# V( G6 e4 A, ?
% testGetArea.m : U$ P5 T* V- s
...( T# _+ Q8 y# Q
% testCase.verifyError(@()getArea1(10),'MATLAB:minrhs'); 需要去掉这个测试 ( d4 C& m/ D/ Y& R5 u2 A ... % F" F9 _7 o# G" `! b, M+ \ " M* Y0 B6 ]. F1 e; o去掉不再适用的测试之后,我们继续给单元测试添加新的测试点,首先添加一个Postive 测试点,确保getArea函数接受单一参数计算结果正确 " W9 g& `7 z! p- ?+ L6 t% 确保单一参数计算正确 , z# l. K @5 c% p! r' @
function tests = testGetArea1 N) W7 E2 n' t# ^0 D0 S% X" f
...从略, t/ d% V! F" j. f
z+ d9 \) o. M
function testOneInput(testCase) , t& c3 F3 `% i; y7 H4 b# O& u testCase.verifyTrue(getArea2(10) ==100,'!=100');1 z8 a- }: j5 l, K. b
testCase.verifyTrue(getArea2(22) ==484,'!=484'); & O1 o$ M& ~" |* L+ `8 z end . o$ \! _4 n! {' M! f8 g 7 M3 ], Q; g5 n/ ^
再添加一个Negative测试点,确保getArea函数会处理输入是零的情况4 o+ X. l4 Y1 V9 V
% 保证不接受零输入 % |7 K4 y/ ~) l8 o
function tests = testGetArea ' W& p' Z3 x, B ...从略( r U2 U) e. I2 T6 J
. X$ F8 J2 b S
function testTwoInputsZero(testCase)+ z- y# l+ S+ ^
testCase.verifyError(@()getArea(10,0),'MATLAB:expectedNonZero'); 4 N5 I3 [( i0 w3 k" M S testCase.verifyError(@()getArea(0,22),'MATLAB:expectedNonZero'); ' A- U1 H5 [6 ]/ @7 _# b end. c7 x$ V4 E* w
% U; l9 e3 M& r+ O
然后调用1 h- W& ?' b! w: o- G
% command line 3 v7 S3 }' U9 E* Z( M5 }& H; ^. _
>> runtests('testGetArea') $ d8 t' I; J/ L8 C' z3 ?6 A3 Z ... 8 Y6 I9 N, E; J( ~1 x : F$ t) W* R9 h! t+ N. n每次运行这个命令,会运行之前所有的测试点和新的测试点,这也就保证了对新添加的算法没有破坏以前有的功能。我们前面问了一个问题: 验证getArea(10,22) == 220 真的有必要吗。: V! _' B3 E6 A* }: W9 M
其必要性之一,也是单元测试功能之一:即这个验证其实是对getArea能正确处理两个参数的能力的一个历史记录。因为我们在不停的算法开发中,很难保证不会偶然破坏一些以前的什么功能,但是只要有这条测试在,无论我们对getArea函数做怎样翻天复地的修改,只要一运行测试,都会验证这条历史记录,确保我们没有损坏已经有的功能,换句话说,新的函数是向后兼容的。对于一个科学工程计算系统来说,一个函数会被用在很多不同的地方,向后兼容让我们放心的继续开发新的功能,而不用担心是否要去检查所有其它使用该函数的地方。所以从这个角度说:单元测试是算法开发的堡垒,算法的开发应该以单元测试来步步为营,在确保算法没有退化的基础上开发新的内容。话说回来,为了让这个版本的getArea能够顺利运行,我们确实去掉了一个对单一参数报错的测试,因为函数开始支持这种功能了,这种做法和我们说以单元测试步步为营并不矛盾,如果新的算法导致旧的测试失败,我们要根据实际情况,酌情决定是修改算法还是修改测试。 0 _* }( ]. m' O4 W! F3 ^9 D+ y在getArea的第三个版本中,我们给函数添加了两个可选的参数:shape和units,并且它们的顺序可以相互颠倒的。新的函数可以应付如下的情况: ' E2 ]7 d1 x/ ~1 k P$ `% command line 9 O. U4 a1 d# _6 w$ W; V >> getArea(10,22,'shape','square','units','m') %接受两对name-value pair + L, B$ [2 t/ w& I: ` ans = %--name value --name value " V( D& c2 H4 h* T area: 220: z4 W5 Q# @; m* l7 G8 J
shape: 'square' ; i, g" T% t9 F" P( B- o) W% e units: 'm'. I' F" y" x# @& M
# J# l" u/ J& F >> getArea(10,22,'units','m','shape','square') % 变化了参数的位置 # x. q" f( j8 C" Q% \ m4 }6 m/ O ans = 1 M6 H/ E* _. F) k Q area: 220 % |3 \2 x1 a( h- f) W shape: 'square'& e; O! X( L: Y" b4 a5 G6 I
units: 'm' 7 ]+ u; s& n. I, C( H7 d5 D! P' x( F 7 |1 G4 O# B% ?" P* C
5 }& Q; X7 V& D! K# `
>> getArea(10,22,'units','m') % 仅仅提供unit参数 9 W% n) f+ z' ?9 m ans = ' s; q [. ]- N# q( ]9 Z% p* ~ area: 220- P- l6 R; S) l- p* c [5 v2 p: `
shape: 'rectangle'3 t q4 Q' m: C: z8 B) e$ g# V
units: 'm' : s9 ?' ~3 b: P$ C
4 N; H, [) c& I1 Q1 @1 W0 _
为其添加的新的测试点如下:( @% ^5 n6 P5 ?
% testGetArea . b7 u$ ^2 Y8 y- ^" v
function tests = testGetArea * y0 `! c2 F. P ...从略9 c6 Y9 f$ K. X$ i% N9 u
7 A* H2 f2 n; I7 S0 y& J5 S function testFourInputs(testCase) % 记录可以支持四个参数的情况7 f7 F+ C6 L, T3 K0 h8 w
actStruct = getArea5(10,22,'shape','square','unit','m'); ) u& X* {$ i( ?! B8 C- d expStruct = struct('area',220,'shape','square','units','m'); U5 J2 v# P/ {3 }. [, H9 ? testCase.verifyEqual(actStruct,expStruct,'structs not equal');7 X) i0 g( \' @) R% p
6 w: L H( J) f actStruct = getArea5(10,22,'unit','m','shape','square');8 l; `0 v( |! m. p
expStruct = struct('area',220,'shape','square','units','m');8 A5 S' r. T; ~1 ?7 O j
testCase.verifyEqual(actStruct,expStruct,'structs not equal');. C! e5 Q7 K' C* J: K+ q* z
end + R! T9 D o, ~5 B5 u u ! N! J* @% K/ N) }4 d
K y/ j8 O' g( c8 T, e function testThreeInputs(testCase) % 记录可以支持三个参数的情况# `6 Q; A( x3 M0 k! p
actStruct = getArea5(10,22,'units','m');. z- P9 T8 m# k( k
expStruct = struct('area',220,'shape','rectangle','units','m'); : C" K6 t/ w9 l) M* p" D' M testCase.verifyEqual(actStruct,expStruct,'structs not equal'); ; C& ~5 q3 l8 j6 g9 c# H# E end' A! D( e1 \9 ~: J
) M0 l6 Q. P( k( C# A 1 g# v! j! b+ ?7 H: T# M" x6 p在testFourInputs中,我们从getArea函数那里先得到一个结构体,命名叫做actStruct(实际值) 然后准备了一个结构体叫做 expStruct (期望值),然后把用verifyEqual方法来作比较 在testThreeInputs中,我们调换的第三和第四个参数的位置,确保结果依然是我们预期的。 5 ~: u$ [9 O& c ?测试的准备和清理工作: Tests Fixtures ; a/ c2 m% f- u本节介绍单元测试系统中另一个很重要的概念叫做Fixture。假设我们要给图形处理的一系列算法写测试,这些算法需要图像数据作为输入,所以在测试之前,我们需要先载入图像数据,按照上节的例子,单元测试看上去是这样的。 3 L$ |2 o: w# t! i% testImgProcess $ U6 O `) R! C$ q t
function tests = testImgProcess( ) # w1 K/ @8 m% b l7 t% f( n tests = functiontests(localfunctions);5 z: F# q. J7 {# z" F
end& Z& D% Z' W) W/ W+ w! M& e
1 [1 m, g1 m3 w$ }5 C9 R
- L/ H! L8 B( o; ^2 W0 |0 l
function testOp1(testCase) 2 n. V: T W3 J& a) n7 s( V+ x img = imread('testimg.tif'); % 载入图像 5 W9 q/ e5 a% F* j( J8 [ Op1(img); . C" N& u3 f" d$ f8 g$ w % ... rest of the work ( L. S# `% {% q6 P. K8 Y
end3 ]; W% T6 L+ a3 x
; O. V3 ?, t4 p- t! y4 v- y
function testOp2(testCase) ' P6 m u" t8 {. S
img = imread('testimg.tif'); % 载入图像7 ?$ n9 l5 f( s: A8 \
Op2(img); ; W. P/ @4 F" h4 h7 y$ i W % ... rest of the work 0 v* l( W0 A3 m5 j* p2 k5 W8 l
end ) Q$ ]( E7 Z0 K8 x+ k, F 4 ~9 C- s/ ~9 ?+ |* R3 B, T
可以观察到,在每个测试点的一开始,都有同样的准备工作,就是打开一个图像。在单元测试中,这叫做Test Fixture, 即每个测试的共同准备工作。如果这个测试函数中有很多这样的测试点,每次都要重复的调用imread操作很麻烦。对于这样的准备工作,我们可以把它们放在一个叫做setup的局部函数中,该函数统一地在每个测试点的开始之前被调用。这样就不用在每个测试点中都包括一个imread的调用了。新的测试看上去是这样的:4 w8 T; ?( j. {; W4 a1 r
% 使用setup和teardown ; ?) ^* E1 d. K% v
function tests = testImgProcess( ) 9 O# H( I5 c5 c tests = functiontests(localfunctions);- ^4 ^3 j3 c/ [3 b/ W- f* D
end" j6 }+ [& j( R1 j! c- ?. ]
9 U) l) o9 U) x( }) K function setup(testCase) # q8 I# @- G5 X testCase.TestData.img = imread('corn.tif');9 k2 P8 P3 e3 d( U! p( R
% 其它的准备工作 * G/ r3 \. f0 o. M/ W% x end- I: D) U' c4 t N; q
function teardown(testCase)7 \ r) b$ b, u0 `3 D1 Q- G+ L, A% ?
% 其他清理工作 & @% H+ D& a" S0 _ end 2 V3 {6 |" X0 l' L8 U function testOp1(testCase) . P l, p) @6 G6 t( t, I newImg = Op1(testCase.TestData.img); % 直接使用对象testCase的属性TestData ' d3 r& }: ?" z1 k; G % ... rest of the work + o+ W% _0 \+ o$ w end# R2 L0 D' U* I/ i
1 M, l; b2 E- v, {" { function testOp2(testCase) , Z8 W% I7 T9 S* V+ @' ?, N/ k newImg = Op2(TestCase.TestData.img);h ! A v* F, Z3 c % ... rest of the work ) L' R$ l1 u' l) ]1 h( H" H4 C+ Y4 p
end & x0 D M! O2 x, \ ' M& E* i3 n- }0 \1 {/ Z- M g在setup方法中,我们打开一个文件,并把数据动态地添加到testCase对象的TestData结构体上,在之后的每个局部测试点中,我们可以通过 testCase.TestData.img 来访问这个数据。 setup中还可以放其他的准备工作,比如创建一个临时的文件夹放置临时的数据等待。对应的teardown函数中用来存放每个局部测试点运行完毕之后的清理工作,比如清除临时文件夹。 setup和teardown方法在每个局部测试点的开始和结束后运行,所以如果该主测试文件有两个测试点,那么setup和teardown各被运行了两次,流程如图所示:, j! m9 o# p' Z$ n6 t6 @
}9 G( c1 V O& ]& kFigure.4, setup和teardown方法在每个局部测试点的开始和结束后运行 : z$ @ S, @! C. k0 o如果还有一些准备和清理工作只需要开始和结束的时候各运行一次,那么可以把他们放到setupOnce和teardownOnce中去,比如我们要验证一些算法,而给该算法提供的数据来自数据库,在运行算法测试之前,要先连接数据库,在测试结束之后,要关闭和数据库的连接,这样的工作就符合setupOnce和teardownOnce的范畴,如下所示:" J. x7 X; o2 O) Q6 R- A
% 使用setupOnce teardownOnce来管理对数据库的连接 + w. r( ~' s0 T1 Y! `4 S+ J0 j1 E" S function tests = testAlgo( ) " O+ g; D, x7 L& |1 }' _ tests = functiontests(localfunctions); - @% K; B; p- C, F/ `. P end 3 W" @7 X+ K& b' @3 S 1 h7 S4 z6 _6 L, A9 a' w$ f' y7 M- U function setupOnce(testCase)3 t. @7 r8 y) s
testCase.TestData.conn = connect_DB('testdb'); %一个假想的连接数据库的函数7 o2 l' D/ c5 B% h9 Q
end 2 z! y/ l7 g/ s. H5 ?1 J function teardownOnce(testCase) 4 ^ l6 a2 M- D; o disconnect_DB(); 4 q' y: H* J1 v, \: q6 M end) @) S- @( E+ M& m0 ]4 R9 }8 c
function testAlgo1(testCase)( } o$ l4 k" G6 P: R
% retrieve data and do testing : R+ D# y1 j: | end 6 E/ I/ i, `9 K) Q$ Q. a : A9 k( i6 T# ~ function testAlgo2(testCase): N, j2 b+ C3 M' S# {1 [- G
% retrieve data and do testing : G) m+ f, g* F8 M. O! `& V 8 f6 t! L2 a8 _- A# R
end z" ~' j) I6 J K! k( T1 o7 r
; K7 F+ \' x" t& N
setupOnce和teardownOnce方法仅仅在整个测试开始和结束时运行一次,流程如图 Figure.5 所示 5 K) s- n0 E- S# s' e& G L) _8 a: m2 X" S
Figure.5, setupOnce和teardownOnce方法仅仅在整个测试开始和结束是运行一次1 p; L" J3 M. H. d
setupOnce,teardownOnce和setup,teardown也可以联合起来使用,如图Figure.6 所示: v: f. m# {, }2 f" `) u/ W5 S |- y ' `: O6 G* t, b% @! {9 {, hFigure.6 setupOnce,teardownOnce和setup,teardown联合起来使用' R# [ @+ |. C9 s
验证方法: Types of Qualification" U2 r' h5 I# X" z8 Q
在getArea函数的单元测试: 版本I 节中我们提到,如下的测试点中:* Y) N5 ]; h7 V
% testGetArea.m & V1 i6 K, O v+ L) q+ {
function tests = testGetArea / M0 W4 \1 l% P( j5 h7 {6 r. K+ E: K) w tests = functiontests(localfunctions);; c/ K/ B) b, T; M5 x- }% }% [; x
end 4 i5 i4 r( K/ j1 K) U % 添加了第一个测试点 7 J# o+ m: I# V! W function testTwoInputs(testCase) & }( e- g: Z0 _! T8 f+ Z# m testCase.verifyTrue(getArea(10,22)==220,'!=220'); . P, x( M8 O, N# i testCase.verifyTrue(getArea(3,4)==12,'!=12'); ( n: p/ k: ?" g5 A1 ^' z end - P, x% Y4 O# |4 X8 m: E
' U% g# _, I1 @5 r* S% f9 M
参数testCase是类 matlab.unittest.FunctionTestCase 的对象,由Framework提供,该类有很多成员验证方法可以提供给用户调用,比如前几节用到的verifyTrue 和 verifyError ,这个两个验证方法最常见。全部的验证方法下表所示: , M% N* a7 y/ T* |6 e) X, X验证方法 验证 典型使用 |6 G' o$ P6 s; \
verifyTrue 表达式值为真 testCase.verifyTrue(expr,msg)) e2 k4 D; h$ ^/ E& c7 O: l
verifyFalse 表达式值为假 testCase.verifyFalse(expr,msg) " E+ j- D- | j, e' @* ]% S" {- y( F1 ~verifyEqual 两个输入的表达式相同 testCase.verifyEqual(expr1,expr2,msg); w/ R/ Q" v0 W! M0 w
verifyNotEqual 两个输入的表达式不同 testCase.verifyNotEqual(expr1,expr2,msg)" r) M0 P% y# G3 n O3 n
verifySameHandle 两个handle指向同一个对象 testCase.verifySameHandle(h1,h2,msg)6 _1 Z- z! f0 a: e$ h" S+ Q
verifyNotSameHanle 两个handle指向不同对象 testCase.verifyNotSameHandle(h1,h2,msg)# `9 s- ]' f! P9 L/ G
verifyReturnsTrue 函数句柄执行返回结果为真 testCase.verifyReturnsTrue(fh,msg) 3 S% T m f' U0 TverifyFail 无条件产生一个错误 testCase.verifyFail(msg) ' Y! Y+ c% Z+ A$ {7 L5 B ]+ J+ SverifyThat 表达式值满足某条件 testCase.verifyThat(5, IsEqualTo(5), '') " ^9 v+ M' t; V4 J$ z% }0 J* F, n- ~% lverifyGreatThan 大于 testCase.verifyGreaterThan(3,2) ' O- g* f' G8 h, @verifyGreaterThanOrEqual 大于等于 testCase.verifyGreateThanOrEqual(3,2) 3 c. L/ Z% O- b; t8 N. pverifyLessThan 小于 testCase.verifyLessThan(2,3)3 M0 n/ x3 r5 M! N
verifyLessThanOrEqual 小于等于 testCase.verifyLessThanOrEqual(2,3) 8 `/ c- T# f3 F) V2 cverifyClass 表达式的类型 testCase.verifyClass(value,className) 6 {5 u+ s( n# bverifyInstanceOf 对象类型 testCase.verifyInstanceOf(derive,?Base)- k0 D2 a m) F! w! r
verifyEmpty 表达式为空 testCase.verifyEmpty(expr,msg)6 _- V5 t8 A J) m( @
verifyNotEmpty 表达式非空 testCase.verifyNotEmpty(expr,msg) 5 G" I7 x) n, o" k# |6 r! PverifySize 表达式尺寸 testCase.verifySize(expr,dims)* ~& r' u. D9 |+ U& l9 }- M& D
verifyLength 表达式长度 testCase.verifyLength(expr,len) p! b0 ?2 {, x, u# l
verifyNumElements 表达式中元素的总数 testCase.verifyNumElements(expr,value) f2 `# O' w5 T& nverifySubstring 表达式中含有字串 testCase.verifySubstring('thing','th'); l7 u* h7 p3 v3 F
verifyMatches 字串匹配 testCase.verifyMatches('Another', 'An'), C# j$ Z0 l6 V/ y2 |4 C
verifyError 句柄的执行抛出指定错误 testCase.verifyError(fh,id,msg)+ K3 t, i6 |2 \4 z+ w d
verifyWarning 句柄的执行抛出指定警告 testCase.verifyWarning(fh,id,msg) ( R8 W5 t! v/ f) ?- |0 B; m% z2 AverifyWarningFree 句柄的执行没有警告 testCase.verifyWarningFree(fh) 1 z- z4 K A, c* r6 k除了verify系列的函数,MATLAB单元测试还提供" _5 V9 l- N3 C* D! y a' [' f: C5 m4 X, n
assume系列 2 I/ [. z. K) W6 z5 c( o: Tassert系列. t* A) w0 f4 C+ [/ y6 P: {4 ?
fatalAssert系列 . K/ p- L% c8 M& V, M: u的验证函数,也就是说,上面每一个verify函数,都有一个对应的assume,assert和fatalAssert函数。比如除了verifyTrue,还有assumeTrue,assertTrue,fatalAssertTrue三个验证方法。 , p' n5 X/ C8 [% Vassume系列的验证方法一般用来验证一些测试是否满足某些先决条件,如果满足,测试继续,如果不满足,则过滤掉这个测试,但是不产生错误。比如下面的测试点,如果测试者的意图是:在Windows平台下才执行,没有必要在其它平台下执行 & G3 ?2 B+ v% U9 o3 t R% tFoo.m 8 n9 c0 [% {+ x7 }7 b$ }% L
function tests = tFoo3 D+ N" b1 z$ j6 j7 u. O
tests = functiontests(localfunctions); 0 M! s/ o5 H. S* _ m end7 {1 H! w- Q8 U! M( \- X
+ x; s0 n T" B
function testSomething_PC(testCase) 6 L; m& V ~- @. [: y, t testCase.assumeTrue(ispc,'only run in PC'); % 如果这个测试点在其它平台运行,7 d/ ^: V2 w/ n$ g4 h
% 则显示Incomplete " S, l2 ?& G$ p1 T( w8 c % .... $ p4 G3 \! I: b5 h% v+ r8 y- j" O- N end + B6 ~- r" O6 |" a6 I( O
" V5 t) W ?0 V W如果我们在MAC下运行这个测试,则显示' e: g8 J3 @5 Z: H& t
>> runtests('tFoo')2 y9 u% K5 G, O2 L/ k$ x8 l/ G9 |
Running tFoo% ~1 |, j" {0 @9 x" i
================================================================================ 1 T; j- Z& ?: C S3 I0 T4 v tFoo/testSomething_PC was filtered.* u7 P, E1 _; i. W" \
Test Diagnostic: only run in PC / @; w. l f! n+ P. ~9 s! Z Details - [) n, B6 X A/ p b! x/ A ================================================================================ $ W6 x$ ?' M- ?5 a' G .7 r; F- W0 v6 O8 |
Done tFoo / L# H" k( t2 @+ h, u __________ % p) J% k% S3 @# ^ , x, S* [/ y' y4 q/ d
Failure Summary:0 `7 y/ B2 L6 S' M- N+ ^/ b0 ^
+ o8 o/ \3 O$ S3 B Name Failed Incomplete Reason(s): T& V" B# ?4 y, U% R' A
==================================================================== 3 K5 V# p0 u/ R! X. f+ K5 ] tFoo/testSomething_PC X Filtered by assumption. % F% ^+ t$ L5 l9 Z2 P) R7 q2 g 该测试被过滤掉了 ' m i; ?; g% T9 r1 t ans = ; G! }. S8 X9 b. C$ c TestResult with properties:1 |$ t; y/ F* T6 C
) E' `9 R+ ^# ^, b1 g Name: 'tFoo/testSomething_PC'9 f' t7 ?8 w6 }+ w/ s: u
Passed: 0" l- f7 T2 c# A. ^
Failed: 0 3 |1 W L' h$ E- n" s Incomplete: 12 z( D' [5 U; {2 k7 Z/ t
Duration: 0.0466 ( s: b+ j4 r( r& P& I - u4 U) r4 a. ]
Totals: " B& v# a, Z3 T$ n: f 0 Passed, 0 Failed, 1 Incomplete. ' e) j( c# ~* G: n R 0.046577 seconds testing time. ! Y, d; C& P; Z9 j. i! }/ w) l
1 X1 [3 N2 c3 \& r& y B3 q
assert系列的验证方法也是用来验证一些测试是否满足某些先决条件,如果满足,测试继续,如果不满足,则过滤掉这个测试,并且产生错误。但是它不会影响其余的测试点。比如下面这个例子,testSomething测试点中,我们要求该测试的先决条件是数据库必须先被连接,如果没有连接,那么没有必要进行余下的测试,并且testA的测试结果显示失败。但是这个失败将不会影响testB测试点的运行! f7 W3 }1 a o6 h
function tests = tFoo & A' v+ e4 z9 v) T A' t tests = functiontests(localfunctions); 7 a2 X' c6 _" H2 G" q end ( n# v$ g2 H" E# u# v! K0 W 4 f# Z5 P& |* ^0 l ?7 y( ~3 v function testA(testCase) - l. l: c! |9 t
testCase.assertTrue(isConnected(),'database must be connected!') 9 E6 b( K( f# y- {7 r' B
% 其它测试内容 # I( [ x3 B" I+ f$ j/ v
end : K6 I" C5 X ]1 V/ C( u
1 {, g% V. k0 A2 ~( ] function testB(testCase) # {& H+ ?' X8 P C testCase.verifyTrue(1==1,'');! S; H& `" ]! l+ O
end , M1 [/ w: k! f) N - {2 h" z; q+ r) ~/ g* H6 x. }* u
运行这个测试,显示如下 3 n7 d. [! R( Q- z Q: E1 V% command line 6 Q# J) U4 v6 r6 A% E
>> runtests('tFoo')# G* B; R1 j, H1 U# P9 b/ w
Running tFoo, ?3 T+ f0 N2 P, T
================================================================================+ ^* K+ e! e3 }
Assertion failed in tFoo/testA and it did not run to completion.8 r) h' F; f! o; x5 k p S
----------------5 v/ X6 V: [6 U1 G3 ?. n
Test Diagnostic: / A7 R n6 R( z7 B. J& a# M: H ---------------- ' e. b( J9 x8 { database must be connected!) V' t/ @$ o3 O B9 A1 x( s
--------------------- , q: P9 T' s, k$ J; b Framework Diagnostic:0 W8 J/ a$ O1 {& q3 W6 |% S
--------------------- # Q. @" ?8 |' S2 m: F assertTrue failed.6 K/ n! j7 o" o$ }
--> The value must evaluate to "true".( a5 g- g, G i+ B1 S& `7 `
5 A, m8 s3 n: j& c6 V* A& R$ }
Actual logical:$ h% _+ Q8 j. T) x
0$ z! |7 ~0 \! ^6 o
------------------* x m" }; y' R, W# F
Stack Information:( |; v! L* p: ?2 n7 X: T
------------------6 ]$ X7 f1 Z7 o& ^3 {0 g- _5 @
In /Users/iamxuxiao/Documents/MATLAB/tFoo.m (testA) at 6 & c% h3 Z4 u% V: k ================================================================================" R3 Z. P: B; m& B2 r# Y
.. \' o: k- x8 g* R8 e: e9 y+ }) K Done tFoo / i& t& N# [ \; |. P7 K __________' r7 i Y' M6 [* \$ D" h