PHPUnit 單元測試都不會的 PHPer 沒法寫出高品質的程式碼
- 2019 年 11 月 11 日
- 筆記
什麼是單元測試
單元測試(unit testing),是指對軟體中的最小可測試單元進行檢查和驗證。單元就是人為規定的最小的被測功能模組。單元測試是在軟體開發過程中要進行的最低級別的測試活動,軟體的獨立單元將在與程式的其他部分相隔離的情況下進行測試。
在php裡邊,最小單元可以指一個函數、或者類,需要驗證的就是每個函數,每個類的功能與我們預想的一致。
單元測試有什麼意義
- 可以 減少一些細節錯誤的發生 ,比如應該報錯的情況沒有報錯,入參、結果是否與需求對應上等。
- 便於日後修改維護 ,實際工作中存在不少情況是做出了一版功能,但是上線後需要對裡邊的細節進行調整,有單元測試的話改起來會更加放心,並且完善單元測試的過程也是進一步理解需求的過程。
- 更容易 發現平時無法走到的異常分支 ,而這個分支的處理邏輯可能人工測試需要經歷很多步驟才能走到,省時間
最近在工作中也嘗試著為開發中的功能寫單元測試,切實意識到了單元測試的好處,需求裡邊有一個比較複雜的時間推算邏輯,最開始自認為各種情況考慮周全然後劈里啪啦寫完,不過運行了事先寫好的單元測試之後,依舊發現了幾個隱藏比較深問題( 再自信也得過一遍測試啊 )。
修復問題後提測的過程中遇到了需求變更,不少關鍵程式碼需要改動,正常這種情況自測的話會很費勁,因為需要資料庫找各種各樣情況的數據去跑介面,然後數據對不上改完還得重新跑介面自測。但是這次先把單元測試規定正確後,放心大膽的按照自己的想法改造程式碼,經歷了 改程式碼 > 跑測試 > 改程式碼 > 跑測試的循環後,快速交付了需求。
單元測試的一些概念
之前也接觸過php、python、JS之類的語言,對這些語言的單元測試也有一定了解,下邊先看一下單元測試中通用的一些概念。
斷言
想要更加細緻的了解斷言的話,這裡推薦一篇部落格:https://www.jianshu.com/p/9b8c88deed6a
在軟體測試特別是在單元測試時,必用的一個功能就是「斷言」(Assert),顧名思義,編寫程式時,常會做出一定的假設,那斷言就是用來捕獲假設的異常。
下邊舉個栗子:
一個簡單的函數 add() 擁有兩個參數,功能是返回兩個參數的和,當我需要驗證這個函數的正確性的時候就需要模擬兩個入參並 判斷函數的返回值是否為兩個入參之和 ,判斷返回值是否準確這個過程即為斷言。
function add($a, $b) { return $a + $b; }
基境
每一個單元測試方法都是一個獨立的個體,每次單元測試完畢,需要將數據恢復到正確的狀態中,不至於被其他測試方法給影響。
在phpunit中,給出的 TestCase 基類即有兩個方法, setUp
和 setDown
分別用於為每個單元測試創建測試對象和清理測試對象
數據供給器
對同一類情況進行測試,通常可以用數據供給器傳入不同入參和相應的預期返回值。
測試方法可以接受任意參數。這些參數由數據供給器方法提供。在phpunit中使用 @dataProvider 標註來指定使用哪個數據供給器方法。
php如何集成單元測試
PHP的單元測試依賴一個測試框架:phpunit(官方文檔:https://phpunit.readthedocs.io/zh_CN/latest/index.html )
如何安裝
可以通過phar的方式安裝
$ wget https://phar.phpunit.de/phpunit-7.0.phar $ chmod +x phpunit-7.0.phar $ sudo mv phpunit-7.0.phar /usr/local/bin/phpunit $ phpunit --version
也可以通過 composer 進行統一管理
$ composer require phpunit/phpunit
在 composer.json
中會出現如下依賴
{ "require": { "phpunit/phpunit": "^7.5" } }
並且會出現 vendor/bin/phpunit
文件,直接運行即可
如何編寫單元測試
所有類需要繼承 PHPUnitFrameworkTestCase
, setUp
函數用於初始化測試對象, setDown
函數用於清理測試對象,更多規範
更具體寫法可以查看底部的 舉個栗子
phpunit常用斷言方法
更多斷言方法詳見 phpunit 官方文檔,基本都能顧名思義。
斷言函數 |
作用 |
示例 |
---|---|---|
assertEquals($except, $value) |
斷言相等 |
$this->assertEquals(2, 1 + 1) |
assertEmpty($value) |
斷言為空 |
$this->assertEmpty([]) |
assertNotEmpty($value) |
斷言不為空 |
$this->assertNotEmpty([1, 2, 3]) |
assertTrue($value) |
斷言為真 |
$this->assertTrue(1 === 1) |
assertFalse($value) |
斷言為假 |
$this->assertFalse(1 === 『1』) |
expectException(Exception $e) |
斷言本次測試會出現特定異常 |
$this->expectException(Exception::class); throw new Exception(『測試』, 1002); |
expectExceptionCode($code) |
斷言異常狀態碼 |
$this->expectExceptionCode(1002); throw new Exception(『測試』, 1002); |
expectExceptionMessage($msg) |
斷言異常資訊 |
$this->expectExceptionMessage(『測試』); throw new Exception(『測試』, 1002); |
expectOutputString($msg) |
斷言輸出 |
$this->expectOutputString(『Hello』);echo 「Hello」; |
getActualOutput() |
獲取實際輸出 |
|
如何運行單元測試
# 運行全部測試 phpunit # 運行某個分組的單元測試 phpunit --group GroupA # 運行指定測試類的所有測試用例 phpunit tests/xxxxTest.php # 運行所有測試類中滿足filter條件的方法 phpunit --filter xxxFunc # 運行某個測試類中滿足filter條件的
phpunit.xml 是什麼
phpunit.xml 是一個XML格式的配置文件,能夠配置單元測試中的一些默認行為,比如環境變數、啟動文件、日誌記錄等,官方文檔如下 https://phpunit.readthedocs.io/zh_CN/latest/configuration.html
一個樣例配置如下所示:
<?xml version="1.0" encoding="UTF-8"?> <!--phpunit標籤是配置中的核心,這裡配置了啟動文件 "./tests/bootstrap.php"--> <phpunit backupGlobals="false" backupStaticAttributes="false" bootstrap="./tests/bootstrap.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false"> <!--測試套件:非常多的測試用例放在一起即可成為測試套件,執行時會掃描包含的所有 *Test.php文件--> <testsuites> <testsuite name="Unit"> <directory suffix="Test.php">./tests/Unit</directory> </testsuite> </testsuites> <filter> <!--這裡配置了白名單,只有這裡邊的程式碼會被統計覆蓋率--> <whitelist processUncoveredFilesFromWhitelist="true"> <directory suffix=".php">./app/library</directory> <directory suffix=".php">./app/models</directory> </whitelist> </filter> <!--這裡配置了PHP的環境變數--> <php> <server name="APP_ENV" value="product"/> <server name="BCRYPT_ROUNDS" value="4"/> <server name="CACHE_DRIVER" value="array"/> <server name="MAIL_DRIVER" value="array"/> <server name="QUEUE_CONNECTION" value="sync"/> <server name="SESSION_DRIVER" value="array"/> </php> <!--這裡是日誌記錄,把覆蓋率資訊保存到 ./tests/codeCoverage--> <logging> <log type="coverage-html" target="./tests/codeCoverage"/> </logging> </phpunit>
如何查看程式碼覆蓋率
執行 phpunit 之後,根據 <logging>
中的配置,會自動生成程式碼覆蓋率資訊至 ./tests/codeCoverage/
,打開其中 index.html
即可查看覆蓋率資訊。
舉個栗子
以一個簡單的斐波拉契數列計算函數為例
斐波那契數列由0和1開始,之後的斐波那契係數就是由之前的兩數相加而得出。
輸入輸出分析
根據函數特點,我們可以通過驗證已知情況和特殊情況的方式去驗證,經過分析結果如下
正常輸入的已知情況:
入參 |
預期返回 |
描述 |
---|---|---|
0 |
0 |
規則 |
1 |
1 |
規則 |
2 |
1 |
0 + 1 = 1 |
3 |
2 |
1 + 1 = 2 |
4 |
3 |
1 + 2 = 3 |
5 |
5 |
2 + 3 = 5 |
6 |
8 |
3 + 5 = 8 |
… |
… |
… |
12 |
144 |
55 + 89 = 144 |
異常輸入的情況處理
處理為0,或者拋出異常均可
入參 |
預期返回 |
描述 |
---|---|---|
-1 |
0 |
非正常輸入處理為0 |
」 |
0 |
非正常輸入處理為0 |
1.1 |
0 |
非正常輸入處理為0 |
『文字』 |
0 |
非正常輸入處理為0 |
編寫測試類
tests/FunctionTest.php
use PHPUnitFrameworkTestCase; class FunctionsTest extends TestCase { /** * @dataProvider fibon_normal_provider * @param $input * @param $except */ public function test_fibon_normal($input, $except) { $this->assertEquals($except, fibon($input)); } public function fibon_normal_provider() { return [ [0, 0], [1, 1], [2, 1], [3, 2], [4, 3], [5, 5], [6, 8], [12, 144], ]; } /** * @dataProvider fibon_error_provider * @param $input * @param $except */ public function test_fibon_error($input, $except) { $this->assertEquals($except, fibon($input)); } public function fibon_error_provider() { return [ [-1, 0], [1.1, 0], ['', 0], ['文字', 0], ]; } }
函數功能實現
(PS:此法效率很差,約莫是 O(n^2) 的複雜度,僅用於此處演示)
functions.php
function fibon($a) { if (!is_int($a)) { return 0; } if ($a <= 0) { return 0; } elseif ($a == 1) { return 1; } else { return fibon($a - 1) + fibon($a - 2); } }
運行結果
vagrant@homestead:~/code/bmtrip/platoReceivable$ phpunit tests/FunctionsTest.php --filter test_fibon PHPUnit 7.5.14 by Sebastian Bergmann and contributors. ............ 12 / 12 (100%) Time: 5.77 seconds, Memory: 26.00 MB OK (12 tests, 12 assertions) Generating code coverage report in HTML format ... done