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