奇迹觉醒套装怎么获得:MATLAB單元測試

奇迹觉醒女神之光 www.mhotr.icu 2019-3-3 09:33| 發布者: ilovematlab| 查看: 31961| 評論: 1|原作者: oopmatlab

摘要: 本篇是把現代軟件工程思想應用到MATLAB工程開發中的精髓,希望高級MATLAB用戶仔細研讀。作者用實際的例子解釋在開發和逐漸改進算法的時候,如何保證程序已有的功能沒有收到影響,步步為營,讓算法開發和測試系統的建 ...

文章PDF瀏覽下載鏈接

目錄:

inputParser章節中,我們通過不斷改進getArea函數對輸入參數的處理方法,引入這樣一個觀點:一個可靠的科學工程計算項目必須有一套測試系統,才能防止開發的過程中算法退化,工程項目的推進必須在算法開發和算法測試之間不斷迭代完。在inputParser章節的最后,還根據直覺提出了一個測試系統所應該有的基本功能。在本章中,我們將學習MATLAB提供的測試解決方案:MATLAB單元測試(MATLAB Unit Test)。

基于函數的(Function-Based)單元測試的構造

MATLAB基于函數的單元測試構造很簡單,如圖Figure 1所示:用戶通過一個主測試函數和若干局部測試函數(也叫做測試點) (Local Function)來組織各個測試。而測試的運行則交給MATLAB的單元測試架構(以下簡稱Framework)去完成。
Figure.1 單元測試Framework和測試函數
主測試函數和局部測試函數看上去和普通的MATLAB函數沒有區別,其結構如圖Figure 2 所示,只是命名上有一些規定而已,這些特殊的規定是為了Framework可以和測試函數契合而規定的。
Figure.2 簡單的主測試函數和若干局部的測試函數構成的一個單元測試
命名規則如下:
  • 主函數的名稱由用戶任意指定,和其他的MATLAB函數文件一樣,該文件的名稱需要和函數的名稱的相同. (如果主函數的名稱是 testmainfunc , 該文件名稱則是testmainfunc.m )。
  • 在主函數中,必須調用一個叫做 functiontests 的函數,搜集該函數中的所有局部函數, 產生一個包含這些局部函數的函數局部的測試矩陣并返回給Framework
如下所示:
% 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 
  ...
 
其中testCase由單元測試Framework提供,即Framework將自動的調用該函數,并且提供testCase參數。 按照規定,要運行單元測試中的所有測試,必須調用runtests函數
% command line 
   >> runtests('testmainfunc.m') 
 
下面用我們用基于函數的單元測試來給getArea函數的構造其單元測試。

getArea函數的單元測試: 版本 I

首先給主測試文件起個名字叫做testGetArea,該名字是任意的,為了便于理解名字里面通常包含test,并包含要測試的主要函數的名字:
% testGetArea.m 
 function tests = testGetArea
   tests = functiontests(localfunctions);
 end 
 
在該主函數中,localfunctions將搜集所有的局部函數,構造函數句柄數組并返回測試矩陣。這里自然會有一個問題,這個tests句柄數組將返回給誰,這就要了解Framework是如何和測試相互作用的。 如圖Figure.3 所示,整個測試從 runtests('testmainfunc.m') 命令開始, 命令函數,Framework將首先調用testGetArea的主函數,得到所有的局部函數的函數句柄,如空心箭頭線段所示,然后Framework再負責調用每一個測試局部函數,并且把testCase當做參數提供給每個局部函數,如虛線線段所示。我們可以把Framework想象成一個流水線,用戶只需要通過 runtests('testmainfunc.m') 把"testmainfunc.m"放到流水線上并且打開開關"就可以了。它是MATLAB的類 matlab.unittest.FunctionTestCase 的對象。
Figure.3 單元測試Framework和測試函數的相互作用
返回的testCase是類 matlab.unittest.FunctionTestCase 的對象,有很多成員驗證方法可以提供給用戶調用,我們的第一版的getArea函數如下, 要求函數接受兩個參數,并且都是數值類型:
% 第一版的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函數在接受兩個參數的時候,能給出正確的答案
% 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對象所支持的方法,它用來驗證其第一個參數,作為一個表達式,是否為真。verifyTrue的第二個參數接受字符串,在測試失敗時提供診斷提示。 一個很常見的問題是: 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函數中的第二個參數所提供的診斷信息。
% 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
 
我們可以利用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成員函數,它的第一個參數是函數句柄,即要執行的語言(會出錯的語句),第二個參數是要驗證的MATLAB錯誤的Error ID, 就是我們前面用lasterr函數得到的信息。verifyError內部還有try和catch,可以運行函數句柄,捕捉到錯誤,并且把Error ID和第二個參數做比較。 再舉一個例子,我們先在getArea函數中規定所有的輸入必須是數值類型,所以如果輸入的是字符串,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添加了可以處理單個參數的能力,并且把inputParser和validateAttributes聯合起來使用。新的函數在原來的基礎上可以應付如下的新的情況
% 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.  
 
在開發完這第二個版本的函數之后,我們首先運行了一下已經有的testGetArea測試,發現之前添加的一個測試點,驗證函數在接受一個參數時會報錯的情況已不再適用,因為我們已經開始支持單參數的功能了,所以要去掉它,隨著程序算法的不斷開發,修改或刪除已有的測試是很常見的
% testGetArea.m 
   ...
  % testCase.verifyError(@()getArea1(10),'MATLAB:minrhs');  需要去掉這個測試
   ...
 
去掉不再適用的測試之后,我們繼續給單元測試添加新的測試點,首先添加一個Postive 測試點,確保getArea函數接受單一參數計算結果正確
% 確保單一參數計算正確 
 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')
 ...  
 
每次運行這個命令,會運行之前所有的測試點和新的測試點,這也就保證了對新添加的算法沒有破壞以前有的功能。我們前面問了一個問題: 驗證getArea(10,22) == 220 真的有必要嗎。
其必要性之一,也是單元測試功能之一:即這個驗證其實是對getArea能正確處理兩個參數的能力的一個歷史記錄。因為我們在不停的算法開發中,很難保證不會偶然破壞一些以前的什么功能,但是只要有這條測試在,無論我們對getArea函數做怎樣翻天復地的修改,只要一運行測試,都會驗證這條歷史記錄,確保我們沒有損壞已經有的功能,換句話說,新的函數是向后兼容的。對于一個科學工程計算系統來說,一個函數會被用在很多不同的地方,向后兼容讓我們放心的繼續開發新的功能,而不用擔心是否要去檢查所有其它使用該函數的地方。所以從這個角度說:單元測試是算法開發的堡壘,算法的開發應該以單元測試來步步為營,在確保算法沒有退化的基礎上開發新的內容。話說回來,為了讓這個版本的getArea能夠順利運行,我們確實去掉了一個對單一參數報錯的測試,因為函數開始支持這種功能了,這種做法和我們說以單元測試步步為營并不矛盾,如果新的算法導致舊的測試失敗,我們要根據實際情況,酌情決定是修改算法還是修改測試。
在getArea的第三個版本中,我們給函數添加了兩個可選的參數: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')  % 變化了參數的位置
 ans = 
      area: 220
     shape: 'square'
     units: 'm'
 
 
 >> getArea(10,22,'units','m')                   % 僅僅提供unit參數
 ans = 
      area: 220
     shape: 'rectangle'
     units: 'm'  
 
為其添加的新的測試點如下:
% testGetArea 
 function tests = testGetArea
   ...從略
  
  function testFourInputs(testCase) % 記錄可以支持四個參數的情況
     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) % 記錄可以支持三個參數的情況
     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中,我們調換的第三和第四個參數的位置,確保結果依然是我們預期的。

測試的準備和清理工作: 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, 即每個測試的共同準備工作。如果這個測試函數中有很多這樣的測試點,每次都要重復的調用imread操作很麻煩。對于這樣的準備工作,我們可以把它們放在一個叫做setup的局部函數中,該函數統一地在每個測試點的開始之前被調用。這樣就不用在每個測試點中都包括一個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 來訪問這個數據。 setup中還可以放其他的準備工作,比如創建一個臨時的文件夾放置臨時的數據等待。對應的teardown函數中用來存放每個局部測試點運行完畢之后的清理工作,比如清除臨時文件夾。 setup和teardown方法在每個局部測試點的開始和結束后運行,所以如果該主測試文件有兩個測試點,那么setup和teardown各被運行了兩次,流程如圖所示:
Figure.4, setup和teardown方法在每個局部測試點的開始和結束后運行
如果還有一些準備和清理工作只需要開始和結束的時候各運行一次,那么可以把他們放到setupOnce和teardownOnce中去,比如我們要驗證一些算法,而給該算法提供的數據來自數據庫,在運行算法測試之前,要先連接數據庫,在測試結束之后,要關閉和數據庫的連接,這樣的工作就符合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也可以聯合起來使用,如圖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  
 
參數testCase是類 matlab.unittest.FunctionTestCase 的對象,由Framework提供,該類有很多成員驗證方法可以提供給用戶調用,比如前幾節用到的verifyTrueverifyError ,這個兩個驗證方法最常見。全部的驗證方法下表所示:
驗證方法 驗證 典型使用
verifyTrue 表達式值為真 testCase.verifyTrue(expr,msg)
verifyFalse 表達式值為假 testCase.verifyFalse(expr,msg)
verifyEqual 兩個輸入的表達式相同 testCase.verifyEqual(expr1,expr2,msg)
verifyNotEqual 兩個輸入的表達式不同 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 表達式值滿足某條件 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平臺下才執行,沒有必要在其它平臺下執行
% 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.  
 
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)

開發流程概述

在前節的基礎上,本節將抽象的討論MATLAB常見的開發流程,引入用測試驅動開發的思想。先概述一下常見的開發工作流程。最簡單也是最常見的工作流程是:先用代碼實現一個功能,然后在命令行測試該代碼是否達到預期目的,如果達到了,則該函數放到更大的工程項目中去使用,然后不再去更新,如圖所示:
Figure.8 最簡單最常見的工作流程
如果比較復雜的功能,在寫好的代碼放入更大的工程項目之前,我們通常需要在命令行中反復的測試各個方面的功能, 方便起見,我們通?;夠嶁匆桓鱟挪饈緣慕瘧?,比如新的函數如果叫做op1, 通常習慣會寫一個script1.m來一次性測試op1的所有的功能。測試完畢之后,把op1函數放入工程項目中,而該script1.m腳本,通常因為沒有很好的管理方式,則難免遺忘在某個文件夾中,或遺忘在工程項目的最上層目錄里面,最終被清理掉。
Figure.9 用腳本測試
本節我們將引入的工作流程是:開發一個復雜的功能,從開發最簡單的部分開始,循序漸進的完成更復雜的需求,并且在此同時引入該功能配套的單元測試文件,測試和開發同步進行,測試和要測試的代碼共生在同一個目錄下。即使要測試的內容被加入的更大的項目之中,我們還是保留這個測試,單元測試本身也是工程項目中的一部分。
Figure.10 單元測試是工程項目的一部分
測試還是多人合作項目中不可缺少缺少的環節。比如A和B共同開發一個項目兩人分別負責該項目中的不同部分,他們的工作項目依賴相互調用,甚至有少量的重疊,即有可能要修改對方的代碼。那么如何保證A在修改B的代碼的時候不會破壞B已有的功能呢,這就要依靠B寫的測試代碼了。在A修改完代碼之后,但在A提交代碼到Repository之前,A必須在本地的工程項目中運行所有的測試,這些測試確保A不會意外的破壞B的代碼的已有的功能,所以B的測試也起到了?;ぷ約捍氳淖饔?,因為它起到了對他人的約束作用。
Figure.11 提交之前必須運行所有的測試
前面我們提出一個問題:如下測試點里驗證顯而易見的getArea(10,22) == 220 真的有必要嗎?
% testGetArea.m 
     ...
 function testTwoInputs(testCase)
     testCase.verifyTrue(getArea(10,22)==220,'!=220'); 
     ...
 end  
 ...  
 
再從另一角度看:有必要。因為單元測試其實是程序最好的文檔。因為我們不可能給每一個函數都寫文檔,或者在函數里面都寫清楚詳細的注釋。天長日久之后,即使有注釋也許因為遺忘而很難看懂。當我們要回憶一個函數,一個功能如何使用的時候,最快的辦法不是去讀它的實現代碼或者注釋,而是去查找工程項目中其它的地方是如何使用這個功能的。但是如果工程項目過于復雜,這也不會是一件容易的事情。如果有了這個函數的單元測試,因為這個單元測試是僅僅關于這一個功能的,那么我們會很容易就通過單元測試就可以了解這個函數的功能是什么。所以getArea(10,22) == 220 不但是一個歷史的記錄,記錄這個函數要實現的功能,還是該函數最好的說明文檔,為了讓這個說明文檔以后閱讀起來更加的清晰,我們還必要錯誤的提示信息寫得更加詳細一些,比如上面的測試點可可以這樣改寫
%  錯誤提示信息其實是getArea文檔的一部分 
     ...
 function testTwoInputs(testCase)
     testCase.verifyTrue(getArea(10,22)==220,'given width and height, ...
                                              should return 10*22=220'); 
     ...
 end  
 ...  
 
前面所討論的開發模式,測試總是作為主要功能的輔助,還有一種流行的開發模式,測試的地位和要測試的代碼的地位是不相上下,這種測試和開發的工作流程,叫做用測試驅動(Test Driven Development),也值得我們了解一下。 我們先前的這些工作流程無一例外都是先寫算法,然后補上測試代碼;讀者有沒有想過可不可以先寫測試,再寫函數的實現呢。為什么要這樣開發,這樣開發有什么好處,我們將舉例說明。

用測試驅動開發: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。 要求除了計算正確以外,還必須能正確的處理各種非法的輸入,比如輸入是非整數,負數或者字符串的情況。 所謂以測試驅動做開發就得到程序的需求之后,在這里即老師的作業要求,先寫測試的代碼,再寫程序。比如根據老師的要求,很容易就寫出該函數要滿足的條件的一個清單
  • ? fibonacci(0)= 0
  • ? fibonacci(1)= 1
  • ? fibonacci(2)= 1
  • ? fibonacci(3)= 2 ; fibonacci(4)= 3
  • ? fibonacci(1.5) 報錯
  • ? fibonacci(-1) 報錯
  • ? fibonacci('a') 報錯
根據這些條件,我們可以很容易的寫出兩個測試點,一個是正面測試,一個負面測試
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

相關閱讀

發表評論

最新評論

引用 cliffzq 2019-7-13 16:06
學習了,多謝
引用 藍色的船 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). 是不是就不適合做單元測試。
我的方程實現的是,測試數據是否超過規定區域,超過了就把錯誤記錄下來,將錯誤自動發郵件給負責人。所 ...

查看全部評論(1)

MATLAB table數據結構 首篇

MATLAB table是R2013b中引入的一個新的數據結構,雖然不像常用的基本數據類型為人熟悉,但是在編程中非常有用。它用來存放表狀類型的數據結構,并且支持常見的表和表之間的運算。 ... ... ... ... ... ... ...

MATLAB映射表數據結構

除了常用的基本數據類型,MATLAB還有很多其它實用的數據類型不為人熟悉,例如映射表containers.Map,常用的MATLAB高級數據類型。它最大的特點使用方便的索引方式進行快速的查找。本篇介紹為什么需要這種數據結構,以 ...

MATLAB table數據結構 再篇

MATLAB table是R2013b中引入的一個新的數據結構,雖然不像常用的基本數據類型為人熟悉,但是在編程中非常有用。它用來存放表狀類型的數據結構,并且支持常見的表和表之間的運算。 ... ... ... ... ... ... ...

MATLAB單元測試

本篇是把現代軟件工程思想應用到MATLAB工程開發中的精髓,希望高級MATLAB用戶仔細研讀。作者用實際的例子解釋在開發和逐漸改進算法的時候,如何保證程序已有的功能沒有收到影響,步步為營,讓算法開發和測試系統的建 ...

對函數的輸入進行檢查和解析

在工程計算中,如果函數的輸入有錯誤,我們總是希望能盡早捕捉到這些錯誤,并及時終止程序。在MATLAB 中,可以使用validateattributes,validatestring和inputParser 類來對輸入進行檢查。它們提供全面的檢查功能和清 ...

MATLAB映射表數據結構

除了常用的基本數據類型,MATLAB還有很多其它實用的數據類型不為人熟悉,例如映射表containers.Map,常用的MATLAB高級數據類型。它最大的特點使用方便的索引方式進行快速的查找。本篇介紹為什么需要這種數據結構,以 ...

對函數的輸入進行檢查和解析

在工程計算中,如果函數的輸入有錯誤,我們總是希望能盡早捕捉到這些錯誤,并及時終止程序。在MATLAB 中,可以使用validateattributes,validatestring和inputParser 類來對輸入進行檢查。它們提供全面的檢查功能和清 ...

MATLAB性能測試框架

MATLAB Performance Test 框架是Mathworks 在MATLAB R2016a 中推出的?個新的框架,該框架?來獲得代碼性能在統計意義上的數據,還可以?來?較算法的性能,并且給出詳細完整的報告。 ... ... ... ... ... ... ... ...
關閉

站長推薦上一條 /3 下一條

返回頂部