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 基類即有兩個方法, setUpsetDown 分別用於為每個單元測試創建測試對象和清理測試對象

數據供給器

對同一類情況進行測試,通常可以用數據供給器傳入不同入參和相應的預期返回值。

測試方法可以接受任意參數。這些參數由數據供給器方法提供。在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 文件,直接運行即可

如何編寫單元測試

所有類需要繼承 PHPUnitFrameworkTestCasesetUp 函數用於初始化測試對象, 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