PHP 單元測試

jaychen發表於2017-12-25

本文首發於 https://jaychen.cc/article/34

作者 Jaychen

PHP 單元測試

朋友,你聽說過安。。。不是,寫過單元測試嗎。

單元測試是開發過程中必不可少的一環,一個專案有良好的單元測試程式碼,重構的勇氣都大很多。這次寫一篇小文來介紹一下 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 進行開發,那麼你需要進行如下的配置:

PHP 單元測試

這裡指明瞭從哪裡載入 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 執行:

PHP 單元測試

發現在控制檯會輸出如下內容

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 的基本操作就這些了,單元測試本身並不是一個很難的東西,阻礙單元測試的進行並不是在技術上,更多的是一個專案時間安排的衡量與考慮。

相關文章