本文首發於 https://jaychen.cc/article/34
作者 Jaychen
朋友,你聽說過安。。。不是,寫過單元測試嗎。
單元測試是開發過程中必不可少的一環,一個專案有良好的單元測試程式碼,重構的勇氣都大很多。這次寫一篇小文來介紹一下 PHP 的單元測試工具 PHPUnit 的使用。
PHPUnit 的使用並不難,這篇文章主要還是充當一個引子,介紹基本概念和使用,有了這篇文章的基礎之後,去看官網的文件就會更加順風順水。
安裝
安裝 PHPUnit 的方式很簡單,使用 composer 可以一行程式碼就可以安裝。
composer require --dev phpunit/phpunit
複製程式碼
安裝之後,在 vendor/bin
目錄下有一個 phpunit 的可執行檔案,這個就是 phpunit 本體了。假設我們專案的目錄結構如下:
➜ phpunit tree .
├── controller
├── model
├── service
├── test
└── vendor
├── composer.json
複製程式碼
其中我們的單元測試程式碼都放在 test 目錄下。使用 composer 來為我們解決 autoload 的問題。
{
"autoload": {
"psr-4": {
"Controller\\": "controller/",
"Model\\": "model/",
"Service\\": "service/",
"Test\\": "test/",
}
},
}
複製程式碼
如果你還不懂 composer 自動載入的使用,可以參考這篇文章。最後執行 composer dumpautoload -o
讓自動載入生效。
到這裡我們的安裝就算結束了。如果你使用 phpstorm 進行開發,那麼你需要進行如下的配置:
這裡指明瞭從哪裡載入 PHPUnit,由於我們使用 composer 安裝,所以,這裡的檔案選擇 composer 生成的 autoload.php 檔案即可。
使用
好了,假設我們現在進行開發,在 service 目錄中新增了一個 CalculateService
的檔案,並且編寫了一個 abs 的函式。
namespace Service;
class CalculateService
{
public function abs($num)
{
return abs($num);
}
}
複製程式碼
現在我們對 abs
函式進行單元測試,PHPUnit 規定了一個測試類必須遵守如下的規定:
- 單元測試類名必須以 Test 結尾,必須繼承
\PHPUnit\Framework\TestCase
基類。 - 每個測試函式必須以 test 開頭。
上面的規定是必須遵守的,如果程式碼沒有遵守規定 PHPUnit 不會把他當做單元測試程式碼。除了以上的兩條,還有一些良好的編碼習慣可以參考:
- 單元測試程式碼都放在 test 目錄下。
- 每個單元測試類以被測試的類名開頭。例如被測試類為
CalculateService
,那麼單元測試類應該為CalculateServiceTest
。 - 每個單元測試函式應該為被測試函式名結尾。例如被測試函式為
abs
,那麼單元測試函式應該為testAbs
。
根據上面的規範,編寫單元測試程式碼
class UserServiceTest extends \PHPUnit\Framework\TestCase
{
public function testAbs()
{
$userService = new \Service\CalculateService();
$this->assertEquals(4, $userService->abs(4));
}
}
複製程式碼
在上面的測試程式碼中,呼叫了我們要測試的函式 abs
,然後斷言 $userService->abs(4)
的結果為 4。在 phpstorm 中直接在 testAbs
函式處右鍵選擇 run UserServiceTest
執行:
發現在控制檯會輸出如下內容
Time: 17 ms, Memory: 4.00MB
OK (1 test, 1 assertion)
複製程式碼
表明 abs
通過了 $userService->abs(4) == 4
的測試用例。這裡注意一點,這裡並不表明 abs
函式已經通過測試,一個良好的測試應該包含多個測試用例來覆蓋儘可能多的可能性。
現在 PHPUnit 基本的單元測試已經執行成功了,在 PHPUnit 的文件中,有更多關於測試的用法。由於 PHPUnit 的用法過多,這裡不能一一說明,這裡提一些其他用法。
-
PHPUnit 提供了
@test
的註解,如果一個測試函式新增了@test
註解,那麼測試函式名字就不必以 test 開頭。 -
\PHPUnit\Framework\TestCase
有一個setUp
函式,如果自己編寫的測試類重寫了這個函式,那麼每次在開始執行測試函式之前,會先執行setUp
進行測試之前的初始化。同樣,也有一個tearDown
的函式,如果重寫,那麼在測試函式執行完畢之後呼叫tearDown
函式。 -
.... 更多的內容需參考 PHPUnit 的文件。
phpunit.xml 檔案
在上面的例子中,我們使用 phpstorm 逐個執行測試函式,但是如果我們需要一次性執行所有的單元測試,那麼我們可以編寫 phpunit.xml 檔案來實現。
給出一個 phpunit.xml 的編寫例子來講解 phpunit.xml 的作用
<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
<testsuites>
<testsuite>
<directory>test</directory>
</testsuite>
</testsuites>
</phpunit>
複製程式碼
這裡 <directory>test</directory>
指定了測試程式碼都放在 test 目錄下,在 phpstorm 下右鍵點選 phpunit.xml 檔案選擇 Run phpunit.xml
,phpunit 就會到 test 目錄下查詢所有單元測試並逐個執行。
除了使用 phpunit.xml 來一次性執行所有的單元測試,還可以在 phpunit.xml 中配置單元測試結果的輸出日誌。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
.....
<logging>
<log type="testdox-html" target="tmp/log.html"/>
</logging>
</phpunit>
複製程式碼
此時在執行 phpunit.xml 檔案,就會在專案目錄下生成一個 tmp/log.html
檔案,這個檔案記錄了所有單元測試的結果。
當然,,,更多 phpunit.xml 配置相關的內容,還是需要檢視文件。 :laughing:
Mock 測試
PHPUnit 還提供了 Mock 測試。這裡先介紹一下什麼是 Mock 測試。
假設 foo 函式呼叫了 bar 函式,那麼在對 foo 函式進行單元測試會有兩個問題:
- foo 函式依賴於 bar 函式的結果,那麼在對 foo 進行單元測試的時候必然會引入 bar ,那麼這樣子單元測試就沒意義了,如果測試不通過,那麼無法保證 bug 出在 foo 還是 bar。
- bar 函式可能在測試環境不可執行,那麼 foo 無法獲取 bar 的執行結果,從而無法對 foo 進行單元測試。
Mock 測試就是為了解決上面的問題而出現的,使用 Mock 我們可以虛擬出一個 bar 的呼叫,並且假設 bar 呼叫返回結果。如果還是聽不懂,上一段程式碼就知道了。
class MockTest extends \PHPUnit\Framework\TestCase {
public function testGet()
{
$stub = $this->createMock(\App\UserService::class); //1
$stub->method('get')->willReturn(3); //2
$this->assertEquals(3,$stub->get(1)); //3
}
}
複製程式碼
上面的測試函式就使用到了 Mock,一行一行程式碼來分析:
- 第一行建立了一個虛擬的
UserService
物件。 - 第二行假設
UserService
中的get
函式的返回值為 3。 - 第三行呼叫
$stub->get(1)
不會真的去執行get
函式,而是根據第二行的willReturn
函式直接返回 3。
以上就是一個簡單的 Mock 測試,當然 Mock 測試還有很多複雜的用法,這裡沒辦法一一展開,其實掌握基本的用法,更多複雜的高階用法在實踐中碰到了再去檢視文件也不遲。
好了,PHPUnit 的基本操作就這些了,單元測試本身並不是一個很難的東西,阻礙單元測試的進行並不是在技術上,更多的是一個專案時間安排的衡量與考慮。