Laravel Testing Decoded (導讀) 第二章 介紹 PHPUnit

PHPUnit 是一個單元測試框架,由 Sebastian Bergmann 所開發。

安裝

有幾種方式可以安裝 PHPUnit

  • Pear
  • Composer
  • PHAR

可以自由選擇你希望的方式來安裝,不過在本書中會大量利用 Laravel 以及 Composer ,會使用最簡單的途徑,在 Laravel 透過 composer.json 來進行安裝 PHPUnit

什麼是 Composer 呢?Composer 是一個 PHP 的套件管理工具,作為 Laravel 開發人員之前,需要基本了解 Composer 是什麼以及如何使用它

在深入了解 Laravel 之前,讓我們從最基礎開始

建立一個新的資料夾練習,在資料夾裡新增一個 composer.json 的檔案並加入以下

1
2
3
4
5
{
"require-dev": {
"phpunit/phpunit": "5.7.*"
}
}

注意我們並沒有加到 require 中,PHPUnit 只有在開發過程中所需要,我們不需要擔心會在正式服務器上。

下一步是安裝以及任何其它依賴的套件,使用 command line

假設已將 Composer 進行 global 安裝

1
composer install --dev

如果一切順利安裝,你可以查看 vendor/bin/phpunit 、 vendor/phpunit ,試試查詢 PHPUnit 的命令列表

1
./vendor/bin/phpunit -h

Global 安裝

在前一章節使用的方法是需要在每一個項目下透過 Composer 進行安裝 PHPUnit,你也可以將 PHPUnit 做 Global 安裝。

在你的 Home 目錄(或任何地方)建立一個新的資料夾,在資料夾裡新增一個 composer.json 的檔案加入你希望 Global require-dev 的套件並安裝

1
2
3
4
5
{
"require": {
"phpunit/phpunit": "5.7.*"
}
}

接下來將這個資料夾下 vendor/bin 的完整路徑加入到 ~/.bash_profile 或 ~/.zshrc (有使用 oh-my-zsh)

1
exportPATH=/Users/duncan/.composer/vendor/bin

透過這個方式,PHPUnit 就可以在任何地方所呼叫

使用 Composer Global 指令

前面需要手動建立資料夾、composer.json ,Composer 有提供 global 安裝的方式

1
composer global require "phpunit/phpunit"

安裝完會在 Home 目錄下建立 .composer 的資料夾,並在 ~/.bash_profile 或 ~/.zshrc (有使用 oh-my-zsh)加入以下

1
export PATH=$PATH:$HOME/.composer/vendor/bin

Assertions 101

開始來測試 “hello world”,在這章節,將介紹你第一個 PHPunit assertion,assertTrue

在你的專案下建立 tests 資料夾,並在資料夾下建立一個新檔叫 PracticeTest.php

1
2
3
4
5
6
7
8
9
10
<?php
class PracticeTest extends PHPUnit_Framework_TestCase
{
public function testHelloWorld()
{
$greeting = 'Hello, World.';
$this->assertTrue($greeting === 'Hello, World.');
}
}

執行測試來看它是否可以正常運行

因為我們還沒配置任何設定,所以必需指定一個路徑來進行測試,PHPUnit 會在指定的路徑下尋找測試檔案。

首先,請注意最後一行

這表示一個測試成功了,如果測試失敗,你會看到大寫 F

你將會時常看到測試失敗,所以要去適應它,幸運的是,這個失敗顯示被證明是非常有用的。

以上我們可以看到一個 testHellowWorld 測試失敗,並且是在第八行。

為什麼會有顏色?在下一章會更深入介紹 PHPUnit 的配置選項,在那之前,使用 –colors 就會有顏色顯示

1
phpunit --colors tests

解析一個 Test Class 的結構

僅管這是一個簡單的 Class,但有幾個重要並要注意的地方,以下又是一個測試程式

1
2
3
4
5
6
7
8
9
10
<?php
class PracticeTest extends PHPUnit_Framework_TestCase
{
public function testHelloWorld()
{
$greeting = 'Hello, World.';
$this->assertTrue($greeting === 'Hello, World.', $greeting);
}
}
  • File Naming:檔名是很重要的,注意我們要跟著 FooTest.php 的規範,雖然這是可以修改的
  • Matching:Class 名稱要與檔名一致
  • Inheritance:Class 要繼承 PHPUnit_Framework_TestCase,這個 Class 在使用 PHPUnit 時可用,不過,很快可以發現,在 Laravel 的測試中,會是繼承 TestCase ,如果往上查,你會發現父類別確實繼承了 PHPUnit_Framework_TestCase 。
  • Method Naming:每一個 Method 名稱應該都描述著這個測試在測什麼,並以 test 為開頭

assertTure

1
2
$greeting = 'Hello, World.';
$this->assertTrue($greeting == 'Hello, World.', $greeting);

希望你會發現 PHPUnit 的 test assertions 相當有可讀性,即使不了解具體細節,也很容易知道在做什麼,所有的 assertions 在 test class 都可使用,在這個例子,我們希望 $greeting 等於 “Hello, World.”

這個 assertTure method 接受兩個參數

1
$this->assertTrue(ACTUAL, OPTIONAL MESSAGE);

你會發現在 PHPUnit 裡,反向 methods 幾乎可以用,如果你要 assert 這個值為 false,那你可以使用 assertFalse

1
$this->assertFalse(ACTUAL, OPTIONAL MESSAGE);

assertEquals

在前面的例子中,我們唯一的目標是 assert 一個變數一直等於指定的字串,雖然 assertTrue 可以使用,但在這種情況下,它不是一個最佳可讀的方式,不要忽略測試的可讀性。

這種介紹另一個更適合這個例子的 assertion:assertEquals

1
2
$greeting = 'Hello, World.';
$this->assertEquals('Hello, World.', $greeting);

這樣可讀性更好,與大多數的 PHPUnit assertions 一樣,assertEquals 接受三個參數

1
$this->assertEquals(EXPECTED, ACTUAL, OPTIONAL MESSAGE);

assertNotEquals 是一個反向 assertion,並接受相同的參數

如果你想證明兩個值是相等的,那麼明顯的使用 assertEquals 比 assertTrue 更好的選擇,即使兩個都可以使用。

assertSame

assertEquals 在需要更嚴格的相等比較時會失敗,例如 assert 變數等於 0?

你可以嘗試:

1
2
$val = 0;
$this->assertEquals(0, $val);

這樣是可以通過測試,但如果讓變數為 null ,這個測試還是會通過測試

1
2
$val = null;
$this->assertEquals(0, $val); // true

這這種情況下,當你需要更嚴格的比較(或有效的 ===),那 assertSame 可以做到

1
2
$val = null;
$this->assertSame(0, $val); // false
1
2
$val = 0;
$this->assertSame(0, $val); // true

assertContains

本書的目的不是教每個 assertion ,但重要的是覆蓋到要點,雖然 PHPUnit 提供了幾十個 assertions ,但你可能會發現,少數幾個就能滿足大多數的測試需求。

假設你有一個名字列表,需要証明該陣列包含一個特定的值,assertTrue 可以處理這個任務,但最好選擇更有可讀性的選項:assertContains 。

1
2
3
4
5
public function testLaravelDevsIncludesDayle()
{
$names = ['Taylor', 'Shawn', 'Dayle'];
$this->assertContains('Dayle', $names);
}

assertContains 的參數值是:

1
$this->assertContains(NEEDLE, HAYSTACK, OPTIONALMESSAGE);

正如你所知道的,反向 assertion 也是可用的

1
2
$names = ['Taylor', 'Shawn', 'Dayle'];
$this->assertNotContains('Troll', $names); // true

assertArrayHasKey

有時你需要 assert 陣列裡的 key,而不是 value

1
2
3
4
$family = [
'parents' => 'Joe',
'children' => ['Timmy', 'Suzy']
];

在這個例子中,假設我們需要驗證 $family 裡有包含 “parents”,在這樣的情況下, assertContains 不是正確的選擇

1
2
// 無法 assert 陣列裡包含了 'parents'
$this->assertContains('parents', $family); // false

我們所需要 assert 指定陣列中的一個 key 值,解決方案是 assertArrayHasKey 。

1
2
3
4
5
6
7
8
public function testFamilyRequiresParent()
{
$family = [
'parents' => 'Joe',
'children' => ['Timmy', 'Suzy']
];
$this->assertArrayHasKey('parents', $family); // true
}

assertInternalType

assertInternalType 用來測試變數的型態

1
$this->assertInternalType(EXPECTED, ACTUAL, MESSAGE);

繼續上一個例子,要 assert 這個 $family 裡 parents 為字串或陣列,我們可以這樣做

1
2
3
4
5
6
7
8
public function testFamilyRequiresParent()
{
$family = [
'parents' => 'Joe',
'children' => ['Timmy', 'Suzy']
];
$this->assertInternalType('array', $family['parents']); // false
}
1
$this->assertInternalType('string', $family['parents']); // true

或是要 assert 的型態是整數

1
2
$age = 25;
$this->assertInternalType('integer', $age); // true

assertInstanceOf

時常你需要確保變數值是某個 Class 的 Instance(實體),這在 PHPUnit 中透過 assertInstanceOf 很容易做到

1
$this->assertInstanceOf(EXPECTED, ACTUAL, MESSAGE);

範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DateFormatter
{
protected $stamp;
public function __construct(DateTime $stamp)
{
$this->stamp = $stamp;
}
public function getStamp()
{
return $this->stamp;
}
}

要確保 $stamp 是 PHP DateTime Class 的 Instance ,我們可以使用以下測試方式

1
2
3
4
5
public function testStampMustBeInstanceOfDateTime()
{
$date = new DateFormatter(new DateTime);
$this->assertInstanceOf('DateTime', $date->getStamp()); // true
}

Asserting Exceptions

在單元測試時,重要的是測試你的程式碼中每一個可能的路徑,如果使用太多條件,會讓一個 Class 或 Method 難以測試。

這樣的例子中可能會拋出一個異常,在 PHPUnit 我們使用 doc-blocks 來 assert exceptions .

1
2
3
/**
* @expectedException EXCEPTION_NAME
*/

這個 doc-blocks 宣告 PHPUnit 應該期望拋出異常,如果沒有,那麼測試就會失敗。

假設一個 Method 如果非數字值傳入應該拋出異常

1
2
3
4
5
6
7
8
/**
* @expectedException InvalidArgumentException
*/
public function testCalculatesCommission()
{
$commission = new Commission;
$commission->setSalePrice('fifteen dollars');
}

請注意,這種情況下,沒有使用 assert ,我們只是加上必要的程式碼,讓這個 Class 發生異常。

總結

雖然可以繼續介面每個 PHPUnit assertion ,但這是不明智的,本章涵蓋少數的 assertions 應該足夠你進行測試了,當你還需要其它功能時,再去查詢文件即可。

接下來在我們開始進行 Laravel 特定的測試之前,我們應該花一些時間來更全面的設定 PHPUnit 配置來滿足我們的需求