《現代 PHP》學習筆記(廿二):測試

前言

本文為《現代 PHP》一書的學習筆記。

環境

  • Windows 10
  • Wnmp 3.1.0

測試

許多 PHP 開發者將測試視為不必要且浪費時間的行為,而且現存太多的測試工具,有著不易上手的學習曲線。但其實測試是一個會發生在應用程式開發流程的開始、中途和結束的程序。

為何要做測試

測試可以幫助開發者在第一時間就運作良好的程式,不需要在未來浪費無數小時來為程式除錯並重構。

何時要做測試

測試是一個在開發前、開發中、開發後都存在的議題,敷衍地建立幾個測試並且通過它,這是錯誤的行為。

  • 開發前:安裝並部署測試工具。

  • 開發中:建立應用程式中的每個部分時,都撰寫測試並執行測試。

  • 開發後:持續性地修正並改良測試,就像應用程式本身一樣。

測試什麼

獨立地測試應用程式中最小的區塊,如每個類別、方法和函式,以確保他們如預期執行,這種測試稱為「單元測試」。

但測試區塊並不能保證整個應用程式都能如期運作,因此還需要利用自動化的工具測試應用程式高層級的行為,這種測試稱為「功能測試」。

如何測試

PHP 開發者使用了幾種熱門的方式進行測試,他們之間並不互斥。

單元測試

單元測試是最熱門的,現在標準的 PHP 單元測試框架為由 Sebastian Bergmann 所撰寫的 PHPUnit。

測試驅動開發(TDD)

測試驅動開發(Test-driven development)是在撰寫程式碼之前先撰寫測試,這些測試會故意失敗,因為它描述了應用程式應當擁有的行為。

TDD 不停重複「撰寫測試並建立功能」的這個過程,直到應用程式完成為止。

行為驅動開發(BDD)

行為驅動開發(Behavior-driven development)有兩種形式:SpecBDD 和 StoryBDD。

BDD 使用流暢的人性化語言來描述應用程式的實作層面,SpecBDD 測試以開發者的角度組合事情,而 StoryBDD 以專案管理者的角度組合事情,他們經常被一起使用。

PHPUnit

PHPUnit 測試會集合成測試案例,測試案例會集合成測試程序,PHPUnit 會使用測試執行器來進行測試。

每個案例都包含了名稱以 test 為前綴的公開方法,這些方法是獨立的測試,用來測試特別的情況是否如預期執行。

一個測試類別通常以 Test 結尾。

初始的 PHPUnit 測試執行器是命令列執行器,可以在終端機使用 phpunit 指令來觸發。

目錄架構

範例的目錄架構如下:

1
2
3
4
5
6
|- src/
|- tests/
|- bootstrap.php
|- composer.json
|- phpunit.xml
|- .travis.yml
  • src 目錄包含 PHP 專案的原始碼。
  • tests 目錄包含 PHP 專案的 PHPUnit 測試,其中的 bootstrap.php 檔在單元測試執行前,會由 PHPUnit 所匯入。
  • composer.json 列出 Composer 所管理的相依性元件。
  • phpunit.xml 提供 PHPUnit 測試執行器的設定細節。
  • .travis.yml 提供 Travis CI 持續測試網頁服務所需要的設定細節。

啟用 Xdebug

Xdebug 剖析器可以用來產生有用的程式碼覆蓋率資訊。

如果是在 Wnmp 的環境,打開 php.ini 檔並取消以下註解以啟用 Xdebug PHP 擴充。

1
;zend_extension=php_xdebug.dll

安裝 PHPUnit

Composer 是安裝 PHPUnit 測試框架最方便的方式之一。

1
composer require --dev phpunit/phpunit

配置 PHPUnit

在專案的 phpunit.xml 檔中配置 PHPUnit。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="whovian">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>

<filter>
<whitelist>
<directory>src</directory>
</whitelist>
</filter>
</phpunit>
  • <phpunit> 根元素的屬性是 PHPUnit 測試執行器的設定,最重要的設定是 bootstrap 設定,它指明在 PHPUnit 測試執行前,會被匯入的 PHP 檔案路徑。
  • <testsuites> 元素定義 tests 資料夾中,以 Test.php 結尾的檔案將被 PHPUnit 執行。
  • <filter> 元素列出所有包含在程式碼覆蓋率檢測中的目錄。
  • <whitelist> 元素告訴 PHPUnit 只要針對 src 目錄進行程式碼覆蓋率檢測。

實作

假想一個 Whovian 類別作為測試範例。

範例 10-1:Whovian 類別

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Whovian
{
/**
* @var string
*/
protected $favoriteDoctor;

/**
* Constructor
* @param string $favoriteDoctor
*/
public function __construct($favoriteDoctor)
{
$this->favoriteDoctor = (string)$favoriteDoctor;
}

/**
* Say
* @return string
*/
public function say()
{
return 'The best doctor is ' . $this->favoriteDoctor;
}

/**
* Respond to
* @param string $input
* @return string
* @throws \Exception
*/
public function respondTo($input)
{
$input = strtolower($input);
$myDoctor = strtolower($this->favoriteDoctor);

if (strpos($input, $myDoctor) === false) {
throw new Exception(
sprintf(
'No way! %s is the best doctor ever!',
$this->favoriteDoctor
)
);
}

return 'I agree!';
}
}

每個測試案例都需要繼承 PHPUnit_Framework_TestCase 類別。

範例 10-2:WhovianTest 測試案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
require dirname(__DIR__) . '/src/Whovian.php';

class WhovianTest extends PHPUnit_Framework_TestCase
{
public function testSetsDoctorWithConstructor()
{
$whovian = new Whovian('Peter Capaldi');
$this->assertAttributeEquals('Peter Capaldi', 'favoriteDoctor', $whovian);
}

public function testSaysDoctorName()
{
$whovian = new Whovian('David Tennant');
$this->assertEquals('The best doctor is David Tennant', $whovian->say());
}

public function testRespondToInAgreement()
{
$whovian = new Whovian('David Tennant');

$opinion = 'David Tennant is the best doctor, period';
$this->assertEquals('I agree!', $whovian->respondTo($opinion));
}

/**
* @expectedException Exception
*/
public function testRespondToInDisagreement()
{
$whovian = new Whovian('David Tennant');

$opinion = 'No way. Matt Smith was awesome!';
$whovian->respondTo($opinion);
}
}
  • assertAttributeEquals() 方法斷言預期的値與指定物件的屬性的値相同。
  • assertEquals() 方法斷言預期的値與結果的値相同。

執行測試

打開終端機,進入專案的最上層目錄,使用以下指令啟動 PHPUnit 測試執行器。

1
vendor/bin/phpunit -c phpunit.xml
  • -c--configuration 選項可以使用不同的設定檔。

程式碼覆蓋率

PHPUnit 的程式碼覆蓋率報表,可以觀看哪些程式碼被測試(或未被測試)。

使用以下指令生成報表。

1
vendor/bin/phpunit -c phpunit.xml --coverage-html coverage
  • --coverage-html 的値代表存放報表的資料夾路徑。

接著,在 http://localhost/php/modern-php/10-testing/coverage/ 可以查看報表。

通常 100% 的覆蓋率是不切實際的,覆蓋率多寡的程度根據每個專案而有所不同。

Travic CI

測試應當自動化執行,Travic CI 是很好的持續測試網頁服務,因為它可以原生地和 GitHub 儲存庫結合。

前往 Travic CI,以 GitHub 帳戶登入,選擇要測試的儲存庫。

範例的 .travis.yml 檔如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
language: php

php:
- 5.3.3
- 5.3
- 5.4
- 5.5
- 5.6

before_script:
- COMPOSER_ROOT_VERSION=dev-master composer install --prefer-source

script: vendor/bin/phpunit --configuration ./build/travis-ci.xml

notifications:
email: false
irc:
channels:
- "irc.freenode.org#phpunit"
use_notice: true
  • language 是應用程式所使用的語言。
  • php 是 Travic CI 要測試應用程式的 PHP 版本。
  • before_script 是在 Travic CI 要測試應用程式之前會執行的指令。
  • script 是在 Travic CI 要測試應用程式時執行的指令。
  • notification 是通知的相關設定。

執行

每次提交新的程式碼到 GitHub 儲存庫時,Travic CI 會自動執行應用程式測試,並將測試結果以電子郵件通知。