赌场送彩金35|临海赌场看场子

MATLAB单元测试

2019-3-3 09:33| 发布者: ilovematlab| 查看: 29814| 评论: 1|原作者: oopmatlab

摘要: 本篇是把现代软件工程思想应用到MATLAB工程开发中的精髓,希望高级MATLAB用户仔细研读。作者用实际的例子解释在开发和逐渐改进算法的时候,如何保证程序已有的功能没有收到影响,步步为营,让算法开发和测试系统的建 ...

文章PDF浏览下载链接

目录:

inputParser章节中,我们通过不?#32454;?#36827;getArea函数对输入?#38382;?#30340;处理方法,引入这样一个观点:一个可靠的科学工程计算项目必须有一套测试系统,才能防止开发的过程中算法退化,工程项目的推进必须在算法开发和算法测试之间不?#31995;?#20195;完。在inputParser章节的最后,还根据?#26412;?#25552;出了一个测试系统所应该有的基本功能。在本章中,我们将学习MATLAB提供的测试解决方案:MATLAB单元测试(MATLAB Unit Test)。

基于函数的(Function-Based)单元测试的构造

MATLAB基于函数的单元测试构造很简单,如图Figure 1所示:用户通过一个主测试函数和若干局部测试函数(?#27493;?#20570;测试点) (Local Function)来组织各个测试。而测试的运行则交给MATLAB的单元测试架构(以下简称Framework)去完成。
Figure.1 单元测试Framework和测试函数
主测试函数和局部测试函数看上去和普通的MATLAB函数没有区别,其结构如图Figure 2 所示,只是命名上有一些规定而已,这些特殊的规定是为了Framework可以和测试函数契合而规定的。
Figure.2 简单的主测试函数和若干局部的测试函数构成的一个单元测试
命名规则如下:
  • 主函数的名称由用户?#25105;?#25351;定,和其他的MATLAB函数文件一样,该文件的名称需要和函数的名称的相同. (如果主函数的名称是 testmainfunc , 该文件名称则是testmainfunc.m )。
  • 在主函数中,必须调用一个叫做 functiontests 的函数,搜集该函数中的所有局部函数, 产生一个包含这些局部函数的函数局部的测试矩阵并返回给Framework
如下所示:
% testmainfunc.m 
  function tests = testmainfunc
    tests = functiontests(localfunctions);   % 主测试函数中必须要有这个命令
  end
  ... 
 
其中 localfunctions 是一个MATLAB函数,用来返回所有局部函数的函数句柄。 局部函数的命名必须以 test 开头,局部函数只接受一个输入?#38382;?#21363;测试对象,即下面例子中的形参 testCase
% testmainfunc.m 
  ...
  function testPoint1(testCase)    % 只接受一个输入?#38382;?
      testCase.verifyEqual(.....);
  end 
 
  function testPoint2(testCase)    % 只接受一个输入?#38382;?
      testCase.verifyEqual(.....);
  end 
  ...
 
其中testCase由单元测试Framework提供,即Framework将自动的调用该函数,并且提供testCase?#38382;? 按照规定,要运行单元测试中的所有测试,必须调用runtests函数
% command line 
   >> runtests('testmainfunc.m') 
 
下面用我们用基于函数的单元测试来给getArea函数的构造其单元测试。

getArea函数的单元测试: 版本 I

首先给主测试文件起个名字叫做testGetArea,该名字是?#25105;?#30340;,为了便于理解名字里面通常包含test,并包含要测试的主要函数的名字:
% testGetArea.m 
 function tests = testGetArea
   tests = functiontests(localfunctions);
 end 
 
在该主函数中,localfunctions将搜集所有的局部函数,构造函数句柄数组并返回测试矩阵。这里自然会有一个问题,这个tests句柄数组将返回给谁,这就要了解Framework是如何和测试相互作用的。 如图Figure.3 所示,整个测试从 runtests('testmainfunc.m') 命令开始, 命令函数,Framework将首先调用testGetArea的主函数,得到所有的局部函数的函数句柄,如空心箭头线段所示,然后Framework再负责调用每一个测试局部函数,并且把testCase当做?#38382;?#25552;供给每个局部函数,如虚线线段所示。我们可以把Framework想象成一个流水线,用户只需要通过 runtests('testmainfunc.m') 把"testmainfunc.m"放到流水线上并且打开开关"就可以了。它是MATLAB的类 matlab.unittest.FunctionTestCase 的对象。
Figure.3 单元测试Framework和测试函数的相互作用
返回的testCase是类 matlab.unittest.FunctionTestCase 的对象,有很多成员验证方法可以提供给用户调用,我们的第一版的getArea函数如下, 要求函数接受两个?#38382;?#24182;且都是数?#36947;?#22411;:
% 第一版的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  
 
我们先给这个getArea写第一个测试点,确保测试getArea函数在接受两个?#38382;?#30340;时候,能给出正确的答案
% 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添加一个局部函数叫做testTwoInputs,按照规定,该局部函数的名字要以test开头,后面的名字要能够尽量反应该测试点的实际测试的内容。verifyTrue是一个testCase对象所支持的方法,它用来验证其第一个?#38382;?#20316;为一个表达式,是否为真。verifyTrue的第二个?#38382;?#25509;受字符串,在测试失败时提供诊断提示。 一个很常见的问题是: getArea是一个极其简单的函数,内部的工作就是把两个输入相乘,在这里验证 getArea(10,22) == 220 真的有必要吗?请读者记住这个问题,它是理解单元测试的精要之一。 下面我们来运行这个测试:
% command line 
 >> results =runtests('testGetArea')
 Running testGetArea
 .
 Done testGetArea
 __________
 results =    % 测试返回matlab.unittest.TestResult对象
   TestResult with properties:
           Name: 'testGetArea/testTwoInputs'
         Passed: 1              
         Failed: 0
     Incomplete: 0
       Duration: 0.0018
 Totals:
    1 Passed, 0 Failed, 0 Incomplete.
    0.0018203 seconds testing time.
 
测试返回一个 matlab.unittest.TestResult 对象,其中包括运行测试的结果,不出意料我们的函数通过了这轮简单的测试。 如果函数没有通过测试,比如我们故意要验证一个错误的结果: getArea(10,22) ==0
% 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函数中的第二个?#38382;?#25152;提供的诊断信息。
% 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不支持单个?#38382;?#22914;下:
% command line 
 >> getArea(10)          % 如预期报错 调用少一个?#38382;?
 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
 
我们可以利用lasterr函数得到了这个错误的Error ID,这个Error ID将在负面测试中用到。 下面是这个负面测试,验证在只有一个输入的情况下,getArea函数能够如预期报错。我们给测试添加一个新的测试点,叫做 testTwoInputsInvalid
% 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
 
testTwoInputsInvalid 中, 我们使用了测试对象的verifyError成员函数,它的第一个?#38382;?#26159;函数句柄,即要执行的语言(会出错的语句),第二个?#38382;?#26159;要验证的MATLAB错误的Error ID, 就是我们前面用lasterr函数得到的信息。verifyError内部还有try和catch,可以运行函数句柄,捕捉到错误,并且把Error ID和第二个?#38382;?#20570;比较。 再举一个例子,我们先在getArea函数中规定所有的输入必须是数?#36947;?#22411;,所以如果输入的是字符串,getArea将报错,先再命令行中实验一下,以便得到Error ID:
% 在命令行中得到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.  
 

getArea函数的单元测试: 版本II & III

回忆getArea函数的开发,第二个版本我们给getArea添加了可?#28304;?#29702;单个?#38382;?#30340;能力,并且把inputParser和validateAttributes联合起来使用。新的函数在原来的基础上可以应付如下的新的情况
% command line 
 >> getArea(10)    % 正确处理了单个?#38382;?#30340;情况
 ans =
    100
 
 >> getArea(10,0)  % 如预期检查出第二个?#38382;?#30340;错误,并给出提示
 Error using getArea (line 37)
 The value of 'height' is invalid. Expected input number 2, height, to be nonzero.
  
 >> getArea(0,22)  % 如预期检查出第一个?#38382;?#30340;错误,并给出提示
 Error using getArea (line 37)
 The value of 'width' is invalid. Expected input number 1, width, to be nonzero.  
 
在开发完这第二个版本的函数之后,我们首先运行了一下已经有的testGetArea测试,发现之前添加的一个测试点,验证函数在接受一个?#38382;?#26102;会报错的情况已不再适用,因为我们已经开始支持单?#38382;?#30340;功能了,所以要去掉它,随着程序算法的不断开发,修改或删除已有的测试是很常见的
% testGetArea.m 
   ...
  % testCase.verifyError(@()getArea1(10),'MATLAB:minrhs');  需要去掉这个测试
   ...
 
去掉不再适用的测试之后,我们继续给单元测试添?#26377;?#30340;测试点,首先添加一个Postive 测试点,确保getArea函数接受单一?#38382;?#35745;算结果正确
% 确保单一?#38382;?#35745;算正确 
 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
 
然后调用
% command line 
 >> runtests('testGetArea')
 ...  
 
?#30475;?#36816;行这个命令,会运行之前所有的测试点和新的测试点,这也就保证了对新添加的算法没有破坏以前有的功能。我们前面问了一个问题: 验证getArea(10,22) == 220 真的有必要吗。
其必要性之一,也是单元测试功能之一:即这个验证其实是对getArea能正确处理两个?#38382;?#30340;能力的一个历史记录。因为我们在不停的算法开发中,很难保证不会偶然破坏一些以前的什么功能,但是只要有这条测试在,无论我们对getArea函数做怎样翻天复地的修改,只要一运行测试,都会验证这条历史记录,确保我们没有损坏已经有的功能,换句话说,新的函数是向后兼容的。对于一个科学工程计算系统来说,一个函数会被用在很多不同的地方,向后兼容让我们放心的继续开发新的功能,而不用担心是否要去检查所有其它使用该函数的地方。所?#28304;?#36825;个?#23884;人担?#21333;元测试是算法开发的堡垒,算法的开发应该以单元测试来步步为营,在确保算法没有退化的基础上开发新的内容。话说回来,为了让这个版本的getArea能够顺利运行,我们确实去掉了一个对单一?#38382;?#25253;错的测试,因为函数开始支持这种功能了,这种做法和我们说以单元测试步步为营并不矛盾,如果新的算法导致旧的测试失败,我们要根据实?#26159;?#20917;,酌情决定是修改算法还是修改测试。
在getArea的第三个版本中,我们给函数添加了两个可选的?#38382;?shape和units,并且它们的顺序可以相互颠倒的。新的函数可以应付如下的情况:
% command line 
 >> getArea(10,22,'shape','square','units','m') %接受两对name-value pair
 ans =            %--name  value  --name   value
      area: 220
     shape: 'square'
     units: 'm'
 
 >> getArea(10,22,'units','m','shape','square')  % 变化了?#38382;?#30340;位置
 ans = 
      area: 220
     shape: 'square'
     units: 'm'
 
 
 >> getArea(10,22,'units','m')                   % 仅仅提供unit?#38382;?
 ans = 
      area: 220
     shape: 'rectangle'
     units: 'm'  
 
为其添加的新的测试点如下:
% testGetArea 
 function tests = testGetArea
   ...从略
  
  function testFourInputs(testCase) % 记录可以支持四个?#38382;?#30340;情况
     actStruct = getArea5(10,22,'shape','square','unit','m');
     expStruct = struct('area',220,'shape','square','units','m');
     testCase.verifyEqual(actStruct,expStruct,'structs not equal');
  
     actStruct = getArea5(10,22,'unit','m','shape','square');
     expStruct = struct('area',220,'shape','square','units','m');
     testCase.verifyEqual(actStruct,expStruct,'structs not equal');
  end
  
  
  function testThreeInputs(testCase) % 记录可以支持三个?#38382;?#30340;情况
     actStruct = getArea5(10,22,'units','m');
     expStruct = struct('area',220,'shape','rectangle','units','m');
     testCase.verifyEqual(actStruct,expStruct,'structs not equal');
  end
   
 
在testFourInputs中,我们从getArea函数那里先得到一个结构体,命名叫做actStruct(实际值) 然后准备了一个结构体叫做 expStruct (期望值),然后把用verifyEqual方法来作比较 在testThreeInputs中,我们调换的第三和第四个?#38382;?#30340;位置,确保结果依然是我们预期的。

测试的准备和清理工作: Tests Fixtures

本节介绍单元测试系统中另一个很重要的概念叫做Fixture。假设我们要给图形处理的一系列算法写测试,这些算法需要图像数据作为输入,所以在测试之前,我们需要先载入图像数据,按照上节的例子,单元测试看上去是这样的。
% 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 
 
可以观察到,在每个测试点的一开始,都有同样的准备工作,就是打开一个图像。在单元测试中,这叫做Test Fixture, 即每个测试的共同准备工作。如果这个测试函数中有很多这样的测试点,?#30475;?#37117;要重复的调用imread操作很麻?#22330;?#23545;于这样的准备工作,我们可以把它们放在一个叫做setup的局部函数中,该函数统一地在每个测试点的开始之前被调用。这样就不用在每个测试点中?#21450;?#25324;一个imread的调用了。新的测试看上去是这样的:
% 使用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  
 
在setup方法中,我们打开一个文件,并把数据动态地添加到testCase对象的TestData结构体上,在之后的每个局部测试点中,我们可以通过 testCase.TestData.img 来访?#25910;?#20010;数据。 setup中还可以放其他的准备工作,比如创建一个临时的文件夹放置临时的数据等待。对应的teardown函数中用来存放每个局部测试点运行完?#29616;?#21518;的清理工作,比如清除临时文件?#23567;? setup和teardown方法在每个局部测试点的开始和结束后运行,所以如果该主测试文件有两个测试点,那么setup和teardown各被运行了两次,流程如图所示:
Figure.4, setup和teardown方法在每个局部测试点的开始和结束后运行
如果还有一些准备和清理工作只需要开始和结束的时候各运行一次,那么可以把他们放到setupOnce和teardownOnce中去,比如我们要验证一些算法,而给该算法提供的数据来自数据库,在运行算法测试之前,要先连接数据库,在测试结束之后,要关闭和数据库的连接,这样的工作?#22836;?#21512;setupOnce和teardownOnce的范畴,如下所示:
% 使用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
 
setupOnce和teardownOnce方法仅仅在整个测试开始和结束时运行一次,流程如图 Figure.5 所示
Figure.5, setupOnce和teardownOnce方法仅仅在整个测试开始和结束是运行一次
setupOnce,teardownOnce和setup,teardown?#37096;?#20197;联合起来使用,如图Figure.6 所示:
Figure.6 setupOnce,teardownOnce和setup,teardown联合起来使用

验证方法: Types of Qualification

getArea函数的单元测试: 版本I 节中我们提到,如下的测试点中:
% 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  
 
?#38382;齮estCase是类 matlab.unittest.FunctionTestCase 的对象,由Framework提供,该类有很多成员验证方法可以提供给用户调用,比如前几节用到的verifyTrueverifyError ,这个两个验证方法最常见。全部的验证方法下表所示:
验证方法 验证 典型使用
verifyTrue 表达式值为真 testCase.verifyTrue(expr,msg)
verifyFalse 表达式值为假 testCase.verifyFalse(expr,msg)
verifyEqual 两个输入的表达式相同 testCase.verifyEqual(expr1,expr2,msg)
verifyNotEqual 两个输入的表达?#35762;?#21516; testCase.verifyNotEqual(expr1,expr2,msg)
verifySameHandle 两个handle指向同一个对象 testCase.verifySameHandle(h1,h2,msg)
verifyNotSameHanle 两个handle指向不同对象 testCase.verifyNotSameHandle(h1,h2,msg)
verifyReturnsTrue 函数句柄执行返回结果为真 testCase.verifyReturnsTrue(fh,msg)
verifyFail 无条件产生一个错误 testCase.verifyFail(msg)
verifyThat 表达式值满足?#31243;?#20214; testCase.verifyThat(5, IsEqualTo(5), '')
verifyGreatThan 大于 testCase.verifyGreaterThan(3,2)
verifyGreaterThanOrEqual 大于等于 testCase.verifyGreateThanOrEqual(3,2)
verifyLessThan 小于 testCase.verifyLessThan(2,3)
verifyLessThanOrEqual 小于等于 testCase.verifyLessThanOrEqual(2,3)
verifyClass 表达式的类型 testCase.verifyClass(value,className)
verifyInstanceOf 对象类型 testCase.verifyInstanceOf(derive,?Base)
verifyEmpty 表达式为空 testCase.verifyEmpty(expr,msg)
verifyNotEmpty 表达式非空 testCase.verifyNotEmpty(expr,msg)
verifySize 表达式尺寸 testCase.verifySize(expr,dims)
verifyLength 表达式长度 testCase.verifyLength(expr,len)
verifyNumElements 表达式中元素的总数 testCase.verifyNumElements(expr,value)
verifySubstring 表达式中含有字串 testCase.verifySubstring('thing','th')
verifyMatches 字串匹配 testCase.verifyMatches('Another', 'An')
verifyError 句柄的执行抛出指定错误 testCase.verifyError(fh,id,msg)
verifyWarning 句柄的执行抛出指定警告 testCase.verifyWarning(fh,id,msg)
verifyWarningFree 句柄的执行没有警告 testCase.verifyWarningFree(fh)
除了verify系列的函数,MATLAB单元测试还提供
  • assume系列
  • assert系列
  • fatalAssert系列
的验证函数,也就是说,上面每一个verify函数,都有一个对应的assume,assert和fatalAssert函数。比如除了verifyTrue,还有assumeTrue,assertTrue,fatalAssertTrue三个验证方法。
assume系列的验证方法一般用来验证一些测试是否满足某些先决条件,如果满足,测试继续,如果不满足,则过滤掉这个测试,但是不产生错误。比如下面的测试点,如果测试者的意图是:在Windows?#25945;?#19979;才执行,没有必要在其它?#25945;?#19979;执行
% tFoo.m 
 function tests = tFoo
     tests = functiontests(localfunctions);
 end
 
 function testSomething_PC(testCase)    
     testCase.assumeTrue(ispc,'only run in PC');   % 如果这个测试点在其它?#25945;?#36816;行,
                                                   % 则显示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.  
 
assert系列的验证方法也是用来验证一些测试是否满足某些先决条件,如果满足,测试继续,如果不满足,则过滤掉这个测试,并且产生错误。但是它不会影响其余的测试点。比如下面这个例子,testSomething测试点中,我们要求该测试的先决条件是数据库必须先被连接,如果没有连接,那么没有必要进行余下的测试,并且testA的测试结果显示失败。但是这个失败将不会影响testB测试点的运行
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.
 
最后,fatalAssert系列的验证方法,顾名思义,就是如果失败,立即停止结束所有的测试。如果还有未运行的测试点,则不再运行它们,例子从略。

测试方法论和以测试驱动开发(Test-Driven Development)

开发流程概述

在前节的基础?#24076;?#26412;节将抽象的讨论MATLAB常见的开发流程,引入用测试驱动开发的思想。先概述一下常见的开发工作流程。最简单也是最常见的工作流程是?#21512;?#29992;代码实现一个功能,然后在命令行测试该代码是否达到预期目的,如果达到了,则该函数放到更大的工程项目中去使用,然后不再去更新,如图所示:
Figure.8 最简单最常见的工作流程
如果比?#32454;?#26434;的功能,在写好的代码放入更大的工程项目之前,我们通常需要在命令行中反复的测试各个方面的功能, 方便起见,我们通常还会写一个专门测试的脚本,比如新的函数如果叫做op1, 通常习惯会写一个script1.m来一次性测试op1的所有的功能。测试完?#29616;?#21518;,把op1函数放入工程项目中,而该script1.m脚本,通常因为没有很好的管理方式,则难免遗忘在某个文件夹中,或遗忘在工程项目的最上层目录里面,最终被清理掉。
Figure.9 用脚本测试
本节我们将引入的工作流程是:开发一个复杂的功能,从开发最简单的部分开始,循序渐进的完成更复杂的需求,并且在此同时引入该功能配套的单元测试文件,测试和开发同步进行,测试和要测试的代码共生在同一个目录下。即使要测试的内容被加入的更大的项目之中,我们还是保留这个测试,单元测试本身也是工程项目中的一部分。
Figure.10 单元测试是工程项目的一部分
测试还是多人合作项目中不可缺少缺少的?#26041;凇?/mark>比如A和B共同开发一个项目两人分别负责该项目中的不同部分,他们的工作项目?#35272;?#30456;互调用,甚至有少量的重叠,即有可能要修改对方的代码。那么如何保证A在修改B的代码的时候不会破坏B已有的功能呢,这就要依靠B写的测试代码了。在A修改完代码之后,但在A提交代码到Repository之前,A必须在本地的工程项目中运行所有的测试,这些测试确保A不会意外的破坏B的代码的已有的功能,所以B的测试?#36130;?#21040;了保护自己代码的作用,因为它起到了对他人的?#38469;?#20316;用
Figure.11 提交之前必须运行所有的测试
前面我们提出一个问题:如下测试点里验证显而易见的getArea(10,22) == 220 真的有必要吗?
% testGetArea.m 
     ...
 function testTwoInputs(testCase)
     testCase.verifyTrue(getArea(10,22)==220,'!=220'); 
     ...
 end  
 ...  
 
再从另一角度看:有必要。因为单元测试其实是程序最好的文档。因为我们不可能给每一个函数都写文档,或者在函数里面都写清楚详细的注释。天长?#31449;?#20043;后,即使有注释也许因为遗忘而很难看懂。当我们要回忆一个函数,一个功能如何使用的时候,最快的办法不是去读它的实现代码或者注释,而是去查找工程项目中其它的地方是如何使用这个功能的。但是如果工程项目过于复杂,这也不会是一件容易的事情。如果有了这个函数的单元测试,因为这个单元测试是仅仅关于这一个功能的,那么我们会很容易就通过单元测试就可以了解这个函数的功能是什?#30784;?#25152;以getArea(10,22) == 220 不但是一个历史的记录,记录这个函数要实现的功能,还是该函数最好的?#24471;?#25991;档,为了让这个?#24471;?#25991;档?#38498;?#38405;读起来更加的清晰,我们还必要错误的提示信息写得更加详细一些,比如上面的测试点可可以这样改写
%  错误提示信息其实是getArea文档的一部分 
     ...
 function testTwoInputs(testCase)
     testCase.verifyTrue(getArea(10,22)==220,'given width and height, ...
                                              should return 10*22=220'); 
     ...
 end  
 ...  
 
前面所讨论的开发模式,测试总是作为主要功能的辅助,还有一种流行的开发模式,测试的地位和要测试的代码的地位是不相上下,这种测试和开发的工作流程,叫做用测试驱动(Test Driven Development),也值得我们了解一下。 我们先前的这些工作流程无一例外都是先写算法,然后补上测试代码;读者有没有想过可不可以先写测试,再写函数的实现呢。为什么要这样开发,这样开发有什么好处,我们将举例?#24471;鰲?

用测试驱动开发:Fibonacci例

假设一个教编程的老师给学生布置了一道MATLAB程序,要求写一个计算Fibonacci数列的函数。已知,Fibonacci函数定义如下:
F(n) = F(n-1) + F(n-2)
当n=1;2 时F(1) = F(2) = 1。
并且规定n=0时,F(0)=0。 要求除?#24605;?#31639;正确以外,还必须能正确的处理各种非法的输入,比如输入是非整数,负数或者字符串的情况。 所谓以测试驱动做开发就得到程序的需求之后,在这里即老师的作业要求,先写测试的代码,再写程序。比如根据老师的要求,很容易就写出该函数要满足的条件的一个清单
  • ? fibonacci(0)= 0
  • ? fibonacci(1)= 1
  • ? fibonacci(2)= 1
  • ? fibonacci(3)= 2 ; fibonacci(4)= 3
  • ? fibonacci(1.5) 报错
  • ? fibonacci(-1) 报错
  • ? fibonacci('a') 报错
根据这些条件,我们可?#38498;?#23481;易的写出两个测试点,一个是正面测试,一个负面测试
function tests = testFib(  )
     tests = functiontests(localfunctions);
 end
 
 function testValidInputs(testCase)
     % fibonacci function only accepts integer 
     testCase.verifyTrue(fibonacci(int8(0))  ==0, 'f(0) Error');
     testCase.verifyTrue(fibonacci(int16(1)) ==1, 'f(1) Error');
     testCase.verifyTrue(fibonacci(int32(2)) ==1, 'f(2) Error');
     testCase.verifyTrue(fibonacci(uint8(3)) ==2, 'f(3) Error');
     testCase.verifyTrue(fibonacci(uint16(4))==3, 'f(4) Error');
     testC
18

鲜花

握手

雷人

路过

鸡蛋

刚表态过的朋友 (18 人)

相关阅读

发表评论

最新评论

引用 蓝色的船 2018-10-27 17:17
我是来膜拜大神的
引用 蓝色de闪电 2018-6-5 14:15
不明觉厉
引用 SongCW 2017-12-7 21:29
用仿真测试的方法Mtest,基于需求开发,方便完整
引用 ArthurB 2017-11-3 00:05
作者写的内容非常丰富,受益匪浅
引用 胖柯基爱代码 2017-8-9 14:32
求问,如果我想测试的函数是没有输出的,比如说 function errorreport(data,xls). 是不是就不适合做单元测试。
我的方程实现的是,测试数据是否超过规定区域,超过?#21496;?#25226;错误记录下来,将错误自动发邮件给负责人。所 ...

查看全部评论(1)

MATLAB table数据结构 首篇

MATLAB table是R2013b中引入的一个新的数据结构,虽然不像常用的基本数据类型为人熟悉,但是在编程中非常有用。它用来存放表状类型的数据结构,并且支持常见的表和表之间的运算。 ... ... ... ... ... ... ...

MATLAB?#25104;?#34920;数据结构

除了常用的基本数据类型,MATLAB还有很多其它实用的数据类型不为人熟悉,例如?#25104;?#34920;containers.Map,常用的MATLAB高级数据类型。它最大的特点使用方便的索引方式进行快速的查找。本篇介绍为什么需要这种数据结构,以 ...

MATLAB table数据结构 再篇

MATLAB table是R2013b中引入的一个新的数据结构,虽然不像常用的基本数据类型为人熟悉,但是在编程中非常有用。它用来存放表状类型的数据结构,并且支持常见的表和表之间的运算。 ... ... ... ... ... ... ...

对函数的输入进行检查和解析

在工程计算中,如果函数的输入有错误,我们总是希望能尽早捕捉到这些错误,并及时终止程序。在MATLAB 中,可以使用validateattributes,validatestring和inputParser 类来对输入进行检查。它们提供全面的检查功能和清 ...

MATLAB单元测试

本篇是把现代软件工程思想应用到MATLAB工程开发中的精髓,希望高级MATLAB用户仔细研读。作者用实际的例子解释在开发和逐渐改进算法的时候,如何保证程序已有的功能没有收到影响,步步为营,让算法开发和测试系统的建 ...

MATLAB?#25104;?#34920;数据结构

除了常用的基本数据类型,MATLAB还有很多其它实用的数据类型不为人熟悉,例如?#25104;?#34920;containers.Map,常用的MATLAB高级数据类型。它最大的特点使用方便的索引方式进行快速的查找。本篇介绍为什么需要这种数据结构,以 ...

对函数的输入进行检查和解析

在工程计算中,如果函数的输入有错误,我们总是希望能尽早捕捉到这些错误,并及时终止程序。在MATLAB 中,可以使用validateattributes,validatestring和inputParser 类来对输入进行检查。它们提供全面的检查功能和清 ...

MATLAB性能测试框架

MATLAB Performance Test 框架是Mathworks 在MATLAB R2016a 中推出的?个新的框架,该框架?来获得代码性能在统计意义上的数据,还可以?来?较算法的性能,并且给出详细完整的报告。 ... ... ... ... ... ... ... ...
关闭

站长推荐上一条 /3 下一条

返回顶部
赌场送彩金35 皇家社会队标 苏州西交利物浦大学 西班牙人节日饮食 全北现代超清壁纸 ac米兰vs卡利亚里 36选7走势图表 威尼斯恋人 阿尔艾因世俱杯资格 巴塞罗那椅 亚眠大学世界排名