PHPUnit 手冊

PHPUnit 手冊

Sebastian Bergmann

本做品依據 Creative Commons Attribution 3.0 Unported 許可協議進行受權。html

此版本對應於 PHPUnit 6.1。最後更新於 2017-04-25。node


1. 安裝 PHPUnit
需求
PHP 檔案包 (PHAR)
Windows
校驗 PHPUnit PHAR 發行包
Composer
可選的組件包
2. 編寫 PHPUnit 測試
測試的依賴關係
數據供給器
對異常進行測試
對 PHP 錯誤進行測試
對輸出進行測試
錯誤相關信息的輸出
邊緣狀況
3. 命令行測試執行器
命令行選項
4. 基境(fixture)
setUp() 多 tearDown() 少
變體
基境共享
全局狀態
5. 組織測試
用文件系統來編排測試套件
用 XML 配置來編排測試套件
6. 有風險的測試
無用測試
意外的代碼覆蓋
測試執行期間產生的輸出
測試執行時長的超時限制
全局狀態篡改
7. 未完成的測試與跳過的測試
未完成的測試
跳過測試
用 @requires 來跳過測試
8. 數據庫測試
數據庫測試所支持的供應商
數據庫測試的難點
數據庫測試的四個階段
1. 清理數據庫
2. 創建基境
3–5. 運行測試、驗證結果、並拆除基境
PHPUnit 數據庫測試用例的配置
實現 getConnection()
實現 getDataSet()
數據庫構架(DDL)怎麼辦?
小建議:使用你本身的抽象數據庫 TestCase 類
理解 DataSet(數據集)和 DataTable(數據表)
可用的各類實現
小心外鍵
實現自有的 DataSet/DataTable
數據庫鏈接 API
數據庫斷言 API
對錶中數據行的數量做出斷言
對錶的狀態做出斷言
對查詢的結果做出斷言
對多個表的狀態做出斷言
常見問題(FAQ)
PHPUnit 會爲每一個測試(從新)建立數據庫嗎?
爲了讓數據庫擴展模塊正常工做,須要在應用程序中使用 PDO 嗎?
若是看到 Too much Connections」 錯誤該怎麼辦?
Flat XML / CSV 數據集中如何處理 NULL?
9. 測試替身
Stubs (樁件)
仿件對象(Mock Object)
Prophecy
對特質(Trait)與抽象類進行模仿
對 Web 服務(Web Services)進行上樁或模仿
對文件系統進行模仿
10. 測試實踐
在開發過程當中
在調試過程當中
11. 代碼覆蓋率分析
用於代碼覆蓋率的軟件衡量標準
將文件列入白名單
略過代碼塊
指明要覆蓋的方法
邊緣狀況
12. 測試的其餘用途
敏捷文檔
跨團隊測試
13. Logging (日誌記錄)
測試結果 (XML)
測試結果 (TAP)
測試結果 (JSON)
代碼覆蓋率 (XML)
代碼覆蓋率 (TEXT)
14. 擴展 PHPUnit
PHPUnit\Framework\TestCase 的子類
編寫自定義斷言
實現 PHPUnit_Framework_TestListener
從 PHPUnit_Extensions_TestDecorator 派生子類
實現 PHPUnit_Framework_Test
A. 斷言
assertArrayHasKey()
assertClassHasAttribute()
assertArraySubset()
assertClassHasStaticAttribute()
assertContains()
assertContainsOnly()
assertContainsOnlyInstancesOf()
assertCount()
assertDirectoryExists()
assertDirectoryIsReadable()
assertDirectoryIsWritable()
assertEmpty()
assertEqualXMLStructure()
assertEquals()
assertFalse()
assertFileEquals()
assertFileExists()
assertFileIsReadable()
assertFileIsWritable()
assertGreaterThan()
assertGreaterThanOrEqual()
assertInfinite()
assertInstanceOf()
assertInternalType()
assertIsReadable()
assertIsWritable()
assertJsonFileEqualsJsonFile()
assertJsonStringEqualsJsonFile()
assertJsonStringEqualsJsonString()
assertLessThan()
assertLessThanOrEqual()
assertNan()
assertNull()
assertObjectHasAttribute()
assertRegExp()
assertStringMatchesFormat()
assertStringMatchesFormatFile()
assertSame()
assertStringEndsWith()
assertStringEqualsFile()
assertStringStartsWith()
assertThat()
assertTrue()
assertXmlFileEqualsXmlFile()
assertXmlStringEqualsXmlFile()
assertXmlStringEqualsXmlString()
B. 標註
@author
@after
@afterClass
@backupGlobals
@backupStaticAttributes
@before
@beforeClass
@codeCoverageIgnore*
@covers
@coversDefaultClass
@coversNothing
@dataProvider
@depends
@expectedException
@expectedExceptionCode
@expectedExceptionMessage
@expectedExceptionMessageRegExp
@group
@large
@medium
@preserveGlobalState
@requires
@runTestsInSeparateProcesses
@runInSeparateProcess
@small
@test
@testdox
@ticket
@uses
C. XML 配置文件
PHPUnit
測試套件
分組
Whitelisting Files for Code Coverage
Logging (日誌記錄)
測試監聽器
設定 PHP INI 設置、常量、全局變量
D. 索引
E. 參考書目
F. 版權

第 1 章 安裝 PHPUnit

需求

PHPUnit 6.1 須要 PHP 5.6,強烈推薦使用最新版本的 PHP。python

PHPUnit 須要使用 dom 和 json 擴展,它們一般是默認啓用的。mysql

PHPUnit 還須要 pcrereflectionspl 擴展。這些標準擴展默認啓用,而且除非修改 PHP 的構建系統和 C 源代碼,不然沒法禁用它們。git

代碼覆蓋率分析報告功能須要 Xdebug (2.2.1以上)與 tokenizer 擴展。生成 XML 格式的報告須要有 xmlwriter 擴展。github

PHP 檔案包 (PHAR)

要獲取 PHPUnit,最簡單的方法是下載 PHPUnit 的 PHP 檔案包 (PHAR),它將 PHPUnit 所須要的全部必要組件(以及某些可選組件)捆綁在單個文件中:web

要使用 PHP檔案包(PHAR)須要有 phar 擴展。正則表達式

要使用 PHAR 的 --self-update 功能須要有 openssl 擴展。redis

若是啓用了 Suhosin 擴展,須要在 php.ini 中容許執行 PHAR:

suhosin.executor.include.whitelist = phar

 

若是要全局安裝 PHAR:

 
 
 
 

$wget https://phar.phpunit.de/phpunit.phar$chmod +x phpunit.phar$sudo mv phpunit.phar /usr/local/bin/phpunit$phpunit --versionPHPUnit x.y.z by Sebastian Bergmann and contributors.

也能夠直接使用下載的 PHAR 文件:

 
 

$wget https://phar.phpunit.de/phpunit.phar$php phpunit.phar --versionPHPUnit x.y.z by Sebastian Bergmann and contributors.

Windows

總體上說,在 Windows 下安裝 PHAR 和手工在 Windows 下安裝 Composer 是同樣的過程:

  1. 爲 PHP 的二進制可執行文件創建一個目錄,例如 C:\bin

  2. 將 ;C:\bin 附加到 PATH 環境變量中(相關幫助

  3. 下載 https://phar.phpunit.de/phpunit.phar 並將文件保存到 C:\bin\phpunit.phar

  4. 打開命令行(例如,按 Windows+R » 輸入 cmd » ENTER)

  5. 創建外包覆批處理腳本(最後獲得 C:\bin\phpunit.cmd):

     
     
     
    C:\Users\username>cd C:\binC:\bin>echo @php "%~dp0phpunit.phar" %* > phpunit.cmdC:\bin>exit
  6. 新開一個命令行窗口,確認一下能夠在任意路徑下執行 PHPUnit:

     
    
    C:\Users\username>phpunit --versionPHPUnit x.y.z by Sebastian Bergmann and contributors.

對於 Cygwin 或 MingW32 (例如 TortoiseGit) shell 環境,能夠跳過第五步。 取而代之的是,把文件保存爲 phpunit (沒有 .phar 擴展名),而後用 chmod 775 phpunit 將其設爲可執行。

校驗 PHPUnit PHAR 發行包

由 PHPUnit 項目分發的全部官方代碼發行包都由發行包管理器進行簽名。在 phar.phpunit.de 上有 PGP 簽名和 SHA1 散列值可用於校驗。

下面的例子詳細說明了如何對發行包進行校驗。首先下載 phpunit.phar 和與之對應的單獨 PGP 簽名 phpunit.phar.asc


wget https://phar.phpunit.de/phpunit.pharwget https://phar.phpunit.de/phpunit.phar.asc

用單獨的簽名(phpunit.phar)對 PHPUnit 的 PHP 檔案包(phpunit.phar.asc)進行校驗:


gpg: Signature made Sat 19 Jul 2014 01:28:02 PM CEST using RSA key ID 6372C20A
gpg: Can't check signature: public key not foundgpg phpunit.phar.asc

在本地系統中沒有發行包管理器的公鑰(6372C20A)。爲了能進行校驗,必須從某個密鑰服務器上取得發行包管理器的公鑰。其中一個服務器是 pgp.uni-mainz.de。全部密鑰服務器是連接在一塊兒的,所以鏈接到任一密鑰服務器均可以。


gpg: requesting key 6372C20A from hkp server pgp.uni-mainz.de
gpg: key 6372C20A: public key "Sebastian Bergmann <sb@sebastian-bergmann.de>" imported
gpg: Total number processed: 1
gpg:               imported: 1  (RSA: 1)gpg --keyserver pgp.uni-mainz.de --recv-keys 0x4AA394086372C20A

如今已經取得了條目名稱爲"Sebastian Bergmann <sb@sebastian-bergmann.de>"的公鑰。不過沒法檢驗這個密鑰確實是由名叫 Sebastian Bergmann 的人建立的。可是能夠先試着校驗發行包的簽名:


gpg: Signature made Sat 19 Jul 2014 01:28:02 PM CEST using RSA key ID 6372C20A
gpg: Good signature from "Sebastian Bergmann <sb@sebastian-bergmann.de>"
gpg:                 aka "Sebastian Bergmann <sebastian@php.net>"
gpg:                 aka "Sebastian Bergmann <sebastian@thephp.cc>"
gpg:                 aka "Sebastian Bergmann <sebastian@phpunit.de>"
gpg:                 aka "Sebastian Bergmann <sebastian.bergmann@thephp.cc>"
gpg:                 aka "[jpeg image of size 40635]"
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: D840 6D0D 8294 7747 2937  7831 4AA3 9408 6372 C20Agpg phpunit.phar.asc

此時,簽名已經沒問題了,可是這個公鑰還不能信任。簽名沒問題意味着文件未被篡改。但是因爲公鑰加密系統的性質,還須要再校驗密鑰 6372C20A 確實是由真正的 Sebastian Bergmann 建立的。

任何攻擊者都能建立公鑰並將其上傳到公鑰服務器。他們能夠創建一個帶惡意的發行包,並用這個假密鑰進行簽名。這樣,若是嘗試對這個損壞了的發行包進行簽名校驗,因爲密鑰是「真」密鑰,校驗將成功完成。所以,須要對這個密鑰的真實性進行校驗。如何對公鑰的真實性進行校驗已經超出了本文檔的範疇。

有個比較謹慎的作法是建立一個腳原本管理 PHPUnit 的安裝,在運行測試套件以前校驗 GnuPG 簽名。例如:

#!/usr/bin/env bash
clean=1 # 是否在測試完成以後刪除 phpunit.phar ?
aftercmd="php phpunit.phar --bootstrap bootstrap.php src/tests"
gpg --fingerprint D8406D0D82947747293778314AA394086372C20A
if [ $? -ne 0 ]; then
    echo -e "\033[33mDownloading PGP Public Key...\033[0m"
    gpg --recv-keys D8406D0D82947747293778314AA394086372C20A
    # Sebastian Bergmann <sb@sebastian-bergmann.de>
    gpg --fingerprint D8406D0D82947747293778314AA394086372C20A
    if [ $? -ne 0 ]; then
        echo -e "\033[31mCould not download PGP public key for verification\033[0m"
        exit
    fi
fi

if [ "$clean" -eq 1 ]; then
    # 若是存在就清理掉
    if [ -f phpunit.phar ]; then
        rm -f phpunit.phar
    fi
    if [ -f phpunit.phar.asc ]; then
        rm -f phpunit.phar.asc
    fi
fi

# 抓取最新的發行版和對應的簽名
if [ ! -f phpunit.phar ]; then
    wget https://phar.phpunit.de/phpunit.phar
fi
if [ ! -f phpunit.phar.asc ]; then
    wget https://phar.phpunit.de/phpunit.phar.asc
fi

# 在運行前先校驗
gpg --verify phpunit.phar.asc phpunit.phar
if [ $? -eq 0 ]; then
    echo
    echo -e "\033[33mBegin Unit Testing\033[0m"
    # 運行測試套件
    `$after_cmd`
    # 清理
    if [ "$clean" -eq 1 ]; then
        echo -e "\033[32mCleaning Up!\033[0m"
        rm -f phpunit.phar
        rm -f phpunit.phar.asc
    fi
else
    echo
    chmod -x phpunit.phar
    mv phpunit.phar /tmp/bad-phpunit.phar
    mv phpunit.phar.asc /tmp/bad-phpunit.phar.asc
    echo -e "\033[31mSignature did not match! PHPUnit has been moved to /tmp/bad-phpunit.phar\033[0m"
    exit 1
fi
      

Composer

若是用 Composer 來管理項目的依賴關係,只要在項目的 composer.json 文件中簡單地加上對 phpunit/phpunit 的依賴關係便可。下面是一個最小化的 composer.json 文件的例子,只定義了一個對 PHPUnit 6.1 的開發時(development-time)依賴:

{
    "require-dev": {
        "phpunit/phpunit": "5.5.*"
    }
}

要經過 Composer 完成系統級的安裝,能夠運行:

composer global require "phpunit/phpunit=5.5.*"

請確保 path 變量中包含有 ~/.composer/vendor/bin/

可選的組件包

有如下可選組件包可用:

PHP_Invoker

一個工具類,能夠用帶有超時限制的方式調用可調用內容。當須要在嚴格模式下保證測試的超時限制時,這個組件包是必須的。

PHPUnit 的 PHAR 分發中已經包含了此組件包。若要經過 Composer 安裝此組件包,添加以下 "require-dev" 依賴項:

"phpunit/php-invoker": "*"
DbUnit

移植到 PHP/PHPUnit 上的 DbUnit 用於提供對數據庫交互測試的支持。

PHPUnit 的 PHAR 分發中已經包含了此組件包。若要經過 Composer 安裝此組件包,添加以下 "require-dev" 依賴項:

"phpunit/dbunit": ">=1.2"

第 2 章 編寫 PHPUnit 測試

例 2.1展現瞭如何用 PHPUnit 編寫測試來對 PHP 數組操做進行測試。本例介紹了用 PHPUnit 編寫測試的基本慣例與步驟:

  1. 針對類 Class 的測試寫在類 ClassTest中。

  2. ClassTest(一般)繼承自 PHPUnit\Framework\TestCase

  3. 測試都是命名爲 test* 的公用方法。

    也能夠在方法的文檔註釋塊(docblock)中使用 @test 標註將其標記爲測試方法。

  4. 在測試方法內,相似於 assertEquals()(參見 附錄 A)這樣的斷言方法用來對實際值與預期值的匹配作出斷言。

例 2.1: 用 PHPUnit 測試數組操做

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testPushAndPop()
    {
        $stack = [];
        $this->assertEquals(0, count($stack));

        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertEquals(1, count($stack));

        $this->assertEquals('foo', array_pop($stack));
        $this->assertEquals(0, count($stack));
    }
}
?>

 

當你想把一些東西寫到 print 語句或者調試表達式中時,別這麼作,將其寫成一個測試來代替。

 
  --Martin Fowler

測試的依賴關係

 

單元測試主要是做爲一種良好實踐來編寫的,它能幫助開發人員識別並修復 bug、重構代碼,還能夠看做被測軟件單元的文檔。要實現這些好處,理想的單元測試應當覆蓋程序中全部可能的路徑。一個單元測試一般覆蓋一個函數或方法中的一個特定路徑。可是,測試方法並不必定非要是一個封裝良好的獨立實體。測試方法之間常常有隱含的依賴關係暗藏在測試的實現方案中。

 
  --Adrian Kuhn et. al.

PHPUnit支持對測試方法之間的顯式依賴關係進行聲明。這種依賴關係並非定義在測試方法的執行順序中,而是容許生產者(producer)返回一個測試基境(fixture)的實例,並將此實例傳遞給依賴於它的消費者(consumer)們。

  • 生產者(producer),是能生成被測單元並將其做爲返回值的測試方法。

  • 消費者(consumer),是依賴於一個或多個生產者及其返回值的測試方法。

例 2.2展現瞭如何用 @depends 標註來表達測試方法之間的依賴關係。

例 2.2: 用 @depends 標註來表達依賴關係

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testEmpty()
    {
        $stack = [];
        $this->assertEmpty($stack);

        return $stack;
    }

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack)
    {
        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

    /**
     * @depends testPush
     */
    public function testPop(array $stack)
    {
        $this->assertEquals('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}
?>

在上例中,第一個測試, testEmpty(),建立了一個新數組,並斷言其爲空。隨後,此測試將此基境做爲結果返回。第二個測試,testPush(),依賴於 testEmpty() ,並將所依賴的測試之結果做爲參數傳入。最後,testPop() 依賴於 testPush()

注意

默認狀況下,生產者所產生的返回值將「原樣」傳遞給相應的消費者。這意味着,若是生產者返回的是一個對象,那麼傳遞給消費者的將是一個指向此對象的引用。若是須要傳遞對象的副本而非引用,則應當用 @depends clone 替代 @depends

爲了快速定位缺陷,咱們但願把注意力集中於相關的失敗測試上。這就是爲何當某個測試所依賴的測試失敗時,PHPUnit 會跳過這個測試。經過利用測試之間的依賴關係,缺陷定位獲得了改進,如例 2.3中所示。

例 2.3: 利用測試之間的依賴關係

<?php
use PHPUnit\Framework\TestCase;

class DependencyFailureTest extends TestCase
{
    public function testOne()
    {
        $this->assertTrue(false);
    }

    /**
     * @depends testOne
     */
    public function testTwo()
    {
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

FS

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) DependencyFailureTest::testOne
Failed asserting that false is true.

/home/sb/DependencyFailureTest.php:6

There was 1 skipped test:

1) DependencyFailureTest::testTwo
This test depends on "DependencyFailureTest::testOne" to pass.


FAILURES!
Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.phpunit --verbose DependencyFailureTest

測試能夠使用多個 @depends 標註。PHPUnit 不會更改測試的運行順序,所以你須要自行保證某個測試所依賴的全部測試均出現於這個測試以前。

擁有多個 @depends 標註的測試,其第一個參數是第一個生產者提供的基境,第二個參數是第二個生產者提供的基境,以此類推。參見例 2.4

例 2.4: 有多重依賴的測試

<?php
use PHPUnit\Framework\TestCase;

class MultipleDependenciesTest extends TestCase
{
    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     */
    public function testConsumer()
    {
        $this->assertEquals(
            ['first', 'second'],
            func_get_args()
        );
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

...

Time: 0 seconds, Memory: 3.25Mb

OK (3 tests, 3 assertions)phpunit --verbose MultipleDependenciesTest

數據供給器

測試方法能夠接受任意參數。這些參數由數據供給器方法(在 例 2.5中,是 additionProvider() 方法)提供。用 @dataProvider 標註來指定使用哪一個數據供給器方法。

數據供給器方法必須聲明爲 public,其返回值要麼是一個數組,其每一個元素也是數組;要麼是一個實現了 Iterator 接口的對象,在對它進行迭代時每步產生一個數組。每一個數組都是測試數據集的一部分,將以它的內容做爲參數來調用測試方法。

例 2.5: 使用返回數組的數組的數據供給器

<?php
use PHPUnit\Framework\TestCase;

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 (1, 1, 3)
Failed asserting that 2 matches expected 3.

/home/sb/DataTest.php:9

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.phpunit DataTest

當使用到大量數據集時,最好逐個用字符串鍵名對其命名,避免用默認的數字鍵名。這樣輸出信息會更加詳細些,其中將包含打斷測試的數據集所對應的名稱。

例 2.6: 使用帶有命名數據集的數據供給器

<?php
use PHPUnit\Framework\TestCase;

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return [
            'adding zeros'  => [0, 0, 0],
            'zero plus one' => [0, 1, 1],
            'one plus zero' => [1, 0, 1],
            'one plus one'  => [1, 1, 3]
        ];
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set "one plus one" (1, 1, 3)
Failed asserting that 2 matches expected 3.

/home/sb/DataTest.php:9

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.phpunit DataTest

例 2.7: 使用返回迭代器對象的數據供給器

<?php
use PHPUnit\Framework\TestCase;

require 'CsvFileIterator.php';

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return new CsvFileIterator('data.csv');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 ('1', '1', '3')
Failed asserting that 2 matches expected '3'.

/home/sb/DataTest.php:11

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.phpunit DataTest

例 2.8: CsvFileIterator 類

<?php
use PHPUnit\Framework\TestCase;

class CsvFileIterator implements Iterator {
    protected $file;
    protected $key = 0;
    protected $current;

    public function __construct($file) {
        $this->file = fopen($file, 'r');
    }

    public function __destruct() {
        fclose($this->file);
    }

    public function rewind() {
        rewind($this->file);
        $this->current = fgetcsv($this->file);
        $this->key = 0;
    }

    public function valid() {
        return !feof($this->file);
    }

    public function key() {
        return $this->key;
    }

    public function current() {
        return $this->current;
    }

    public function next() {
        $this->current = fgetcsv($this->file);
        $this->key++;
    }
}
?>

若是測試同時從 @dataProvider 方法和一個或多個 @depends 測試接收數據,那麼來自於數據供給器的參數將先於來自所依賴的測試的。來自於所依賴的測試的參數對於每一個數據集都是同樣的。參見例 2.9

例 2.9: 在同一個測試中組合使用 @depends 和 @dataProvider

<?php
use PHPUnit\Framework\TestCase;

class DependencyAndDataProviderComboTest extends TestCase
{
    public function provider()
    {
        return [['provider1'], ['provider2']];
    }

    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     * @dataProvider provider
     */
    public function testConsumer()
    {
        $this->assertEquals(
            ['provider1', 'first', 'second'],
            func_get_args()
        );
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 3.50Mb

There was 1 failure:

1) DependencyAndDataProviderComboTest::testConsumer with data set #1 ('provider2')
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
-    0 => 'provider1'
+    0 => 'provider2'
1 => 'first'
2 => 'second'
)

/home/sb/DependencyAndDataProviderComboTest.php:31

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
phpunit --verbose DependencyAndDataProviderComboTest

注意

若是一個測試依賴於另一個使用了數據供給器的測試,僅當被依賴的測試至少能在一組數據上成功時,依賴於它的測試纔會運行。使用了數據供給器的測試,其運行結果是沒法注入到依賴於此測試的其餘測試中的。

注意

全部的數據供給器方法的執行都是在對 setUpBeforeClass 靜態方法的調用和第一次對 setUp 方法的調用以前完成的。所以,沒法在數據供給器中使用建立於這兩個方法內的變量。這是必須的,這樣 PHPUnit 才能計算測試的總數量。

對異常進行測試

例 2.10展現瞭如何用 @expectException 標註來測試被測代碼中是否拋出了異常。

例 2.10: 使用 expectException() 方法

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    public function testException()
    {
        $this->expectException(InvalidArgumentException::class);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ExceptionTest

除了 expectException() 方法外,還有 expectExceptionCode()expectExceptionMessage() 和 expectExceptionMessageRegExp() 方法能夠用於爲被測代碼所拋出的異常創建預期。

或者,也能夠用 @expectedException@expectedExceptionCode@expectedExceptionMessage 和 @expectedExceptionMessageRegExp 標註來爲被測代碼所拋出的異常創建預期。例 2.11展現了一個範例。

例 2.11: 使用 @expectedException 標註

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    /**
     * @expectedException InvalidArgumentException
     */
    public function testException()
    {
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ExceptionTest

對 PHP 錯誤進行測試

默認狀況下,PHPUnit 將測試在執行中觸發的 PHP 錯誤、警告、通知都轉換爲異常。利用這些異常,就能夠,好比說,預期測試將觸發 PHP 錯誤,如例 2.12所示。

注意

PHP 的 error_reporting 運行時配置會對 PHPUnit 將哪些錯誤轉換爲異常有所限制。若是在這個特性上碰到問題,請確認 PHP 的配置中沒有抑制想要測試的錯誤類型。

例 2.12: 用 @expectedException 來預期 PHP 錯誤

<?php
use PHPUnit\Framework\TestCase;

class ExpectedErrorTest extends TestCase
{
    /**
     * @expectedException PHPUnit_Framework_Error
     */
    public function testFailingInclude()
    {
        include 'not_existing_file.php';
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)phpunit -d error_reporting=2 ExpectedErrorTest

PHPUnit_Framework_Error_Notice 和 PHPUnit_Framework_Error_Warning 分別表明 PHP 通知與 PHP 警告。

注意

對異常進行測試是越明確越好的。對太籠統的類進行測試有可能致使不良反作用。所以,再也不容許用 @expectedException 或 setExpectedException() 對 Exception 類進行測試。

若是測試依靠會觸發錯誤的 PHP 函數,例如 fopen ,有時候在測試中使用錯誤抑制符會頗有用。經過抑制住錯誤通知,就能對返回值進行檢查,不然錯誤通知將會致使拋出PHPUnit_Framework_Error_Notice

例 2.13: 對會引起PHP 錯誤的代碼的返回值進行測試

<?php
use PHPUnit\Framework\TestCase;

class ErrorSuppressionTest extends TestCase
{
    public function testFileWriting() {
        $writer = new FileWriter;
        $this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff'));
    }
}
class FileWriter
{
    public function write($file, $content) {
        $file = fopen($file, 'w');
        if($file == false) {
            return false;
        }
        // ...
    }
}

?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

.

Time: 1 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)phpunit ErrorSuppressionTest


若是不使用錯誤抑制符,此測試將會失敗,並報告 fopen(/is-not-writeable/file): failed to open stream: No such file or directory

對輸出進行測試

有時候,想要斷言(好比說)某方法的運行過程當中生成了預期的輸出(例如,經過 echo 或 print)。PHPUnit\Framework\TestCase 類使用 PHP 的 輸出緩衝 特性來爲此提供必要的功能支持。

例 2.14展現瞭如何用 expectOutputString() 方法來設定所預期的輸出。若是沒有產生預期的輸出,測試將計爲失敗。

例 2.14: 對函數或方法的輸出進行測試

<?php
use PHPUnit\Framework\TestCase;

class OutputTest extends TestCase
{
    public function testExpectFooActualFoo()
    {
        $this->expectOutputString('foo');
        print 'foo';
    }

    public function testExpectBarActualBaz()
    {
        $this->expectOutputString('bar');
        print 'baz';
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

.F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) OutputTest::testExpectBarActualBaz
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'


FAILURES!
Tests: 2, Assertions: 2, Failures: 1.phpunit OutputTest

表 2.1中列舉了用於對輸出進行測試的各類方法。

表 2.1. 用於對輸出進行測試的方法

方法 含義
void expectOutputRegex(string $regularExpression) 設置輸出預期爲輸出應當匹配正則表達式 $regularExpression
void expectOutputString(string $expectedString) 設置輸出預期爲輸出應當與 $expectedString 字符串相等。
bool setOutputCallback(callable $callback) 設置回調函數,用來作諸如將實際輸出規範化之類的動做。

注意

在嚴格模式下,自己產生輸出的測試將會失敗。

錯誤相關信息的輸出

當有測試失敗時,PHPUnit 全力提供儘量多的有助於找出問題所在的上下文信息。

例 2.15: 數組比較失敗時生成的錯誤相關信息輸出

<?php
use PHPUnit\Framework\TestCase;

class ArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2,  3, 4, 5, 6],
            [1, 2, 33, 4, 5, 6]
        );
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayDiffTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
     0 => 1
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )

/home/sb/ArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ArrayDiffTest

在這個例子中,數組中只有一個值不一樣,但其餘值也都同時顯示出來,以提供關於錯誤發生的位置的上下文信息。

當生成的輸出很長而難以閱讀時,PHPUnit 將對其進行分割,並在每一個差別附近提供少數幾行上下文信息。

例 2.16: 長數組比較失敗時生成的錯誤相關信息輸出

<?php
use PHPUnit\Framework\TestCase;

class LongArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2,  3, 4, 5, 6],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 33, 4, 5, 6]
        );
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) LongArrayDiffTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
     13 => 2
-    14 => 3
+    14 => 33
     15 => 4
     16 => 5
     17 => 6
 )


/home/sb/LongArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit LongArrayDiffTest

邊緣狀況

當比較失敗時,PHPUnit 爲輸入值創建文本表示,而後以此進行對比。這種實現致使在差別指示中顯示出來的問題可能比實際上存在的多。

這種狀況只出如今對數組或者對象使用 assertEquals 或其餘「弱」比較函數時。

例 2.17: 當使用弱比較時在生成的差別結果中出現的邊緣狀況

<?php
use PHPUnit\Framework\TestCase;

class ArrayWeakComparisonTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2, 3, 4, 5, 6],
            ['1', 2, 33, 4, 5, 6]
        );
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayWeakComparisonTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
-    0 => 1
+    0 => '1'
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )


/home/sb/ArrayWeakComparisonTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ArrayWeakComparisonTest

在這個例子中,第一個索引項中的 1 and '1' 在報告中被視爲不一樣,雖然 assertEquals 認爲這兩個值是匹配的。

第 3 章 命令行測試執行器

PHPUnit 命令行測試執行器可經過 phpunit 命令調用。下面的代碼展現瞭如何用 PHPUnit 命令行測試執行器來運行測試:


PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

..

Time: 0 seconds


OK (2 tests, 2 assertions)phpunit ArrayTest

上面這個調用例子中,PHPUnit 命令行測試執行器將在當前工做目錄中尋找 ArrayTest.php 源文件並加載之。而在此源文件中應當能找到 ArrayTest 測試用例類,此類中的測試將被執行。

對於每一個測試的運行,PHPUnit 命令行工具輸出一個字符來指示進展:

.

當測試成功時輸出。

F

當測試方法運行過程當中一個斷言失敗時輸出。

E

當測試方法運行過程當中產生一個錯誤時輸出。

R

當測試被標記爲有風險時輸出(參見第 6 章)。

S

當測試被跳過期輸出(參見第 7 章)。

I

當測試被標記爲不完整或未實現時輸出(參見第 7 章)。

PHPUnit 區分 敗(failure)錯誤(error)。失敗指的是被違背了的 PHPUnit 斷言,例如一個失敗的 assertEquals() 調用。錯誤指的是意料以外的異常(exception)或 PHP 錯誤。這種差別已被證實在某些時候是很是有用的,由於錯誤每每比失敗更容易修復。若是獲得了一個很是長的問題列表,那麼最好先對付錯誤,當錯誤所有修復了以後再試一次瞧瞧還有沒有失敗。

命令行選項

讓咱們來瞧瞧如下代碼中命令行測試運行器的各類選項:


PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

Usage: phpunit [options] UnitTest [UnitTest.php]
       phpunit [options] <directory>

Code Coverage Options:

  --coverage-clover <file>  Generate code coverage report in Clover XML format.
  --coverage-crap4j <file>  Generate code coverage report in Crap4J XML format.
  --coverage-html <dir>     Generate code coverage report in HTML format.
  --coverage-php <file>     Export PHP_CodeCoverage object to file.
  --coverage-text=<file>    Generate code coverage report in text format.
                            Default: Standard output.
  --coverage-xml <dir>      Generate code coverage report in PHPUnit XML format.

Logging Options:

  --log-junit <file>        Log test execution in JUnit XML format to file.
  --log-tap <file>          Log test execution in TAP format to file.
  --log-json <file>         Log test execution in JSON format.
  --testdox-html <file>     Write agile documentation in HTML format to file.
  --testdox-text <file>     Write agile documentation in Text format to file.

Test Selection Options:

  --filter <pattern>        Filter which tests to run.
  --testsuite <pattern>     Filter which testsuite to run.
  --group ...               Only runs tests from the specified group(s).
  --exclude-group ...       Exclude tests from the specified group(s).
  --list-groups             List available test groups.
  --test-suffix ...         Only search for test in files with specified
                            suffix(es). Default: Test.php,.phpt

Test Execution Options:

  --report-useless-tests    Be strict about tests that do not test anything.
  --strict-coverage         Be strict about unintentionally covered code.
  --strict-global-state     Be strict about changes to global state
  --disallow-test-output    Be strict about output during tests.
  --enforce-time-limit      Enforce time limit based on test size.
  --disallow-todo-tests     Disallow @todo-annotated tests.

  --process-isolation       Run each test in a separate PHP process.
  --no-globals-backup       Do not backup and restore $GLOBALS for each test.
  --static-backup           Backup and restore static attributes for each test.

  --colors=<flag>           Use colors in output ("never", "auto" or "always").
  --columns <n>             Number of columns to use for progress output.
  --columns max             Use maximum number of columns for progress output.
  --stderr                  Write to STDERR instead of STDOUT.
  --stop-on-error           Stop execution upon first error.
  --stop-on-failure         Stop execution upon first error or failure.
  --stop-on-risky           Stop execution upon first risky test.
  --stop-on-skipped         Stop execution upon first skipped test.
  --stop-on-incomplete      Stop execution upon first incomplete test.
  -v|--verbose              Output more verbose information.
  --debug                   Display debugging information during test execution.

  --loader <loader>         TestSuiteLoader implementation to use.
  --repeat <times>          Runs the test(s) repeatedly.
  --tap                     Report test execution progress in TAP format.
  --testdox                 Report test execution progress in TestDox format.
  --printer <printer>       TestListener implementation to use.

Configuration Options:

  --bootstrap <file>        A "bootstrap" PHP file that is run before the tests.
  -c|--configuration <file> Read configuration from XML file.
  --no-configuration        Ignore default configuration file (phpunit.xml).
  --include-path <path(s)>  Prepend PHP's include_path with given path(s).
  -d key[=value]            Sets a php.ini value.

Miscellaneous Options:

  -h|--help                 Prints this usage information.
  --version                 Prints the version and exits.phpunit --help
phpunit UnitTest

運行由 UnitTest 類提供的測試。這個類應當在 UnitTest.php 源文件中聲明。

UnitTest 這個類必須知足如下二個條件之一:要麼它繼承自 PHPUnit\Framework\TestCase;要麼它提供 public static suite() 方法,這個方法返回一個 PHPUnit_Framework_Test 對象,好比,一個 PHPUnit_Framework_TestSuite 類的實例。

phpunit UnitTest UnitTest.php

運行由 UnitTest 類提供的測試。這個類應當在指定的源文件中聲明。

--coverage-clover

爲運行的測試生成帶有代碼覆蓋率信息的 XML 格式的日誌文件。更多細節請參見第 13 章

請注意,此功能僅當安裝了 tokenizer 和 Xdebug 這兩個 PHP 擴展後纔可用。

--coverage-crap4j

生成 Crap4j 格式的代碼覆蓋率報告。更多細節請參見第 11 章

請注意,此功能僅當安裝了 tokenizer 和 Xdebug 這兩個 PHP 擴展後纔可用。

--coverage-html

生成 HTML 格式的代碼覆蓋率報告。更多細節請參見 第 11 章

請注意,此功能僅當安裝了 tokenizer 和 Xdebug 這兩個 PHP 擴展後纔可用。

--coverage-php

生成一個序列化後的 PHP_CodeCoverage 對象,此對象含有代碼覆蓋率信息。

請注意,此功能僅當安裝了 tokenizer 和 Xdebug 這兩個 PHP 擴展後纔可用。

--coverage-text

爲運行的測試以人們可讀的格式生成帶有代碼覆蓋率信息的日誌文件或命令行輸出。更多細節請參見 第 13 章

請注意,此功能僅當安裝了 tokenizer 和 Xdebug 這兩個 PHP 擴展後纔可用。

--log-junit

爲運行的測試生成 JUnit XML 格式的日誌文件。更多細節請參見 第 13 章

--log-tap

爲運行的測試生成 Test Anything Protocol (TAP) 格式的日誌文件。更多細節請參見第 13 章

--log-json

生成 JSON 格式的日誌文件。更多細節請參見第 13 章

--testdox-html 和 --testdox-text

爲運行的測試以 HTML 或純文本格式生成敏捷文檔。更多細節請參見 第 12 章

--filter

只運行名稱與給定模式匹配的測試。若是模式未閉合包裹於分隔符,PHPUnit 將用 / 分隔符對其進行閉合包裹。

測試名稱將以如下格式之一進行匹配:

TestNamespace\TestCaseClass::testMethod

默認的測試名稱格式等價於在測試方法內使用 __METHOD__ 魔術常量。

TestNamespace\TestCaseClass::testMethod with data set #0

當測試擁有數據供給器時,數據的每輪迭代都會將其當前索引附加在默認測試名稱結尾處。

TestNamespace\TestCaseClass::testMethod with data set "my named data"

當測試擁有使用命名數據集的數據供給器時,數據的每輪迭代都會將當前名稱附加在默認測試名稱結尾處。命名數據集的例子參見例 3.1

例 3.1: 命名數據集

<?php
use PHPUnit\Framework\TestCase;

namespace TestNamespace;

class TestCaseClass extends TestCase
{
    /**
     * @dataProvider provider
     */
    public function testMethod($data)
    {
        $this->assertTrue($data);
    }

    public function provider()
    {
        return [
            'my named data' => [true],
            'my data'       => [true]
        ];
    }
}
?>
/path/to/my/test.phpt

對於 PHPT 測試,其測試名稱是文件系統路徑。

有效的過濾器模式例子參見例 3.2

例 3.2: 過濾器模式例子

  • --filter 'TestNamespace\\TestCaseClass::testMethod'

  • --filter 'TestNamespace\\TestCaseClass'

  • --filter TestNamespace

  • --filter TestCaseClass

  • --filter testMethod

  • --filter '/::testMethod .*"my named data"/'

  • --filter '/::testMethod .*#5$/'

  • --filter '/::testMethod .*#(5|6|7)$/'


在匹配數據供給器時有一些額外的快捷方式,參見例 3.3

例 3.3: 過濾器的快捷方式

  • --filter 'testMethod#2'

  • --filter 'testMethod#2-4'

  • --filter '#2'

  • --filter '#2-4'

  • --filter 'testMethod@my named data'

  • --filter 'testMethod@my.*data'

  • --filter '@my named data'

  • --filter '@my.*data'

--testsuite

只運行名稱與給定模式匹配的測試套件。

--group

只運行來自指定分組(能夠多個)的測試。能夠用 @group 標註爲測試標記其所屬的分組。

@author 標註是 @group 的一個別名,容許按做者來篩選測試。

--exclude-group

排除來自指定分組(能夠多個)的測試。能夠用 @group 標註爲測試標記其所屬的分組。

--list-groups

列出全部有效的測試分組。

--test-suffix

只查找文件名以指定後綴(能夠多個)結尾的測試文件。

--report-useless-tests

更嚴格對待事實上不測試任何內容的測試。詳情參見 第 6 章

--strict-coverage

更嚴格對待意外的代碼覆蓋。詳情參見 第 6 章

--strict-global-state

更嚴格對待全局狀態篡改。詳情參見 第 6 章

--disallow-test-output

更嚴格對待測試執行期間產生的輸出。詳情參見第 6 章

--disallow-todo-tests

不執行文檔註釋塊中含有 @todo 標註的測試。

--enforce-time-limit

根據測試規模對其加上執行時長限制。詳情參見第 6 章

--process-isolation

每一個測試都在獨立的PHP進程中運行。

--no-globals-backup

不要備份並還原 $GLOBALS。更多細節請參見「全局狀態」一節

--static-backup

備份並還原用戶定義的類中的靜態屬性。更多細節請參見「全局狀態」一節

--colors

使用彩色輸出。Windows下,用 ANSICON 或 ConEmu

本選項有三個可能的值:

  • never: 徹底不使用彩色輸出。當未使用 --colors 選項時,這是默認值。

  • auto: 若是當前終端不支持彩色、或者輸出被管道輸出至其餘命令、或輸出被重定向至文件時,不使用彩色輸出,其他狀況使用彩色。

  • always: 老是使用彩色輸出,即便當前終端不支持彩色、輸出被管道輸出至其餘命令、或輸出被重定向至文件。

當使用了 --colors 選項但未指定任何值時,將選擇 auto 作爲其值。

--columns

定義輸出所使用的列數。若是將其值定義爲 max,則使用當前終端所支持的最大列數。

--stderr

選擇輸出到 STDERR 而非 STDOUT.

--stop-on-error

首次錯誤出現後中止執行。

--stop-on-failure

首次錯誤或失敗出現後中止執行。

--stop-on-risky

首次碰到有風險的測試時中止執行。

--stop-on-skipped

首次碰到跳過的測試時中止執行。

--stop-on-incomplete

首次碰到不完整的測試時中止執行。

--verbose

輸出更詳盡的信息,例如不完整或者跳過的測試的名稱。

--debug

輸出調試信息,例如當一個測試開始執行時輸出其名稱。

--loader

指定要使用的 PHPUnit_Runner_TestSuiteLoader 實現。

標準的測試套件加載器將在當前工做目錄和 PHP 的 include_path 配置指令中指定的每一個目錄內查找源文件。諸如 Project_Package_Class 這樣的類名對應的源文件名爲Project/Package/Class.php

--repeat

將測試重複運行指定次數。

--tap

使用 Test Anything Protocol (TAP) 報告測試進度。更多細節請參見 第 13 章

--testdox

將測試進度以敏捷文檔方式報告。更多細節請參見 第 12 章

--printer

指定要使用的結果輸出器(printer)。輸出器類必須擴展 PHPUnit_Util_Printer 而且實現 PHPUnit_Framework_TestListener 接口。

--bootstrap

在測試前先運行一個 "bootstrap" PHP 文件。

--configuration-c

從 XML 文件中讀取配置信息。更多細節請參見附錄 C

若是 phpunit.xml 或 phpunit.xml.dist (按此順序)存在於當前工做目錄而且使用 --configuration,將自動今後文件中讀取配置。

--no-configuration

忽略當前工做目錄下的 phpunit.xml 與 phpunit.xml.dist

--include-path

向 PHP 的 include_path 開頭添加指定路徑(能夠多個)。

-d

設置指定的 PHP 配置選項的值。

注意

請注意,從 4.8 開始,選項不能放在參數以後。

第 4 章 基境(fixture)

在編寫測試時,最費時的部分之一是編寫代碼來將整個場景設置成某個已知的狀態,並在測試結束後將其復原到初始狀態。這個已知的狀態稱爲測試的 基境(fixture)

例 2.1中,基境十分簡單,就是存儲在 $stack 變量中的數組。然而,絕大多數時候基境均遠比一個簡單數組要複雜,用於創建基境的代碼量也會隨之增加。測試的真正內容就被淹沒於創建基境帶來的干擾中。當編寫多個須要相似基境的測試時這個問題就變得更糟糕了。若是沒有來自於測試框架的幫助,就不得不在寫每個測試時都將創建基境的代碼重複一次。

PHPUnit 支持共享創建基境的代碼。在運行某個測試方法前,會調用一個名叫 setUp() 的模板方法。setUp() 是建立測試所用對象的地方。當測試方法運行結束後,無論是成功仍是失敗,都會調用另一個名叫 tearDown() 的模板方法。tearDown() 是清理測試所用對象的地方。

例 2.2中,咱們在測試之間運用生產者-消費者關係來共享基境。這並不是老是預期的方式,甚至有時是不可能的。例 4.1展現了另一個編寫測試 StackTest 的方式。在這個方式中,再也不重用基境自己,而是重用創建基境的代碼。首先聲明一個實例變量,$stack,用來替代方法內的局部變量。而後把 array 基境的創建放到 setUp() 方法中。最後,從測試方法中去除冗餘代碼,在assertEquals() 斷言方法中使用新引入的實例變量 $this->stack替代方法內的局部變量 $stack

例 4.1: 用 setUp() 創建棧的基境

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    protected $stack;

    protected function setUp()
    {
        $this->stack = [];
    }

    public function testEmpty()
    {
        $this->assertTrue(empty($this->stack));
    }

    public function testPush()
    {
        array_push($this->stack, 'foo');
        $this->assertEquals('foo', $this->stack[count($this->stack)-1]);
        $this->assertFalse(empty($this->stack));
    }

    public function testPop()
    {
        array_push($this->stack, 'foo');
        $this->assertEquals('foo', array_pop($this->stack));
        $this->assertTrue(empty($this->stack));
    }
}
?>

測試類的每一個測試方法都會運行一次 setUp() 和 tearDown() 模板方法(同時,每一個測試方法都是在一個全新的測試類實例上運行的)。

另外,setUpBeforeClass() 與 tearDownAfterClass() 模板方法將分別在測試用例類的第一個測試運行以前和測試用例類的最後一個測試運行以後調用。

下面這個例子中展現了測試用例類中全部可用的模板方法。

例 4.2: 展現全部可用模板方法的例子

<?php
use PHPUnit\Framework\TestCase;

class TemplateMethodsTest extends TestCase
{
    public static function setUpBeforeClass()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function setUp()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function assertPreConditions()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    public function testOne()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
        $this->assertTrue(true);
    }

    public function testTwo()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
        $this->assertTrue(false);
    }

    protected function assertPostConditions()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function tearDown()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    public static function tearDownAfterClass()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function onNotSuccessfulTest(Exception $e)
    {
        fwrite(STDOUT, __METHOD__ . "\n");
        throw $e;
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

TemplateMethodsTest::setUpBeforeClass
TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testOne
TemplateMethodsTest::assertPostConditions
TemplateMethodsTest::tearDown
.TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testTwo
TemplateMethodsTest::tearDown
TemplateMethodsTest::onNotSuccessfulTest
FTemplateMethodsTest::tearDownAfterClass


Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) TemplateMethodsTest::testTwo
Failed asserting that <boolean:false> is true.
/home/sb/TemplateMethodsTest.php:30

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.phpunit TemplateMethodsTest

setUp() 多 tearDown() 少

理論上說,setUp() 和 tearDown() 是精確對稱的,可是實踐中並不是如此。實際上,只有在 setUp() 中分配了諸如文件或套接字之類的外部資源時才須要實現 tearDown() 。若是 setUp() 中只建立純 PHP 對象,一般能夠略過 tearDown()。不過,若是在 setUp() 中建立了大量對象,你可能想要在 tearDown() 中 unset() 指向這些對象的變量,這樣它們就能夠被垃圾回收機制回收掉。對測試用例對象的垃圾回收動做則是不可預知的。

變體

若是兩個基境創建工做略有不一樣的測試該怎麼辦?有兩種可能:

  • 若是兩個 setUp() 代碼僅有微小差別,把有差別的代碼內容從 setUp() 移到測試方法內。

  • 若是兩個 setUp() 是確實不同,那麼須要另一個測試用例類。參考基境創建工做的不一樣之處來命名這個類。

基境共享

有幾個好的理由來在測試之間共享基境,可是大部分狀況下,在測試之間共享基境的需求都源於某個未解決的設計問題。

一個有實際意義的多測試間共享基境的例子是數據庫鏈接:只登陸數據庫一次,而後重用此鏈接,而不是每一個測試都創建一個新的數據庫鏈接。這樣能加快測試的運行。

例 4.3用 setUpBeforeClass() 和 tearDownAfterClass() 模板方法來分別在測試用例類的第一個測試以前和最後一個測試以後鏈接與斷開數據庫。

例 4.3: 在同一個測試套件內的不一樣測試之間共享基境

<?php
use PHPUnit\Framework\TestCase;

class DatabaseTest extends TestCase
{
    protected static $dbh;

    public static function setUpBeforeClass()
    {
        self::$dbh = new PDO('sqlite::memory:');
    }

    public static function tearDownAfterClass()
    {
        self::$dbh = null;
    }
}
?>

須要反覆強調的是:在測試之間共享基境會下降測試的價值。潛在的設計問題是對象之間並不是鬆散耦合。若是解決掉潛在的設計問題並使用樁件(stub)(參見第 9 章)來編寫測試,就能達成更好的結果,而不是在測試之間產生運行時依賴並錯過改進設計的機會。

全局狀態

使用單件(singleton)的代碼很難測試。使用全局變量的代碼也同樣。一般狀況下,欲測代碼和全局變量之間會強烈耦合,而且其建立沒法控制。另一個問題是,一個測試對全局變量的改變可能會破壞另一個測試。

在 PHP 中,全局變量是這樣運做的:

  • 全局變量 $foo = 'bar'; 其實是存儲爲 $GLOBALS['foo'] = 'bar'; 的。

  • $GLOBALS這個變量是一種被稱爲超全局變量的變量。

  • 超全局變量是一種在任何變量做用域中都老是可用的內建變量。

  • 在函數或者方法的變量做用域中,要訪問全局變量 $foo,能夠直接訪問 $GLOBALS['foo'],或者用 global $foo; 來建立一個引用全局變量的局部變量。

除了全局變量,類的靜態屬性也是一種全局狀態。

默認狀況下,PHPUnit 用一種更改全局變量與超全局變量($GLOBALS$_ENV$_POST$_GET$_COOKIE$_SERVER$_FILES$_REQUEST)不會影響到其餘測試的方式來運行全部測試。同時,還能夠選擇將這種隔離擴展到類的靜態屬性。

注意

對全局變量和類的靜態屬性的備份與還原操做使用了 serialize() 與 unserialize()

某些類的實例對象(好比 PDO)沒法序列化,所以若是把這樣一個對象存放在好比說 $GLOBALS 數組內時,備份操做就會出問題。

「@backupGlobals」一節中所討論的 @backupGlobals 標註能夠用來控制對全局變量的備份與還原操做。另外,還能夠提供一個全局變量的黑名單,黑名單中的全局變量將被排除於備份與還原操做以外,就像這樣:

class MyTest extends TestCase
{
    protected $backupGlobalsBlacklist = ['globalVariable'];

    // ...
}

 

注意

在方法(例如 setUp() 方法)內對 $backupGlobalsBlacklist 屬性進行設置是無效的。

在 「@backupStaticAttributes」一節 中提到的 @backupStaticAttributes 標註能夠用於在每一個測試以前備份全部已聲明類的靜態屬性值並在其後恢復。

它所處理的並不僅是測試類自身,而是在測試開始時已聲明的全部類。它只做用於靜態類屬性,不做用於函數內聲明的靜態變量。

注意

只有啓用了 @backupStaticAttributes 的測試方法纔會在方法以前執行此操做。若是在此以前運行的某個沒有啓用 @backupStaticAttributes 的測試方法改變了靜態屬性的值,那麼被備份及還原的將會是這個改變後的值——而非初始聲明時提供的默認值。PHP 並不額外記錄任何靜態變量的聲明時提供的初始默認值。

一樣的狀況也發生於測試內部新加載/聲明的類的靜態屬性上。它們也沒法在測試結束以後復原爲聲明時提供的原始默認值,由於無從得知這些默認值。這些被修改過的值會泄漏到後繼測試中。

對單元測試而言,推薦在 setUp() 中顯式的重置測試中使用到的靜態屬性(最好同時在 tearDown() 中執行重置,這樣就保證不會影響到後繼的測試)。

能夠提供黑名單來將靜態屬性從備份與還原操做中排除出去:

class MyTest extends TestCase
{
    protected $backupStaticAttributesBlacklist = [
        'className' => ['attributeName']
    ];

    // ...
}

 

注意

在方法(例如 setUp() )內對 $backupStaticAttributesBlacklist 屬性進行設置是無效的。

第 5 章 組織測試

PHPUnit 的目標之一是測試應當可組合:咱們但願能將任意數量的測試以任意組合方式運行,例如,整個項目的全部測試,或者項目中的某個組件內的全部類的測試,又或者僅僅某單個類的測試。

PHPUnit 支持好幾種不一樣的方式來組織測試以及將它們編排組合成測試套件。本章介紹了最經常使用的方法。

用文件系統來編排測試套件

編排測試套件的各類方式中,最簡單的大概就是把全部測試用例源文件放在一個測試目錄中。經過對測試目錄進行遞歸遍歷,PHPUnit 能自動發現並運行測試。

如今來看看 sebastianbergmann/money 這個庫的測試套件。在這個項目的目錄結構中,能夠看到 tests 目錄下的測試用例類鏡像了 src 目錄下被測系統(SUT, System Under Test)的包(package)與類(class)的結構:

src                                 tests
`-- Currency.php                    `-- CurrencyTest.php
`-- IntlFormatter.php               `-- IntlFormatterTest.php
`-- Money.php                       `-- MoneyTest.php
`-- autoload.php

要運行這個庫的所有測試,只要將 PHPUnit 命令行測試執行器指向測試目錄便可:


PHPUnit 6.1.0 by Sebastian Bergmann.

.................................

Time: 636 ms, Memory: 3.50Mb

OK (33 tests, 52 assertions)phpunit --bootstrap src/autoload.php tests

注意

當 PHPUnit 命令行測試執行器指向一個目錄時,它會在目錄下查找 *Test.php 文件。

若是隻想運行在 CurrencyTest 文件中的 tests/CurrencyTest.php 測試用例類中聲明的測試,能夠使用以下命令:


PHPUnit 6.1.0 by Sebastian Bergmann.

........

Time: 280 ms, Memory: 2.75Mb

OK (8 tests, 8 assertions)phpunit --bootstrap src/autoload.php tests/CurrencyTest

若是想要對運行哪些測試有更細粒度的控制,能夠使用 --filter 選項:


PHPUnit 6.1.0 by Sebastian Bergmann.

..

Time: 167 ms, Memory: 3.00Mb

OK (2 test, 2 assertions)phpunit --bootstrap src/autoload.php --filter testObjectCanBeConstructedForValidConstructorArgument tests

注意

這種方法的缺點是沒法控制測試的運行順序。這可能致使測試的依賴關係方面的問題,參見 「測試的依賴關係」一節。在下一節中,能夠看到如何用 XML 配置文件來明確指定測試的執行順序。

用 XML 配置來編排測試套件

PHPUnit的 XML 配置文件(附錄 C)也能夠用於編排測試套件。例 5.1展現了一個最小化的 phpunit.xml 例子,它將在遞歸遍歷 tests 時添加全部在 *Test.php 文件中找到的 *Test 類。

例 5.1: 用 XML 配置來編排測試套件

<phpunit bootstrap="src/autoload.php">
  <testsuites>
    <testsuite name="money">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>

若是 phpunit.xml 或 phpunit.xml.dist (按此順序)存在於當前工做目錄而且使用 --configuration,將自動今後文件中讀取配置。

能夠明確指定測試的執行順序:

例 5.2: 用 XML 配置來編排測試套件

<phpunit bootstrap="src/autoload.php">
  <testsuites>
    <testsuite name="money">
      <file>tests/IntlFormatterTest.php</file>
      <file>tests/MoneyTest.php</file>
      <file>tests/CurrencyTest.php</file>
    </testsuite>
  </testsuites>
</phpunit>

第 6 章 有風險的測試

在執行測試時,PHPUnit 能夠進行一些額外的檢查,見下文。

無用測試

PHPUnit 能夠更嚴格對待事實上不測試任何內容的測試。此項檢查能夠用命令行選項 --report-useless-tests 或在 PHPUnit 的 XML 配置文件中設置beStrictAboutTestsThatDoNotTestAnything="true" 來啓用。

在啓用本項檢查後,若是某個測試未進行任何斷言,它將被標記爲有風險。仿件對象中的預期和諸如 @expectedException 這樣的標註一樣視爲斷言。

意外的代碼覆蓋

PHPUnit 能夠更嚴格對待意外的代碼覆蓋。此項檢查能夠用命令行選項 --strict-coverage 或在 PHPUnit 的 XML 配置文件中設置 checkForUnintentionallyCoveredCode="true" 來啓用。

在啓用本項檢查後,若是某個帶有 @covers 標註的測試執行了未在 @covers 或 @uses 標註中列出的代碼,它將被標記爲有風險。

測試執行期間產生的輸出

PHPUnit 能夠更嚴格對待測試執行期間產生的輸出。 此項檢查能夠用命令行選項 --disallow-test-output 或在 PHPUnit 的 XML 配置文件中設置 beStrictAboutOutputDuringTests="true" 來啓用。

在啓用本項檢查後,若是某個測試產生了輸出,例如,在測試代碼或被測代碼中調用了 print,它將被標記爲有風險。

測試執行時長的超時限制

若是安裝了 PHP_Invoker 包而且 pcntl 擴展可用,那麼能夠對測試的執行時長進行限制。此時間限制能夠用命令行選項 --enforce-time-limit 或在 PHPUnit 的 XML 配置文件中設置beStrictAboutTestSize="true" 來啓用。

帶有 @large 標註的測試若是執行時間超過60秒將視爲失敗。此超時限制能夠經過XML配置文件中的 timeoutForLargeTests 屬性進行配置。

帶有 @medium 標註的測試若是執行時間超過10秒將視爲失敗。此超時限制能夠經過XML配置文件中的 timeoutForMediumTests 屬性進行配置。

沒有 @medium 或 @large 標註的測試都將視同爲帶有 @small標註,這類測試若是執行時間超過1秒將視爲失敗。此超時限制能夠經過XML配置文件中的 timeoutForSmallTests 屬性進行配置。

全局狀態篡改

PHPUnit 能夠更嚴格對待篡改全局狀態的測試。此項檢查能夠用命令行選項 --strict-global-state 或在 PHPUnit 的 XML 配置文件中設置 beStrictAboutChangesToGlobalState="true" 來啓用。

第 7 章 未完成的測試與跳過的測試

未完成的測試

開始寫新的測試用例類時,可能想從寫下空測試方法開始,好比:

public function testSomething()
{
}

以此來跟蹤須要編寫的測試。空測試的問題是 PHPUnit 框架會將它們解讀爲成功。這種錯誤解讀致使錯誤報告變得毫無用處——沒法分辨出測試是真的成功了仍是根本就未編寫實現。在未實現的測試中調用 $this->fail() 一樣沒啥幫助,由於測試將被解讀爲失敗。這和將未實現的測試解讀爲成功是同樣的錯誤。

假如把成功的測試視爲綠燈、測試失敗視爲紅燈,那麼還額外須要黃燈來將測試標記爲未完成或還沒有實現。PHPUnit_Framework_IncompleteTest 是一個標記接口,用於將測試方法拋出的異常標記爲測試未完成或目前還沒有實現而致使的結果。PHPUnit_Framework_IncompleteTestError 是這個接口的標準實現。

例 7.1展現了一個測試用例類 SampleTest,它有一個測試方法 testSomething()。經過在測試方法中調用便捷方法 markTestIncomplete()(會自動拋出一個 PHPUnit_Framework_IncompleteTestError異常)將這個測試標記爲未完成。

例 7.1: 將測試標記爲未完成

<?php
use PHPUnit\Framework\TestCase;

class SampleTest extends TestCase
{
    public function testSomething()
    {
        // 可選:若是願意,在這裏隨便測試點什麼。
        $this->assertTrue(true, '這應該已是能正常工做的。');

        // 在這裏中止,並將此測試標記爲未完成。
        $this->markTestIncomplete(
          '此測試目前還沒有實現。'
        );
    }
}
?>

在 PHPUnit 命令行測試執行器的輸出中,未完成的測試記爲 I,以下例所示:


PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

I

Time: 0 seconds, Memory: 3.95Mb

There was 1 incomplete test:

1) SampleTest::testSomething
This test has not been implemented yet.

/home/sb/SampleTest.php:12
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 1, Incomplete: 1.phpunit --verbose SampleTest

表 7.1列舉了用於將測試標記爲未完成的 API。

表 7.1. 用於未完成的測試的 API

方法 含義
void markTestIncomplete() 將當前測試標記爲未完成。
void markTestIncomplete(string $message) 將當前測試標記爲未完成,並用 $message 做爲說明信息。

跳過測試

並不是全部測試都能在任何環境中運行。好比說,考慮這樣一種狀況:一個數據庫抽象層,針對其所支持的各類數據庫系統有多個不一樣的驅動程序。針對 MySQL 驅動程序的測試固然只在 MySQL 服務器可用才能運行。

例 7.2 展現了一個測試用例類 DatabaseTest,它有一個測試方法 testConnection()。在測試用例類的 setUp()模板方法中,檢查了 MySQLi 擴展是否可用,而且在擴展不可用時用 markTestSkipped()方法來跳過此測試。

例 7.2: 跳過某個測試

<?php
use PHPUnit\Framework\TestCase;

class DatabaseTest extends TestCase
{
    protected function setUp()
    {
        if (!extension_loaded('mysqli')) {
            $this->markTestSkipped(
              'MySQLi 擴展不可用。'
            );
        }
    }

    public function testConnection()
    {
        // ...
    }
}
?>

在 PHPUnit 命令行測試執行器的輸出中,被跳過的測試記爲 S,以下例所示:


PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

S

Time: 0 seconds, Memory: 3.95Mb

There was 1 skipped test:

1) DatabaseTest::testConnection
The MySQLi extension is not available.

/home/sb/DatabaseTest.php:9
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 0, Skipped: 1.phpunit --verbose DatabaseTest

表 7.2列舉了用於跳過測試的 API。

表 7.2. 用於跳過測試的 API

方法 含義
void markTestSkipped() 將當前測試標記爲已跳過。
void markTestSkipped(string $message) 將當前測試標記爲已跳過,並用 $message 做爲說明信息。

用 @requires 來跳過測試

除了上述方法,還能夠用 @requires 標註來表達測試用例的一些常見前提條件。

表 7.3. 可能的 @requires 用法

類型 可能的值 範例 其餘範例
PHP 任何 PHP 版本標識符 @requires PHP 5.3.3 @requires PHP 7.1-dev
PHPUnit 任何 PHPUnit 版本標識符 @requires PHPUnit 3.6.3 @requires PHPUnit 4.6
OS 用來對 PHP_OS 進行匹配的正則表達式 @requires OS Linux @requires OS WIN32|WINNT
function 任何對 function_exists 而言有效的參數 @requires function imap_open @requires function ReflectionMethod::setAccessible
extension 任何擴展模塊名,能夠附帶有版本標識符 @requires extension mysqli @requires extension redis 2.2.0

例 7.3: 用 @requires 來跳過測試

<?php
use PHPUnit\Framework\TestCase;

/**
 * @requires extension mysqli
 */
class DatabaseTest extends TestCase
{
    /**
     * @requires PHP 5.3
     */
    public function testConnection()
    {
        // 測試要求有 mysqli 擴展,而且 PHP >= 5.3
    }

    // ... 全部其餘要求有 mysqli 擴展的測試
}
?>

若是使用了某種在特定版本的 PHP 下沒法編譯的語法,請在此章節內查找 XML 配置信息中關於版本依賴的信息:「測試套件」一節

第 8 章 數據庫測試

在各類編程語言中,許多入門與中級的單元測試範例都暗示着這樣一種信息:很容易用簡單的測試來對應用程序的邏輯進行測試。可是對於以數據庫爲中心的應用程序而言,這與現實相去甚遠。一旦開始使用諸如 WordPress、TYPO三、或 Symfony(配合 Doctrine 或 Propel)之類的東西,就很容易在用 PHPUnit 時碰到超多問題:正是因爲這些庫和數據庫之間實在耦合的太緊密了。

注意

請確保已經安裝了 PHP 擴展模塊 pdo 和與數據庫對應的特定擴展,好比 pdo_mysql。不然如下範例是沒法運行的。

你大概會在平常工做面對的項目中經歷這一幕。你打算把你那或生疏或純熟的 PHPUnit 技能用到工做中去,結果被如下問題之一卡住了:

  1. 待測方法執行了一個至關大的 JOIN 操做,而且獲得的數據用於計算某些重要的結果。

  2. 業務邏輯中混合執行了 SELECT、INSERT、UPDATE 和 DELETE 語句。

  3. 爲了給待測方法創建合理的初始數據,須要在兩個以上(可能遠超過)表裏設置測試數據。

DbUnit 擴展大大簡化了爲測試設置數據庫的操做,而且能夠在對數據執行了一系列操做以後驗證數據庫的內容。

數據庫測試所支持的供應商

DbUnit 目前支持 MySQL、PostgreSQL、Oracle 和 SQLite。經過集成 Zend Framework 或 Doctrine 2,也能夠訪問其餘數據庫系統,好比 IBM DB2 或者 Microsoft SQL Server。

數據庫測試的難點

爲何全部單元測試的範例都不包含數據庫交互?這裏有個很好的理由:這類測試的創建和維護都很複雜。對數據庫進行測試時,須要考慮如下這些變數:

  • 數據庫和表

  • 向表中插入測試所須要的行

  • 測試運行完畢後驗證數據庫的狀態

  • 每一個新測試都要清理數據庫

許多數據庫 API,好比 PDO、MySQLi 或者 OCI8,都十分繁瑣且書寫起來十分冗長,所以,手工進行這些步驟絕對是噩夢。

測試代碼應當儘量簡短精確,這有若干緣由:

  • 你不但願由於生產代碼的小變動而須要對測試代碼進行數量可觀的修改。

  • 你但願在哪怕好幾個月之後也能輕鬆地閱讀並理解測試代碼。

另外,必須認識到,對於代碼而言,本質上來講數據庫是全局輸入變量。測試套件中的兩個不一樣的測試多是運行在同一個數據庫上的,而且可能把數據重用好屢次。一個測試中出現的失敗很容易影響到後繼測試的結果,從而讓整個測試過程變得很是艱難。前面提到的清理步驟對於解決數據庫是全局輸入」的問題是很是重要的。

DbUnit 以一種優雅的方式來幫助簡化數據庫測試中的全部這些問題。

PHPUnit 沒法幫你解決的問題是,相對於不使用數據的測試而言,數據庫測試是很是慢的。隨着數據庫交互規模的增大,運行測試可能須要耗費可觀的時間。然而,只要保持每一個測試所使用的數據量較小而且儘量用非數據庫測試來對代碼進行測試,即便很大的測試套件也能輕鬆在一分鐘內跑完。

以 Doctrine 2 爲例,此項目的測試套件目前包含了大約1000個測試,其中將近一半訪問了數據庫。可是在一臺安裝了MySQL的普通的臺式機上,整個測試套件依然能在15秒鐘內跑完。

數據庫測試的四個階段

Gerard Meszaros 在他的書《xUnit 測試模式》中列出了單元測試的四個階段:

  1. 創建基境(fixture)

  2. 執行被測系統

  3. 驗證結果

  4. 拆除基境(fixture)

什麼是基境(fixture)?

基境(fixture)是對開始執行某個測試時應用程序和數據庫所處初始狀態的描述。

對數據庫進行測試至少要處理創建與拆除的步驟,在其中完成清理工做,並將所需的基境數據寫入表內。於是,對於數據庫擴展模塊而言,在數據庫測試中有很好的理由將這四個步驟還原成相似下面這樣的工做流程,這個流程對於每一個測試都會完整執行:

1. 清理數據庫

因爲老是會有某個測試運行在並不肯定表中是否有數據的數據庫上,PHPUnit 在全部指定表上執行 TRUNCATE 操做來把它們清空。

2. 創建基境

PHPUnit 隨後將迭代全部指定的基境數據行並將其插入到對應的表裏。

3–5. 運行測試、驗證結果、並拆除基境

在全部數據庫都完成重置並加載好初始狀態後,PHPUnit 纔會執行實際的測試。這個部分的測試代碼徹底不須要數據庫擴展模塊的參與,能夠隨意測試任何想要測試的內容。

在測試中,驗證的目的能夠使用一個名爲 assertDataSetsEqual() 的特殊斷言來實現。固然,這徹底是可選的。這個特性將在數據庫斷言」一節中進行解說。

PHPUnit 數據庫測試用例的配置

通常而言,使用 PHPUnit 時,測試用例都是按以下方式擴展自 PHPUnit\Framework\TestCase 類:

<?php
use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    public function testCalculate()
    {
        $this->assertEquals(2, 1 + 1);
    }
}
?>

若是測試代碼用到了數據庫擴展模塊,那麼創建的過程就會更復雜一些,須要擴展另外一個抽象 TestCase 類,它要求實現兩個抽象方法,getConnection() 和 getDataSet()

<?php
class MyGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    public function getConnection()
    {
        $pdo = new PDO('sqlite::memory:');
        return $this->createDefaultDBConnection($pdo, ':memory:');
    }

    /**
     * @return PHPUnit_Extensions_Database_DataSet_IDataSet
     */
    public function getDataSet()
    {
        return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/guestbook-seed.xml');
    }
}
?>

實現 getConnection()

爲了讓清理與載入基境的功能正常運做,PHPUnit 數據庫擴展模塊須要用 PDO 庫來實現跨供應商抽象訪問數據庫鏈接。重要的是要注意到,使用 PHPUnit 的數據庫擴展模塊並不要求應用程序自己基於PDO,PDO鏈接僅僅用於清理和創建基境。

在以前的例子裏,咱們在內存中建立 Sqlite 數據庫並創建了鏈接,將此鏈接傳遞給 createDefaultDBConnection 方法,這個方法將 PDO 實例和第二參數(數據庫名)包裝在一個很是簡單的數據庫鏈接抽象層中,這個抽象層的類型是 PHPUnit_Extensions_Database_DB_IDatabaseConnection

使用數據庫鏈接」一節解說了這個接口的API以及如何充分利用它們。

實現 getDataSet()

getDataSet() 方法定義了在每一個測試執行以前的數據庫初始狀態應該是什麼樣。數據庫的狀態經過由 PHPUnit_Extensions_Database_DataSet_IDataSet 所表明的 DataSet(數據集)和由PHPUnit_Extensions_Database_DataSet_IDataTable所表明的 DataTable(數據表)這兩個概念進行抽象。下一節將詳細講述這些概念是如何運做的以及在數據庫測試中使用它們有什麼好處。

對於具體實現,只須要知道 setUp() 中會調用一次 getDataSet() 方法來接收基境數據集並將其插入數據庫。在範例中使用了工廠方法 createFlatXMLDataSet($filename),它表明一個用 XML 表示的數據集。

數據庫構架(DDL)怎麼辦?

PHPUnit 假設在測試運行以前數據庫以及其中的全部表(table)、觸發器(trigger)、序列(Sequence)和視圖(view)都已經建立好。這意味着開發者必須在運行測試套件以前確保數據庫已經正確創建。

有幾種方法來達成這個數據庫測試的先決條件。

  1. 若是使用的是持久化數據庫(不是 Sqlite Memory),能夠很輕鬆地用 phpMyAdmin(針對MySQL)之類的工具來一次性創建數據庫,並在每一個測試中複用這個數據庫。

  2. 若是使用的是諸如 Doctrine 2 或 Propel 這樣的庫,能夠用它們的API來在測試運行前一次性創建所需的數據庫。能夠利用 PHPUnit 的引導和配置 功能來在每次測試運行時執行這些代碼。

小建議:使用你本身的抽象數據庫 TestCase 類

從前面的實現範例中容易發現 getConnection() 方法是至關穩定的,能夠在不一樣的數據庫測試用例中重用。另外,爲了保持測試的性能良好和數據庫的開銷較低,能夠對代碼進行一點重構,來爲應用程序造成一個通用的抽象測試用例,同時依然能夠爲每一個具體測試用例指定不一樣的數據基境:

<?php
abstract class MyApp_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
    // 只實例化 pdo 一次,供測試的清理和裝載基境使用
    static private $pdo = null;

    // 對於每一個測試,只實例化 PHPUnit_Extensions_Database_DB_IDatabaseConnection 一次
    private $conn = null;

    final public function getConnection()
    {
        if ($this->conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO('sqlite::memory:');
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, ':memory:');
        }

        return $this->conn;
    }
}
?>

這個例子裏,數據庫鏈接信息硬編碼在 PDO 鏈接裏了。PHPUnit 有另一個絕妙的特性,可讓這個 TestCase 類更加通用。經過 XML 配置 能夠爲每一個測試單獨配置數據庫鏈接信息。首先,在應用程序的 tests/ 目錄下建立 phpunit.xml」 文件,內容大致是這樣:

<?xml version="1.0" encoding="UTF-8" ?>
<phpunit>
    <php>
        <var name="DB_DSN" value="mysql:dbname=myguestbook;host=localhost" />
        <var name="DB_USER" value="user" />
        <var name="DB_PASSWD" value="passwd" />
        <var name="DB_DBNAME" value="myguestbook" />
    </php>
</phpunit>

如今能夠修改 TestCase 類了,像這樣:

<?php
abstract class Generic_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
    // 只實例化 pdo 一次,供測試的清理和裝載基境使用
    static private $pdo = null;

    // 對於每一個測試,只實例化 PHPUnit_Extensions_Database_DB_IDatabaseConnection 一次
    private $conn = null;

    final public function getConnection()
    {
        if ($this->conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO( $GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD'] );
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
        }

        return $this->conn;
    }
}
?>

如今能夠從命令行界面以不一樣的配置來運行數據庫測試套件了:


user@desktop> phpunit --configuration developer-a.xml MyTests/user@desktop> phpunit --configuration developer-b.xml MyTests/

在開發機上進行開發時可以輕鬆的針對不一樣的目標數據庫來運行數據庫測試顯得很是重要。若是多個開發人員在同一個數據庫鏈接上運行數據庫測試,很容易由於競態而致使測試失敗。

理解 DataSet(數據集)和 DataTable(數據表)

PHPUnit 的數據庫擴展模塊的核心概念是 DataSet(數據集)和 DataTable(數據表)。爲了掌握如何使用 PHPUnit 進行測試,須要試着去了解這些簡單的概念。DataSet(數據集)和 DataTable(數據表)是圍繞着數據庫表、行、列的抽象層。經過一套簡單的API,底層數據庫內容被隱藏在對象結構之下,同時,這個對象結構也能夠用其餘非數據庫數據源來實現。

爲了能比較實際內容和預期內容,這種抽象是必須的。預期內容能夠用諸如 XML、 YAML、 CSV 文件或者 PHP 數組等方式來表達。DataSet 和 DataTable 接口以語義類似的方式模擬關係數據庫存儲,從而可以對這些概念上徹底不一樣的數據源進行比較。

在測試中,數據庫斷言的工做流由如下三個簡單的步驟組成:

  • 用表名稱來指定數據庫中的一個或多個表(其實是指定了一個數據集)

  • 用你喜歡的格式(YAML、XML等等)來指定預期數據集

  • 斷言這兩個數據集陳述是彼此相等的。

在 PHPUnit 的數據庫擴展中,斷言並不是惟一使用 DataSet 和 DataTable 的情形。就像上一節中所展現的那樣,它們也用於描述數據庫的初始內容。數據庫 TestCase 類強制要求定義一個基境數據集,隨後用它來:

  • 根據此數據集所指定的全部表名,將數據庫中對應表內的行所有刪除。

  • 將數據集內數據表中的全部行寫入數據庫。

可用的各類實現

有三種不一樣類型的 DataSet/DataTable:

  • 基於文件的 DataSet 和 DataTable

  • 基於查詢的 DataSet 和 DataTable

  • 篩選與組合 DataSet 和 DataTable

基於文件的數據集和表通常用於初始化基境或描述數據庫的預期狀態。

Flat XML DataSet (平直 XML 數據集)

最多見的一種數據集名叫 Flat XML。這是一種很是簡單的 XML 格式,根節點爲 <dataset>,根節點下的每一個標籤就表明數據庫中的一行數據。標籤的名稱就等於表名,而每一個屬性表明一個列。一個簡單的留言本應用程序的例子大體上多是這樣:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" user="nancy" created="2010-04-26 12:14:20" />
</dataset>

顯然,這很是易於編寫。在這裏,<guestbook> 是表名,這個表內有兩行記錄,每行有四個列:id」、content」、user」 和 created」,以及各自的值。

不過,這種簡單性是有代價的。

從上面這個例子裏不太容易看出該如何指定一個空表。其實能夠插入一個沒有屬性值的標籤,以空表的名字做爲標籤名。空的 guestbook 表所對應的 Flat XML 文件大體上多是這樣:

<?xml version="1.0" ?>
<dataset>
    <guestbook />
</dataset>

在 Flat XML DataSet 中,要處理 NULL 值會很是煩。在幾乎全部數據庫中(Oracle 是個例外),NULL 值和空字符串值是有區別的,這一點在 Flat XML 格式中很難表述。能夠在數據行的表述中省略掉對應的屬性來表示NULL值。假定上面這個留言本經過在 user 列使用 NULL 值的方式來容許匿名留言,那麼 guestbook 表的內容多是這樣:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" created="2010-04-26 12:14:20" />
</dataset>

在這個例子裏第二個條目是匿名發表的。可是這爲列的識別帶來了一個很是嚴重的問題。在數據集相等斷言的斷定過程當中,每一個數據集都須要指明每一個表擁有哪些列。若是有一個列在數據表的全部行裏其值都是 NULL,那麼數據庫擴展模塊又該從何得知表中包含這個列呢?

在這裏,Flat XML DataSet 作了一個關鍵假設:一個表的列信息由此表第一行的屬性定義決定。在上面這個例子裏,這意味着 guestbook 有 id」、content」、user」 和 created」 這幾個列。第二行中 user」 列沒有定義,所以將向數據庫中插入 NULL 值。

若是從數據集中刪掉第一行,由於沒有指定 user」,guestbook 表擁有的列就只剩下 id」、content」 和 created」。

要在有 NULL 值的狀況下有效地使用 Flat XML Dataset,就必須保證每一個表的第一行不包含 NULL 值,只有後繼的那些行才能省略屬性。這就有點棘手,由於數據行的排列順序也是數據斷言的一個相關因素。

反過來,若是在 Flat XML Dataset 中只指明瞭實際表中全部列的某個子集,那麼全部省略掉的列都會設爲它們的的默認值。若是某個省略掉的列的定義是 NOT NULL DEFAULT NULL」,就會出現錯誤。

總的來講,建議只在不須要 NULL 值的狀況下使用 Flat XML Dataset。

能夠在數據庫 TestCase 中調用 createFlatXmlDataSet($filename) 方法來建立 Flat XML Dataset 實例:

<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
    }
}
?>

XML DataSet (XML 數據集)

有另一種更加結構化的 XML DataSet,它寫起來有點冗長,可是規避了 Flat XML DataSet 所存在的 NULL 問題。在根節點 <dataset> 內,能夠指定 <table><column>、 <row><value> 和 <null /> 標籤。和上面用 Flat XML 所定義的留言本數據集等價的 XML DataSet 以下:

<?xml version="1.0" ?>
<dataset>
    <table name="guestbook">
        <column>id</column>
        <column>content</column>
        <column>user</column>
        <column>created</column>
        <row>
            <value>1</value>
            <value>Hello buddy!</value>
            <value>joe</value>
            <value>2010-04-24 17:15:23</value>
        </row>
        <row>
            <value>2</value>
            <value>I like it!</value>
            <null />
            <value>2010-04-26 12:14:20</value>
        </row>
    </table>
</dataset>

所定義的每一個 <table> 都有一個名稱,而且必須有對全部列及其名稱的定義。其下能夠包含零個或任意正整數個 <row> 元素。沒有定義 <row> 意味着這是個空表。<value> 和 <null /> 標籤必須按照以前給定 <column> 元素的順序來指定。<null /> 標籤顯然意味着這個值爲 NULL。

能夠在數據庫 TestCase 中調用 createXmlDataSet($filename) 方法來建立 XML DataSet 實例:

<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createXMLDataSet('myXmlFixture.xml');
    }
}
?>

MySQL XML DataSet (MySQL XML 數據集)

這種新的 XML 格式是 MySQL 數據庫服務器專用的。PHPUnit 3.5 加入了對這種格式的支持。能夠用 mysqldump 工具來生成這種格式的文件。與一樣爲 mysqldump 所支持的 CSV 數據集不一樣,這種 XML 格式能夠在單個文件中包含多個表的數據。要生成這種格式的文件,能夠這樣調用 mysqldump

mysqldump --xml -t -u [username] --password=[password] [database] > /path/to/file.xml

能夠在數據庫 TestCase 中調用 createMySQLXMLDataSet($filename) 方法來使用這個文件:

<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createMySQLXMLDataSet('/path/to/file.xml');
    }
}
?>

YAML DataSet (YAML 數據集)

也能夠用 YAML DataSet 來寫這個留言本的例子:

guestbook:
  -
    id: 1
    content: "Hello buddy!"
    user: "joe"
    created: 2010-04-24 17:15:23
  -
    id: 2
    content: "I like it!"
    user:
    created: 2010-04-26 12:14:20

簡單方便,同時還解決了和它相似的 FLat XML DataSet 所具備的 NULL 問題。在 YAML 中,只有列名而沒有指定值就表示 NULL。空白字符串則這樣指定:column1: ""

目前,數據庫 TestCase 中沒有 YAML DataSet 的工廠方法,所以須要手工進行實例化:

<?php
class YamlGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        return new PHPUnit_Extensions_Database_DataSet_YamlDataSet(
            dirname(__FILE__)."/_files/guestbook.yml"
        );
    }
}
?>

CSV DataSet (CSV 數據集)

另一種基於文件的 DataSet 是基於 CSV 文件的。數據集中的每一個表用一個單獨的 CSV 文件表示。對於留言本的例子,能夠這樣定義 guestbook-table.csv 文件:

id,content,user,created
1,"Hello buddy!","joe","2010-04-24 17:15:23"
2,"I like it!","nancy","2010-04-26 12:14:20"

用 Excel 或者 OpenOffice 來對這種格式進行編輯是很是方便的,可是在 CSV DataSet 中沒法指定 NULL 值。給出一個空白列的結果是往這個列中插入數據庫的默認空值。

能夠這樣建立 CSV DataSet:

<?php
class CsvGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
        $dataSet->addTable('guestbook', dirname(__FILE__)."/_files/guestbook.csv");
        return $dataSet;
    }
}
?>

Array DataSe (數組數據集)

在 PHPUnit 的數據庫擴展中,(尚)沒有基於數組的 DataSet,不過很容易自行實現之。留言本的例子大體是這樣:

<?php
class ArrayGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        return new MyApp_DbUnit_ArrayDataSet(
            [
                'guestbook' => [
                    [
                        'id' => 1,
                        'content' => 'Hello buddy!',
                        'user' => 'joe',
                        'created' => '2010-04-24 17:15:23'
                    ],
                    [
                        'id' => 2,
                        'content' => 'I like it!',
                        'user' => null,
                        'created' => '2010-04-26 12:14:20'
                    ],
                ],
            ]
        );
    }
}
?>

PHP 版本的 DataSet 相比於全部其餘基於文件的 DataSet 相比有很明顯的優勢:

  • PHP 數組顯然能夠處理 NULL 值。

  • 不須要爲斷言提供任何額外文件,能夠直接在 TestCase 中指定。

對於這種 DataSet 而言,和平直 XML、CSV、YAML DataSet 同樣,表的列名信息由第一個指定的行的鍵名定義。在上面這個例子裏,就是 id」、content」、user」 和 created」。

這個數組 DataSet 類的實現是很是簡單直接的:

<?php
class MyApp_DbUnit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet
{
    /**
     * @var array
     */
    protected $tables = [];

    /**
     * @param array $data
     */
    public function __construct(array $data)
    {
        foreach ($data AS $tableName => $rows) {
            $columns = [];
            if (isset($rows[0])) {
                $columns = array_keys($rows[0]);
            }

            $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns);
            $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData);

            foreach ($rows AS $row) {
                $table->addRow($row);
            }
            $this->tables[$tableName] = $table;
        }
    }

    protected function createIterator($reverse = false)
    {
        return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse);
    }

    public function getTable($tableName)
    {
        if (!isset($this->tables[$tableName])) {
            throw new InvalidArgumentException("$tableName is not a table in the current database.");
        }

        return $this->tables[$tableName];
    }
}
?>

Query (SQL) DataSet (查詢(SQL)數據集)

對於數據庫斷言,不只須要有基於文件的 DataSet,同時也須要有一種內含數據庫實際內容的基於查詢/SQL 的 DataSet。Query DataSet 在此閃亮登場:

<?php
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook');
?>

單純以名稱來添加表是一種隱式地用如下查詢來定義 DataTable 的方法:

<?php
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook', 'SELECT * FROM guestbook');
?>

能夠在這種用法中爲你的表任意指定查詢,例如限定行、列,或者加上 ORDER BY 子句:

<?php
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook', 'SELECT id, content FROM guestbook ORDER BY created DESC');
?>

在關於數據庫斷言的那一節中有更多關於如何使用 Query DataSet 的細節。

Database (DB) Dataset (數據庫數據集)

經過訪問測試所使用的數據庫鏈接,能夠自動建立包含數據庫全部表以及其內容的 DataSet。所使用的數據庫由數據庫鏈接工廠方法的第二個參數指定。

能夠像 testGuestbook() 中那樣建立整個數據庫所對應的 DataSet,或者像 testFilteredGuestbook() 方法中那樣用一個白名單來將 DataSet 限制在若干表名的集合上。

<?php
class MySqlGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    public function getConnection()
    {
        $database = 'my_database';
        $user = 'my_user';
        $password = 'my_password';
        $pdo = new PDO('mysql:...', $user, $password);
        return $this->createDefaultDBConnection($pdo, $database);
    }

    public function testGuestbook()
    {
        $dataSet = $this->getConnection()->createDataSet();
        // ...
    }

    public function testFilteredGuestbook()
    {
        $tableNames = ['guestbook'];
        $dataSet = $this->getConnection()->createDataSet($tableNames);
        // ...
    }
}
?>

Replacement DataSet (替換數據集)

前面談到了 Flat XML 和 CSV DataSet 所存在的 NULL 問題,不過有一種稍微有點複雜的解決方法可讓這兩種數據集都能正常處理 NULL。

Replacement DataSet 是已有數據集的修飾器(decorator),可以將數據集中任意列的值替換爲其餘替代值。爲了讓留言本的例子可以處理 NULL 值,首先指定相似這樣的文件:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" user="##NULL##" created="2010-04-26 12:14:20" />
</dataset>

而後將 Flat XML DataSet 包裝在 Replacement DataSet 中:

<?php
class ReplacementTest extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        $ds = $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
        $rds = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($ds);
        $rds->addFullReplacement('##NULL##', null);
        return $rds;
    }
}
?>

DataSet Filter (數據集篩選器)

若是有一個很是大的基境文件,能夠用數據集篩選器來爲須要包含在子數據集中的表和列指定白/黑名單。與 DB DataSet 聯用來對數據集中的列進行篩選尤爲方便。

<?php
class DataSetFilterTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testIncludeFilteredGuestbook()
    {
        $tableNames = ['guestbook'];
        $dataSet = $this->getConnection()->createDataSet();

        $filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
        $filterDataSet->addIncludeTables(['guestbook']);
        $filterDataSet->setIncludeColumnsForTable('guestbook', ['id', 'content']);
        // ..
    }

    public function testExcludeFilteredGuestbook()
    {
        $tableNames = ['guestbook'];
        $dataSet = $this->getConnection()->createDataSet();

        $filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
        $filterDataSet->addExcludeTables(['foo', 'bar', 'baz']); // only keep the guestbook table!
        $filterDataSet->setExcludeColumnsForTable('guestbook', ['user', 'created']);
        // ..
    }
}
?>

注意:不能對同一個表同時應用排除與包含兩種列篩選器,只能分別應用於不一樣的表。另外,表的白名單和黑名單也只能選擇其一,不能兩者同時使用。

Composite DataSet (組合數據集)

Composite DataSet 能將多個已存在的數據集聚合成單個數據集,所以很是有用。若是多個數據集中存在一樣的表,其中的數據行將按照指定的順序進行追加。例如,假設有兩個數據集,fixture1.xml

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
</dataset>

和 fixture2.xml

<?xml version="1.0" ?>
<dataset>
    <guestbook id="2" content="I like it!" user="##NULL##" created="2010-04-26 12:14:20" />
</dataset>

經過 Composite DataSet 能夠把這兩個基境文件聚合在一塊兒:

<?php
class CompositeTest extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        $ds1 = $this->createFlatXmlDataSet('fixture1.xml');
        $ds2 = $this->createFlatXmlDataSet('fixture2.xml');

        $compositeDs = new PHPUnit_Extensions_Database_DataSet_CompositeDataSet();
        $compositeDs->addDataSet($ds1);
        $compositeDs->addDataSet($ds2);

        return $compositeDs;
    }
}
?>

小心外鍵

在創建基境的過程當中, PHPUnit 的數據庫擴展模塊按照基境中所指定的順序將數據行插入到數據庫內。假如數據庫中使用了外鍵,這就意味着必須指定好表的順序,以免外鍵約束失敗。

實現自有的 DataSet/DataTable

爲了理解 DataSet 和 DataTable 的內部實現,讓咱們來看看 DataSet 的接口。若是沒打算自行實現 DataSet 或者 DataTable,能夠直接跳過這一部分。

<?php
interface PHPUnit_Extensions_Database_DataSet_IDataSet extends IteratorAggregate
{
    public function getTableNames();
    public function getTableMetaData($tableName);
    public function getTable($tableName);
    public function assertEquals(PHPUnit_Extensions_Database_DataSet_IDataSet $other);

    public function getReverseIterator();
}
?>

這些 public 接口在數據庫 TestCase 中 assertDataSetsEqual() 斷言內使用,用以檢測數據集是否相等。IDataSet 中繼承自 IteratorAggregate 接口的 getIterator() 方法用於對數據集中的全部表進行迭代。逆序迭代器讓 PHPUnit 可以按照與建立時相反的順序對全部表執行 TRUNCATE 操做,以此來保證知足外鍵約束。

根據具體實現的不一樣,要採起不一樣的方法來將表實例添加到數據集中。例如,在全部基於文件的數據集中,表都是在構造過程當中直接從源文件生成並加入數據集中,好比 YamlDataSetXmlDataSet和 FlatXmlDataSet均是如此。

數據表則由如下接口表示:

<?php
interface PHPUnit_Extensions_Database_DataSet_ITable
{
    public function getTableMetaData();
    public function getRowCount();
    public function getValue($row, $column);
    public function getRow($row);
    public function assertEquals(PHPUnit_Extensions_Database_DataSet_ITable $other);
}
?>

除了 getTableMetaData() 方法以外,這個接口是一目瞭然的。數據庫擴展模塊中的各類斷言(將於下一章中介紹)用到了全部這些方法,所以它們所有都是必需的。getTableMetaData() 方法須要返回一個實現了 PHPUnit_Extensions_Database_DataSet_ITableMetaData 接口的描述表結構的對象。這個對象包含以下信息:

  • 表的名稱

  • 表的列名數組,按照列在結果集中出現的順序排列。

  • 構成主鍵的列的數組。

這個接口還包含有檢驗兩個表的元數據實例是否彼此相等的斷言,供數據集相等斷言使用。

數據庫鏈接 API

由數據庫 TestCase 中的 getConnection() 方法所返回的鏈接接口有三個頗有意思的方法:

<?php
interface PHPUnit_Extensions_Database_DB_IDatabaseConnection
{
    public function createDataSet(Array $tableNames = NULL);
    public function createQueryTable($resultName, $sql);
    public function getRowCount($tableName, $whereClause = NULL);

    // ...
}
?>
  1. createDataSet() 方法建立一個在數據集實現一節描述過的 Database (DB) DataSet(數據庫數據集)。

    <?php
    class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testCreateDataSet()
        {
            $tableNames = ['guestbook'];
            $dataSet = $this->getConnection()->createDataSet();
        }
    }
    ?>
  2. createQueryTable() 方法用於建立 QueryTable 的實例,須要爲其指定結果名稱和所使用的 SQL 查詢。當涉及到結果/表的斷言(如後面關於數據庫斷言 API 那一節所示)時,這個方法會很方便。

    <?php
    class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testCreateQueryTable()
        {
            $tableNames = ['guestbook'];
            $queryTable = $this->getConnection()->createQueryTable('guestbook', 'SELECT * FROM guestbook');
        }
    }
    ?>
  3. getRowCount() 方法提供了一種方便的方式來取得表中的行數,而且還能夠選擇附加一個 WHERE 子句來在計數前對數據行進行過濾。它能夠和一個簡單的相等斷言合用:

    <?php
    class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testGetRowCount()
        {
            $this->assertEquals(2, $this->getConnection()->getRowCount('guestbook'));
        }
    }
    ?>

數據庫斷言 API

做爲測試工具,數據庫擴展模塊理所固然會提供一些斷言,能夠用來驗證數據庫的當前狀態、表的當前狀態、表中數據行的數量。本節將詳細描述這部分功能:

對錶中數據行的數量做出斷言

不少時候,確認表中是否包含特定數量的數據行是很是有幫助的。能夠輕鬆作到這一點,不須要任何額外的使用鏈接 API 的粘合劑代碼。好比說,在往留言本中插入一個新行以後,想要確認在表中除了以前的例子中一直都有的兩行以外還有第三行:

<?php
class GuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testAddEntry()
    {
        $this->assertEquals(2, $this->getConnection()->getRowCount('guestbook'), "Pre-Condition");

        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $this->assertEquals(3, $this->getConnection()->getRowCount('guestbook'), "Inserting failed");
    }
}
?>

對錶的狀態做出斷言

前面的這個斷言頗有幫助,可是確定還想要檢驗表的實際內容,好覈實是否全部值都寫到了正確的列中。能夠經過表斷言來作到這一點。

爲此,先定義一個 QueryTable 實例,從表名稱和 SQL 查詢派生出其內容,隨後將其與一個基於文件/數組的數據集進行比較:

<?php
class GuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testAddEntry()
    {
        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $queryTable = $this->getConnection()->createQueryTable(
            'guestbook', 'SELECT * FROM guestbook'
        );
        $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
                              ->getTable("guestbook");
        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}
?>

如今須要爲這個斷言編寫Flat XML 文件 expectedBook.xml

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" user="nancy" created="2010-04-26 12:14:20" />
    <guestbook id="3" content="Hello world!" user="suzy" created="2010-05-01 21:47:08" />
</dataset>

在整個時間長河中,只有特定的一秒鐘內這個斷言能夠經過評定,在 2010–05–01 21:47:08。在數據庫測試中,日期構成了一個特殊的問題。能夠從這個斷言中省略 created」 列來規避失敗。

爲了讓斷言能得以經過, Flat XML 文件 expectedBook.xml 須要調整成大體相似這樣:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" />
    <guestbook id="2" content="I like it!" user="nancy" />
    <guestbook id="3" content="Hello world!" user="suzy" />
</dataset>

還得修正一下 QueryTable 的調用:

<?php
$queryTable = $this->getConnection()->createQueryTable(
    'guestbook', 'SELECT id, content, user FROM guestbook'
);
?>

對查詢的結果做出斷言

利用 QueryTable,也能夠對複雜查詢的結果做出斷言,只須要指定查詢以及結果名稱,並隨後將其與某個數據集進行比較:

<?php
class ComplexQueryTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testComplexQuery()
    {
        $queryTable = $this->getConnection()->createQueryTable(
            'myComplexQuery', 'SELECT complexQuery...'
        );
        $expectedTable = $this->createFlatXmlDataSet("complexQueryAssertion.xml")
                              ->getTable("myComplexQuery");
        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}
?>

對多個表的狀態做出斷言

固然能夠一次性對多個表的狀態做出斷言,並將查詢數據集與基於文件的數據集進行比較。有兩種不一樣的方式來進行數據集斷言。

  1. 能夠從自數據庫鏈接創建數據庫數據集,並將其與基於文件的數據集進行比較。

    <?php
    class DataSetAssertionsTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testCreateDataSetAssertion()
        {
            $dataSet = $this->getConnection()->createDataSet(['guestbook']);
            $expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml');
            $this->assertDataSetsEqual($expectedDataSet, $dataSet);
        }
    }
    ?>
  2. 也能夠自行構造數據集:

    <?php
    class DataSetAssertionsTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testManualDataSetAssertion()
        {
            $dataSet = new PHPUnit_Extensions_Database_DataSet_QueryDataSet();
            $dataSet->addTable('guestbook', 'SELECT id, content, user FROM guestbook'); // additional tables
            $expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml');
    
            $this->assertDataSetsEqual($expectedDataSet, $dataSet);
        }
    }
    ?>

常見問題(FAQ)

PHPUnit 會爲每一個測試(從新)建立數據庫嗎?

不,PHPUnit 要求在測試套件開始時全部數據庫對象必須所有可用。數據庫、表、序列、觸發器還有視圖,必須所有在運行測試套件以前建立好。

Doctrine 2 或 eZ Components 擁有強力的工具,能夠按預約義的數據結構建立數據庫,可是這些都必須和 PHPUnit 擴展模塊對接以後才能自動在整個測試套件運行以前從新建立數據庫。

因爲每一個測試都會完全清空數據庫,所以無須爲每一個測試從新建立數據庫。持久可用的數據庫一樣可以完美工做。

爲了讓數據庫擴展模塊正常工做,須要在應用程序中使用 PDO 嗎?

不,只在基境的清理與創建階段還有斷言檢定時用到PDO。在你的自有代碼中,能夠使用任意數據庫抽象。

若是看到 Too much Connections」 錯誤該怎麼辦?

若是沒有對 TestCase 中 getConnection() 方法所建立 PDO 實例進行緩存,那麼每一個數據庫測試都會增長一個或多個數據庫鏈接。MySQL的默認配置只容許100個併發鏈接,其餘供應商的數據庫也都有各自的最大鏈接限制。

子章節 使用你本身的抽象數據庫 TestCase 類」展現瞭如何經過在全部測試中使用單個PDO實例緩存來防止發生此錯誤。

Flat XML / CSV 數據集中如何處理 NULL?

別這麼幹。應當改用 XML 或者 YAML 數據集。

第 9 章 測試替身

Gerard Meszaros 在 [Meszaros2007] 中介紹了測試替身的概念:

 

有時候對被測系統(SUT)進行測試是很困難的,由於它依賴於其餘沒法在測試環境中使用的組件。這有多是由於這些組件不可用,它們不會返回測試所須要的結果,或者執行它們會有不良反作用。在其餘狀況下,咱們的測試策略要求對被測系統的內部行爲有更多控制或更多可見性。

若是在編寫測試時沒法使用(或選擇不使用)實際的依賴組件(DOC),能夠用測試替身來代替。測試替身不須要和真正的依賴組件有徹底同樣的的行爲方式;他只須要提供和真正的組件一樣的 API 便可,這樣被測系統就會覺得它是真正的組件!

 
  --Gerard Meszaros

PHPUnit 提供的 createMock($type) 和 getMockBuilder($type) 方法能夠在測試中用來自動生成對象,此對象能夠充當任意指定原版類型(接口或類名)的測試替身。在任何預期或要求使用原版類的實例對象的上下文中均可以使用這個測試替身對象來代替。

createMock($type) 方法直接返回指定類型(接口或類)的測試替身對象實例。此測試替身的建立使用了最佳實踐的默認值(不執行原始類的 __construct() 和 __clone() 方法,且不對傳遞給測試替身的方法的參數進行克隆)。若是這些默認值非你所需,能夠用 getMockBuilder($type) 方法並使用流暢式接口來定製測試替身的生成過程。

在默認狀況下,原版類的全部方法都會被替換爲只會返回 null 的僞實現(其中不會調用原版方法)。使用諸如 will($this->returnValue()) 之類的方法能夠對這些僞實如今被調用時應當返回什麼值作出配置。

侷限性:final、private、與 static 方法

請注意,finalprivate 和 static 方法沒法對其進行上樁(stub)或模仿(mock)。PHPUnit 的測試替身功能將會忽略它們,並維持它們的原始行爲。

Stubs (樁件)

將對象替換爲(可選地)返回配置好的返回值的測試替身的實踐方法稱爲上樁(stubbing)。能夠用樁件(stub)來「替換掉被測系統所依賴的實際組件,這樣測試就有了對被測系統的間接輸入的控制點。這使得測試能強制安排被測系統的執行路徑,不然被測系統可能沒法執行」。

例 9.2展現瞭如何對方法的調用上樁以及如何設定返回值。首先用 PHPUnit\Framework\TestCase 類提供的 createMock() 方法來創建一個樁件對象,它表面看起來像是 SomeClass類(例 9.1)的實例。隨後用 PHPUnit 提供的 流暢式接口來指定樁件的行爲。本質上,這意味着不須要創建多個臨時對象而後再把它們捆到一塊兒。取而代之的是範例中所示的鏈式方法調用。這使得代碼更加易讀並更加「流暢」。

例 9.1: 須要對其上樁的類

<?php
use PHPUnit\Framework\TestCase;

class SomeClass
{
    public function doSomething()
    {
        // 隨便作點什麼。
    }
}
?>

例 9.2: 對某個方法的調用上樁,返回固定值

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testStub()
    {
        // 爲 SomeClass 類建立樁件。
        $stub = $this->createMock(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->willReturn('foo');

        // 如今調用 $stub->doSomething() 將返回 'foo'。
        $this->assertEquals('foo', $stub->doSomething());
    }
}
?>

侷限性:名字爲「method」的方法

僅當原始類中不包含名字爲「method」的方法時,以上範例才能正常運行。

若是原始類包含名爲「method」的方法,就必須用 $stub->expects($this->any())->method('doSomething')->willReturn('foo');

「在幕後」,當使用了 createMock() 方法時, PHPUnit 自動生成了一個新的 PHP 類來實現想要的行爲。

例 9.3這個例子展現瞭如何用仿件生成器的流暢式接口來配置測試替身的生成。這個測試替身的默認配置用的是和 createMock() 相同的最佳實踐。

例 9.3: 使用可用於配置生成的測試替身類的仿件生成器 API

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testStub()
    {
        // 爲 SomeClass 類創建樁件。
        $stub = $this->getMockBuilder($originalClassName)
                     ->disableOriginalConstructor()
                     ->disableOriginalClone()
                     ->disableArgumentCloning()
                     ->disallowMockingUnknownTypes()
                     ->getMock();

        // 配置樁件。
        $stub->method('doSomething')
             ->willReturn('foo');

        // 如今調用 $stub->doSomething() 將返回 'foo'。
        $this->assertEquals('foo', $stub->doSomething());
    }
}
?>

在以前的例子中,用 willReturn($value) 返回簡單值。這個簡短的語法至關於 will($this->returnValue($value))。而在這個長點的語法中,能夠使用變量,從而實現更復雜的上樁行爲。

有時想要將(未改變的)方法調用時所使用的參數之一做爲樁件的方法的調用結果來返回。 例 9.4展現瞭如何用 returnArgument() 代替 returnValue() 來作到這點。

例 9.4: 對某個方法的調用上樁,返回參數之一

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testReturnArgumentStub()
    {
        // 爲 SomeClass 類建立樁件。
        $stub = $this->createMock(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->returnArgument(0));

        // $stub->doSomething('foo') 返回 'foo'
        $this->assertEquals('foo', $stub->doSomething('foo'));

        // $stub->doSomething('bar') 返回 'bar'
        $this->assertEquals('bar', $stub->doSomething('bar'));
    }
}
?>

在用流暢式接口進行測試時,讓某個已上樁的方法返回對樁件對象的引用有時會頗有用。例 9.5展現瞭如何用 returnSelf() 來作到這點。

例 9.5: 對方法的調用上樁,返回對樁件對象的引用

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testReturnSelf()
    {
        // 爲 SomeClass 類建立樁件。
        $stub = $this->createMock(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->returnSelf());

        // $stub->doSomething() 返回 $stub
        $this->assertSame($stub, $stub->doSomething());
    }
}
?>

有時候,上樁的方法須要根據預約義的參數清單來返回不一樣的值。能夠用 returnValueMap() 方法將參數和相應的返回值關聯起來創建映射。範例參見例 9.6

例 9.6: 對方法的調用上樁,按照映射肯定返回值

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testReturnValueMapStub()
    {
        // 爲 SomeClass 類建立樁件。
        $stub = $this->createMock(SomeClass::class);

        // 建立從參數到返回值的映射。
        $map = [
            ['a', 'b', 'c', 'd'],
            ['e', 'f', 'g', 'h']
        ];

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->returnValueMap($map));

        // $stub->doSomething() 根據提供的參數返回不一樣的值。
        $this->assertEquals('d', $stub->doSomething('a', 'b', 'c'));
        $this->assertEquals('h', $stub->doSomething('e', 'f', 'g'));
    }
}
?>

若是上樁的方法須要返回計算獲得的值而不是固定值(參見 returnValue())或某個(未改變的)參數(參見 returnArgument()),能夠用 returnCallback() 來讓上樁的方法返回回調函數或方法的結果。範例參見例 9.7

例 9.7: 對方法的調用上樁,由回調生成返回值

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testReturnCallbackStub()
    {
        // 爲 SomeClass 類建立樁件。
        $stub = $this->createMock(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->returnCallback('str_rot13'));

        // $stub->doSomething($argument) 返回 str_rot13($argument)
        $this->assertEquals('fbzrguvat', $stub->doSomething('something'));
    }
}
?>

相比於創建回調方法,有一個更簡單的選擇是直接給出指望返回值的列表。能夠用 onConsecutiveCalls() 方法來作到這個。範例參見 例 9.8

例 9.8: 對方法的調用上樁,按照指定順序返回列表中的值

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testOnConsecutiveCallsStub()
    {
        // 爲 SomeClass 類建立樁件。
        $stub = $this->createMock(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->onConsecutiveCalls(2, 3, 5, 7));

        // $stub->doSomething() 每次返回值都不一樣
        $this->assertEquals(2, $stub->doSomething());
        $this->assertEquals(3, $stub->doSomething());
        $this->assertEquals(5, $stub->doSomething());
    }
}
?>

除了返回一個值以外,上樁的方法還能拋出一個異常。例 9.9展現瞭如何用 throwException() 作到這點。

例 9.9: 對方法的調用上樁,拋出異常

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testThrowExceptionStub()
    {
        // 爲 SomeClass 類建立樁件
        $stub = $this->createMock(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->throwException(new Exception));

        // $stub->doSomething() 拋出異常
        $stub->doSomething();
    }
}
?>

另外,也能夠自行編寫樁件,並在此過程當中改善設計。在系統中被普遍使用的資源是經過單個外觀(facade)來訪問的,所以很容易就能用樁件替換掉資源。例如,將散落在代碼各處的對數據庫的直接調用替換爲單個 Database 對象,這個對象實現了 IDatabase 接口。接下來,就能夠建立實現了 IDatabase 的樁件並在測試中使用之。甚至能夠建立一個選項來控制是用樁件仍是用真實數據庫來運行測試,這樣測試就既能在開發過程當中用做本地測試,又能在實際數據庫環境中進行集成測試。

須要上樁的功能每每集中在同一個對象中,這就改善了內聚度。將功能經過單一且一致的接口呈現出來,就下降了這部分與系統其餘部分之間的耦合度。

仿件對象(Mock Object)

將對象替換爲能驗證預期行爲(例如斷言某個方法必會被調用)的測試替身的實踐方法稱爲模仿(mocking)

能夠用 仿件對象(mock object)「做爲觀察點來覈實被測試系統在測試中的間接輸出。一般,仿件對象還須要包括樁件的功能,由於若是測試還沒有失敗則仿件對象須要向被測系統返回一些值,可是其重點仍是在對間接輸出的核實上。所以,仿件對象遠不止是樁件加斷言,它是以一種從根本上徹底不一樣的方式來使用的」(Gerard Meszaros)。

侷限性:對預期的自動校驗

PHPUnit只會對在某個測試的做用域內生成的仿件對象進行自動校驗。諸如在數據供給器內生成或用@depends 標註注入測試的仿件對象,PHPUnit並不會自動對其進行校驗。

這有個例子:假設須要測試的當前方法,在例子中是 update(),確實在一個觀察着另一個對象的對象中上被調用了。例 9.10展現了被測系統(SUT)中 Subject 和 Observer 兩個類的代碼。

例 9.10: 被測系統(SUT)中 Subject 與 Observer 類的代碼

<?php
use PHPUnit\Framework\TestCase;

class Subject
{
    protected $observers = [];
    protected $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function doSomething()
    {
        // 作點什麼
        // ...

        // 通知觀察者發生了些什麼
        $this->notify('something');
    }

    public function doSomethingBad()
    {
        foreach ($this->observers as $observer) {
            $observer->reportError(42, 'Something bad happened', $this);
        }
    }

    protected function notify($argument)
    {
        foreach ($this->observers as $observer) {
            $observer->update($argument);
        }
    }

    // 其餘方法。
}

class Observer
{
    public function update($argument)
    {
        // 作點什麼。
    }

    public function reportError($errorCode, $errorMessage, Subject $subject)
    {
        // 作點什麼。
    }

    // 其餘方法。
}
?>

例 9.11展現瞭如何用仿件對象來測試 Subject 和 Observer 對象之間的互動。

首先用 PHPUnit\Framework\TestCase 類提供的 getMockBuilder() 方法創建 Observer 的仿件對象。因爲給出了一個數組作爲 getMock() 方法的第二(可選)參數,Observer 類只有 update() 方法會被替換爲仿實現。

因爲關注的是檢驗某個方法是否被調用,以及調用時具體所使用的參數,所以引入 expects() 與 with() 方法來指明此交互應該是什麼樣的。

例 9.11: 測試某個方法會以特定參數被調用一次

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testObserversAreUpdated()
    {
        // 爲 Observer 類創建仿件對象,只模仿 update() 方法。
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['update'])
                         ->getMock();

        // 創建預期情況:update() 方法將會被調用一次,
        // 而且將以字符串 'something' 爲參數。
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));

        // 建立 Subject 對象,並將模仿的 Observer 對象鏈接其上。
        $subject = new Subject('My subject');
        $subject->attach($observer);

        // 在 $subject 對象上調用 doSomething() 方法,
        // 預期將以字符串 'something' 爲參數調用 
        // Observer 仿件對象的 update() 方法。
        $subject->doSomething();
    }
}
?>

with() 方法能夠攜帶任何數量的參數,對應於被模仿的方法的參數數量。能夠對方法的參數指定更加高等的約束而不只是簡單的匹配。

例 9.12: 測試某個方法將會以特定數量的參數進行調用,而且對各個參數以多種方式進行約束

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testErrorReported()
    {
        // 爲 Observer 類創建仿件,對 reportError() 方法進行模仿
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['reportError'])
                         ->getMock();

        $observer->expects($this->once())
                 ->method('reportError')
                 ->with(
                       $this->greaterThan(0),
                       $this->stringContains('Something'),
                       $this->anything()
                   );

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // doSomethingBad() 方法應當會經過(observer的)reportError()方法
        // 向 observer 報告錯誤。
        $subject->doSomethingBad();
    }
}
?>

withConsecutive() 方法能夠接受任意多個數組做爲參數,具體數量取決於欲測試的調用。每一個數組都都是對被仿方法的相應參數的一組約束,就像 with() 中那樣。

例 9.13: 測試某個方法將會以特定參數被調用二次

<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testFunctionCalledTwoTimesWithSpecificArguments()
    {
        $mock = $this->getMockBuilder(stdClass::class)
                     ->setMethods(['set'])
                     ->getMock();

        $mock->expects($this->exactly(2))
             ->method('set')
             ->withConsecutive(
                 [$this->equalTo('foo'), $this->greaterThan(0)],
                 [$this->equalTo('bar'), $this->greaterThan(0)]
             );

        $mock->set('foo', 21);
        $mock->set('bar', 48);
    }
}
?>

callback() 約束用來進行更加複雜的參數校驗。此約束的惟一參數是一個 PHP 回調項(callback)。此 PHP 回調項接受須要校驗的參數做爲其惟一參數,並應當在參數經過校驗時返回 true,不然返回 false

例 9.14: 更加複雜的參數校驗

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testErrorReported()
    {
        // 爲 Observer 類創建仿件,模仿 reportError() 方法
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['reportError'])
                         ->getMock();

        $observer->expects($this->once())
                 ->method('reportError')
                 ->with($this->greaterThan(0),
                        $this->stringContains('Something'),
                        $this->callback(function($subject){
                          return is_callable([$subject, 'getName']) &&
                                 $subject->getName() == 'My subject';
                        }));

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // doSomethingBad() 方法應當會經過(observer的)reportError()方法
        // 向 observer 報告錯誤。
        $subject->doSomethingBad();
    }
}
?>

例 9.15: 測試某個方法將會被調用一次,而且以某個特定對象做爲參數。

<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testIdenticalObjectPassed()
    {
        $expectedObject = new stdClass;

        $mock = $this->getMockBuilder(stdClass::class)
                     ->setMethods(['foo'])
                     ->getMock();

        $mock->expects($this->once())
             ->method('foo')
             ->with($this->identicalTo($expectedObject));

        $mock->foo($expectedObject);
    }
}
?>

例 9.16: 建立仿件對象時啓用參數克隆

<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testIdenticalObjectPassed()
    {
        $cloneArguments = true;

        $mock = $this->getMockBuilder(stdClass::class)
                     ->enableArgumentCloning()
                     ->getMock();

        // 如今仿件將對參數進行克隆,所以 identicalTo 約束將會失敗。
    }
}
?>

表 A.1列出了能夠應用於方法參數的各類約束,表 9.1列出了能夠用於指定調用次數的各類匹配器。

表 9.1. 匹配器

匹配器 含義
PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any() 返回一個匹配器,當被評定的方法執行0次或更屢次(即任意次數)時匹配成功。
PHPUnit_Framework_MockObject_Matcher_InvokedCount never() 返回一個匹配器,當被評定的方法從未執行時匹配成功。
PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce() 返回一個匹配器,當被評定的方法執行至少一次時匹配成功。
PHPUnit_Framework_MockObject_Matcher_InvokedCount once() 返回一個匹配器,當被評定的方法執行剛好一次時匹配成功。
PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count) 返回一個匹配器,當被評定的方法執行剛好 $count 次時匹配成功。
PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index) 返回一個匹配器,當被評定的方法是第 $index 個執行的方法時匹配成功。

注意

at() 匹配器的 $index 參數指的是對給定仿件對象的全部方法的調用的索引,從零開始。使用這個匹配器要謹慎,由於它可能致使測試因爲與具體的實現細節過度緊密綁定而變得脆弱。

如一開始提到的,若是 createMock() 方法在生成測試替身時所使用的默認值不符合你的要求,則能夠經過 getMockBuilder($type) 方法來用流暢式接口定製測試替身的生成過程。如下是仿件生成器所提供的方法列表:

  • setMethods(array $methods) 能夠在仿件生成器對象上調用,來指定哪些方法將被替換爲可配置的測試替身。其餘方法的行爲不會有所改變。若是調用 setMethods(null),那麼沒有方法會被替換。

  • setConstructorArgs(array $args) 可用於向原版類的構造函數(默認狀況下不會被替換爲僞實現)提供參數數組。

  • setMockClassName($name) 可用於指定生成的測試替身類的類名。

  • disableOriginalConstructor() 參數可用於禁用對原版類的構造方法的調用。

  • disableOriginalClone() 可用於禁用對原版類的克隆方法的調用。

  • disableAutoload()可用於在測試替身類的生成期間禁用 __autoload()

Prophecy

Prophecy 是個「極爲自我卻又很是強大且靈活的 PHP 對象模仿框架。雖然一開始是爲了知足 phpspec2 的須要而創建的,但它足夠靈活,能夠用最小代價用於任何測試框架內。」

PHPUnit 對用 Prophecy 創建測試替身提供了內建支持。例 9.17展現了例 9.11中展現的測試應該如何用 Prophecy 的的預言式理念方式來達到一樣的效果:

例 9.17: 測試某個方法會以特定參數被調用一次

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testObserversAreUpdated()
    {
        $subject = new Subject('My subject');

        // 爲 Observer 類創建預言(prophecy)。
        $observer = $this->prophesize(Observer::class);

        // 創建預期情況:update() 方法將會被調用一次,
        // 而且將以字符串 'something' 爲參數。
        $observer->update('something')->shouldBeCalled();

        // 揭示預言,並將仿件對象連接到主體上。
        $subject->attach($observer->reveal());

        // 在 $subject 對象上調用 doSomething() 方法,
        // 預期將以字符串 'something' 爲參數調用 
        // Observer 仿件對象的 update() 方法。
        $subject->doSomething();
    }
}
?>

更多關於如何用這個測試替身框架來建立、配置及使用樁件、諜件、仿件的細節,請參考 Prophecy 的 文檔

對特質(Trait)與抽象類進行模仿

getMockForTrait() 方法返回一個使用了特定特質(trait)的仿件對象。給定特質的全部抽象方法將都被模仿。這樣就能對特質的具體方法進行測試。

例 9.18: 對特質的具體方法進行測試

<?php
use PHPUnit\Framework\TestCase;

trait AbstractTrait
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class TraitClassTest extends TestCase
{
    public function testConcreteMethod()
    {
        $mock = $this->getMockForTrait(AbstractTrait::class);

        $mock->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(true));

        $this->assertTrue($mock->concreteMethod());
    }
}
?>

getMockForAbstractClass() 方法返回一個抽象類的仿件對象。給定抽象類的全部抽象方法將都被模仿。這樣就能對抽象類的具體方法進行測試。

例 9.19: 對抽象類的具體方法進行測試

<?php
use PHPUnit\Framework\TestCase;

abstract class AbstractClass
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class AbstractClassTest extends TestCase
{
    public function testConcreteMethod()
    {
        $stub = $this->getMockForAbstractClass(AbstractClass::class);

        $stub->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(true));

        $this->assertTrue($stub->concreteMethod());
    }
}
?>

對 Web 服務(Web Services)進行上樁或模仿

當應用程序須要和 web 服務進行交互時,會想要在不與 web 服務進行實際交互的狀況下對其進行測試。爲了簡單地對 web 服務進行上樁或模仿,能夠像使用 getMock() (見上文)那樣使用getMockFromWsdl()。惟一的區別是 getMockFromWsdl() 所返回的樁件或者仿件是基於以 WSDL 描述的 web 服務,而 getMock() 返回的樁件或者仿件是基於 PHP 類或接口的。

例 9.20展現瞭如何用 getMockFromWsdl() 來對(例如)GoogleSearch.wsdl 中描述的 web 服務上樁。

例 9.20: 對 web 服務上樁

<?php
use PHPUnit\Framework\TestCase;

class GoogleTest extends TestCase
{
    public function testSearch()
    {
        $googleSearch = $this->getMockFromWsdl(
          'GoogleSearch.wsdl', 'GoogleSearch'
        );

        $directoryCategory = new stdClass;
        $directoryCategory->fullViewableName = '';
        $directoryCategory->specialEncoding = '';

        $element = new stdClass;
        $element->summary = '';
        $element->URL = 'https://phpunit.de/';
        $element->snippet = '...';
        $element->title = '<b>PHPUnit</b>';
        $element->cachedSize = '11k';
        $element->relatedInformationPresent = true;
        $element->hostName = 'phpunit.de';
        $element->directoryCategory = $directoryCategory;
        $element->directoryTitle = '';

        $result = new stdClass;
        $result->documentFiltering = false;
        $result->searchComments = '';
        $result->estimatedTotalResultsCount = 3.9000;
        $result->estimateIsExact = false;
        $result->resultElements = [$element];
        $result->searchQuery = 'PHPUnit';
        $result->startIndex = 1;
        $result->endIndex = 1;
        $result->searchTips = '';
        $result->directoryCategories = [];
        $result->searchTime = 0.248822;

        $googleSearch->expects($this->any())
                     ->method('doGoogleSearch')
                     ->will($this->returnValue($result));

        /**
         * $googleSearch->doGoogleSearch() 將會返回上樁的結果,
         * web 服務的 doGoogleSearch() 方法不會被調用。
         */
        $this->assertEquals(
          $result,
          $googleSearch->doGoogleSearch(
            '00000000000000000000000000000000',
            'PHPUnit',
            0,
            1,
            false,
            '',
            false,
            '',
            '',
            ''
          )
        );
    }
}
?>

對文件系統進行模仿

vfsStream 是對虛擬文件系統 的 流包覆器(stream wrapper),能夠用於模仿真實文件系統,在單元測試中可能會有所助益。

若是使用 Composer 來管理項目的依賴關係,那麼只需簡單的在項目的 composer.json 文件中加一條對 mikey179/vfsStream 的依賴關係便可。如下是一個最小化的 composer.json文件例子,只定義了一條對 PHPUnit 4.6 與 vfsStream 的開發時(development-time)依賴:

{
    "require-dev": {
        "phpunit/phpunit": "~4.6",
        "mikey179/vfsStream": "~1"
    }
}

例 9.21展現了一個與文件系統交互的類。

例 9.21: 一個與文件系統交互的類

<?php
use PHPUnit\Framework\TestCase;

class Example
{
    protected $id;
    protected $directory;

    public function __construct($id)
    {
        $this->id = $id;
    }

    public function setDirectory($directory)
    {
        $this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;

        if (!file_exists($this->directory)) {
            mkdir($this->directory, 0700, true);
        }
    }
}?>

若是不使用諸如 vfsStream 這樣的虛擬文件系統,就沒法在隔離外部影響的狀況下對 setDirectory() 方法進行測試(參見 例 9.22)。

例 9.22: 對一個與文件系統交互的類進行測試

<?php
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    protected function setUp()
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(file_exists(dirname(__FILE__) . '/id'));

        $example->setDirectory(dirname(__FILE__));
        $this->assertTrue(file_exists(dirname(__FILE__) . '/id'));
    }

    protected function tearDown()
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }
}
?>

上面的方法有幾個缺點:

  • 和任何其餘外部資源同樣,文件系統可能會間歇性的出現一些問題,這使得和它交互的測試變得不可靠。

  • 在 setUp() 和 tearDown() 方法中,必須確保這個目錄在測試前和測試後均不存在。

  • 若是測試在 tearDown() 方法被調用以前就終止了,這個目錄就會遺留在文件系統中。

例 9.23展現瞭如何在對與文件系統交互的類進行的測試中使用 vfsStream 來模仿文件系統。

例 9.23: 在對與文件系統交互的類進行的測試中模仿文件系統

<?php
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    public function setUp()
    {
        vfsStreamWrapper::register();
        vfsStreamWrapper::setRoot(new vfsStreamDirectory('exampleDir'));
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(vfsStreamWrapper::getRoot()->hasChild('id'));

        $example->setDirectory(vfsStream::url('exampleDir'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('id'));
    }
}
?>

這有幾個優勢:

  • 測試自己更加簡潔。

  • vfsStream 讓開發者可以徹底控制被測代碼所處的文件系統環境。

  • 因爲文件系統操做再也不對真實文件系統進行操做,tearDown() 方法中的清理操做再也不須要了。

第 10 章 測試實踐

 

你總能編寫更多測試。可是很快就會發現,在全部想得出來的測試中只有很小一部分是真正有用的。須要編寫的是那些以爲能運做但卻失敗或以爲必將失敗但卻成功的測試。另一種思考方式是從成本/收益的關係上去考量。須要編寫的是可以給出反饋信息的測試。

 
  --Erich Gamma

在開發過程當中

當須要對軟件的內部結構進行更改時,你其實是要在不影響其可見行爲的狀況下讓它更加容易理解、更加易於修改,測試套件對於安全地進行這些所謂的重構而言是很是寶貴的。不然,你可能在重組過程當中將系統搞壞而不自知。

在使用單元測試來確認重構的轉換步驟中確實保持原有行爲而且沒有引入錯誤時,如下狀況有助於改進項目的編碼與設計:

  1. 全部單元測試均正確運行。

  2. 代碼傳達其設計原則。

  3. 代碼沒有冗餘。

  4. 代碼所包含的類和方法的數量降至最低。

當須要向系統內添加新的功能時,首先爲其編寫測試。而後,當測試可以正常運行就標誌着開發完成了。下一章將詳細討論這種作法。

在調試過程當中

當看到缺陷報告時,你可能會有儘快修復錯誤的衝動。經驗代表,這種衝動不是好事,由於修復一個缺陷時極可能致使另一個缺陷。

下列操做能夠幫你壓住衝動:

  1. 確認可以重現此缺陷。

  2. 在代碼中尋找此缺陷的最小規模表達。例如,若是在輸出中有一個數字看起來不對,那麼就尋找算出此數字的那個對象。

  3. 編寫一個目前會失敗而缺陷修復後將會成功的自動測試。

  4. 修復缺陷。

尋找缺陷的最小可靠重現使你有機會去真正檢查缺陷的緣由。當修復了缺陷以後,所編寫的測試則有助於提升缺陷真正被修復的概率,由於新加入的測試下降了將來修改代碼時又破壞此修復的可能性。而以前所編寫的全部測試則下降了在不經意間致使其餘問題的可能性。

 

進行單元測試帶來了不少好處:

  • 進行測試讓代碼的做者和評審者對補丁可以產生正確的結果有信心。

  • 編寫測試用例對開發者而言是一種很好的發現邊緣狀況的原動力。

  • 進行測試提供了一種良好的方法來快速捕捉退步(Regression),而且能用來保證退步不會重複出現。

  • 單元測試就如何使用 API 提供了可正常工做的範例,可以大大幫助文檔編制工做。

總之,進行集成單元測試下降了任何修改的成本與風險。這使得項目可以更快而且更有信心地進行[...]重大架構改良[...]。

 
  --Benjamin Smedberg

第 11 章 代碼覆蓋率分析

 

計算機科學中所說的代碼覆蓋率是一種用於衡量特定測試套件對程序源代碼測試程度的指標。擁有高代碼覆蓋率的程序相較於低代碼低機率的程序而言測試的更加完全、包含軟件 bug 的可能性更低。

 
  --Wikipedia

在本章中,你將學到 PHPUnit 中與代碼覆蓋率相關的一切功能。經過這部分功能,可以瞭解在測試運行過程當中執行了生產代碼的哪些部分。它使用了 PHP_CodeCoverage 組件,而這個組件又使用了 PHP 的 Xdebug 擴展所提供的代碼覆蓋率功能。

注意

Xdebug 不隨 PHPUnit 分發。若是在運行測試時收到了 Xdebug 擴展未加載的提示,就意味着 Xdebug 未安裝或者未正確配置。在使用 PHPUnit 的代碼覆蓋率分析功能以前,須要閱讀下 Xdebug 安裝指南

PHPUnit 能夠生成基於 HTML 的代碼覆蓋率報告,同時也能生成好幾種(Clover、Crap4J、PHPUnit)基於XML的代碼覆蓋率信息記錄文件。代碼覆蓋率信息也能以文本格式提供(同時能夠輸出到STDOUT)或以PHP代碼格式輸出以供進一步處理。

第 3 章中列出了各類控制代碼覆蓋率功能的命令行參數供參考,同時「Logging (日誌記錄)」一節中能夠找到其餘相關的配置信息。

用於代碼覆蓋率的軟件衡量標準

目前存在多種軟件衡量標準用於衡量代碼覆蓋率:

行覆蓋率(Line Coverage)

行覆蓋率(Line Coverage)按單個可執行行是否已執行到進行計量。

函數與方法覆蓋率(Function and Method Coverage)

函數與方法覆蓋率(Function and Method Coverage)按單個函數或方法是否已調用到進行計量。僅當函數或方法的全部可執行行所有已覆蓋時 PHP_CodeCoverage 纔將其視爲已覆蓋。

類與特質覆蓋率(Class and Trait Coverage)

類與特質覆蓋率(Class and Trait Coverage)按單個類或特質的全部方法是否所有已覆蓋進行計量。僅當一個類或性狀的全部方法所有已覆蓋時 PHP_CodeCoverage 纔將其視爲已覆蓋。

Opcode 覆蓋率(Opcode Coverage)

Opcode 覆蓋率按函數或方法對應的每條 opcode 在運行測試套件時是否執行到進行計量。一行(PHP的)代碼一般會編譯獲得多條 opcode。進行行覆蓋率計量時,只要其中任何一條 opcode 被執行就視爲此行已覆蓋。

分支覆蓋率(Branch Coverage)

分支覆蓋率(Branch Coverage)按控制結構的分支進行計量。測試套件運行時每一個控制結構的布爾表達式求值爲 true 和 false 各自計爲一個分支。

路徑覆蓋率(Path Coverage)

路徑覆蓋率(Path Coverage)按測試套件運行時函數或者方法內部所經歷的執行路徑進行計量。一個執行路徑指的是從進入函數或方法一直到離開的過程當中通過各個分支的特定序列。

變動風險反模式(CRAP)指數(Change Risk Anti-Patterns (CRAP) Index)

變動風險反模式(CRAP)指數(Change Risk Anti-Patterns (CRAP) Index)是基於代碼單元的圈複雜度(cyclomatic complexity)與代碼覆蓋率計算得出的。不太複雜並具備恰當測試覆蓋率的代碼將得出較低的CRAP指數。能夠經過編寫測試或重構代碼來下降其複雜性的方式來下降CRAP指數。

注意

目前 PHP_CodeCoverage 尚不支持 Opcode覆蓋率分支覆蓋率 及 路徑覆蓋率

將文件列入白名單

爲了告訴 PHPUnit 哪些源代碼文件要包含在代碼覆蓋率報告中,必須配置白名單。能夠用命令行選項 --whitelist 或經過配置文件(參見 「Whitelisting Files for Code Coverage」一節)來完成。

能夠在 PHPUnit 的配置信息中設置 addUncoveredFilesFromWhitelist="true" 來將白名單中包含的全部文件所有加入到代碼覆蓋率報告中(參見「Whitelisting Files for Code Coverage」一節)。這樣能夠把徹底沒有測試到的文件也一併包含到報告中。若是須要知道這些未被覆蓋文件中有哪些行是可執行的,須要同時在 PHPUnit 的配置信息中設置processUncoveredFilesFromWhitelist="true"(參見「Whitelisting Files for Code Coverage」一節)。

注意

請注意,當設置了 processUncoveredFilesFromWhitelist="true" 時將對源代碼文件進行載入,這在某些狀況下可能致使問題,好比,源代碼文件包含有處於類或者函數做用域以外的代碼。

略過代碼塊

有時,一些代碼塊是沒法對其進行測試的,所以但願在代碼覆蓋率分析中忽略它們。在 PHPUnit 中能夠用 @codeCoverageIgnore@codeCoverageIgnoreStart 與 @codeCoverageIgnoreEnd 標註來作到這點,如例 11.1中所示。

例 11.1: 使用 @codeCoverageIgnore@codeCoverageIgnoreStart 與 @codeCoverageIgnoreEnd 標註

<?php
use PHPUnit\Framework\TestCase;

/**
 * @codeCoverageIgnore
 */
class Foo
{
    public function bar()
    {
    }
}

class Bar
{
    /**
     * @codeCoverageIgnore
     */
    public function foo()
    {
    }
}

if (false) {
    // @codeCoverageIgnoreStart
    print '*';
    // @codeCoverageIgnoreEnd
}

exit; // @codeCoverageIgnore
?>

代碼中被忽略掉的行(用標註標記爲忽略)將會計爲已執行(若是它們是可執行的),而且不會在代碼覆蓋狀況中被高亮標記。

指明要覆蓋的方法

@covers 標註(參見 表 B.1)能夠用在測試代碼中來指明測試方法想要對哪些方法進行測試。若是提供了這個信息,則只有指定方法的代碼覆蓋率信息會被統計。 例 11.2展現了一個例子。

例 11.2: 在測試中指明欲覆蓋哪些方法

<?php
use PHPUnit\Framework\TestCase;

class BankAccountTest extends TestCase
{
    protected $ba;

    protected function setUp()
    {
        $this->ba = new BankAccount;
    }

    /**
     * @covers BankAccount::getBalance
     */
    public function testBalanceIsInitiallyZero()
    {
        $this->assertEquals(0, $this->ba->getBalance());
    }

    /**
     * @covers BankAccount::withdrawMoney
     */
    public function testBalanceCannotBecomeNegative()
    {
        try {
            $this->ba->withdrawMoney(1);
        }

        catch (BankAccountException $e) {
            $this->assertEquals(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::depositMoney
     */
    public function testBalanceCannotBecomeNegative2()
    {
        try {
            $this->ba->depositMoney(-1);
        }

        catch (BankAccountException $e) {
            $this->assertEquals(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::getBalance
     * @covers BankAccount::depositMoney
     * @covers BankAccount::withdrawMoney
     */
    public function testDepositWithdrawMoney()
    {
        $this->assertEquals(0, $this->ba->getBalance());
        $this->ba->depositMoney(1);
        $this->assertEquals(1, $this->ba->getBalance());
        $this->ba->withdrawMoney(1);
        $this->assertEquals(0, $this->ba->getBalance());
    }
}
?>

同時,能夠用 @coversNothing 標註來指明一個測試不覆蓋任何方法(參見「@coversNothing」一節)。這能夠在編寫集成測試時用於確保代碼覆蓋所有來自單元測試。

例 11.3: 指明測試不欲覆蓋任何方法

<?php
use PHPUnit\Framework\TestCase;

class GuestbookIntegrationTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @coversNothing
     */
    public function testAddEntry()
    {
        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $queryTable = $this->getConnection()->createQueryTable(
            'guestbook', 'SELECT * FROM guestbook'
        );

        $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
                              ->getTable("guestbook");

        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}
?>
      

邊緣狀況

本節中展現了一些值得注意的邊緣狀況,在這些邊緣狀況中可能出現使人迷惑的代碼覆蓋率信息。

例 11.4:

<?php
use PHPUnit\Framework\TestCase;

// 由於覆蓋率是「基於行」而不是基於語句的,
// 每行只會有一種覆蓋狀態
if (false) this_function_call_shows_up_as_covered();

// 因爲代碼覆蓋率的內部工做方式,這兩行顯得很特殊。
// 這一行會顯示爲非可執行
if (false)
    // 這一行會顯示爲已覆蓋,
    // 其實是上一行的 if 語句的覆蓋信息顯示在這了!
    will_also_show_up_as_covered();

// 要避免這種狀況,必須使用大括號
if (false) {
    this_call_will_never_show_up_as_covered();
}
?>

第 12 章 測試的其餘用途

一旦習慣了編寫自動測試,就可能會發現測試的更多用途。這有一些例子。

敏捷文檔

一般,在使用了諸如極限編程之類的敏捷流程的項目中,文檔每每沒法跟上項目設計與代碼的頻繁變動。極限編程要求羣體代碼全部權(collective code ownership),所以全部開發者都須要知道整個系統是如何工做的。若是你足夠訓練有素,爲測試使用了「能說明問題的名稱(speaking names)」來描述各個類應當幹什麼,那麼就能夠用 PHPUnit 的 TestDox 功能來基於項目的測試生成項目的自動文檔。這個文檔可以就項目中的各個類應當起什麼做用給開發者一份概述。

PHPUnit 的 TestDox 功能着眼於測試類及其全部測試方法的名稱,將它們駝峯式大小寫(camel case)拼寫的 PHP 名稱轉換爲句子:testBalanceIsInitiallyZero() 轉化爲 "Balance is initially zero(初始結餘爲零)"。若是有多個測試方法的名字互相之間的差別只是一個或多個數字的後綴,例如 testBalanceCannotBecomeNegative() 和 testBalanceCannotBecomeNegative2(),假如全部這些測試都成功,句子"Balance cannot become negative(結餘不能變爲負數)"只會出現一次。

來看一下從 BankAccount 類生成的敏捷文檔:


PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

BankAccount
 [x] Balance is initially zero
 [x] Balance cannot become negativephpunit --testdox BankAccountTest

另外,敏捷文檔也能夠以 HTML 或純文本格式生成,並寫入文件中,用 --testdox-html 和 --testdox-text 參數便可。

敏捷文檔能夠用於將對項目所使用的外部包所作出的假設文檔化。使用外部包,你就暴露於這個包的行爲與你所預期的不一樣的風險中,而且包的將來版本可能在你所不知道的狀況下有微妙的改變並破壞你的代碼。每次作出假設時就編寫一個對應的測試能夠處理這些風險。若是測試成功,那麼假設就有效。若是全部的假設都經過測試來文檔化,外部包在將來發布新版本就不會引發憂慮:若是測試成功,那麼系統就應當能繼續正常運做。

跨團隊測試

一旦用測試將假設文檔化,你就擁有了測試。包的提供者——你作假設的對象——對你的測試一無所知。若是打算與包的提供者有更親密的關係,能夠用測試來溝通與協調你的活動。

當你願意和包的提供着協調你的活動時,大家能夠共同編寫測試。經過這樣的方式,測試可以展示出儘量多的假設。隱藏的假設是在給合做判死刑。利用測試,你精確的將對所提供的包的預期文檔化。提供者在全部測試順利運行時就知道包已經完整了。

經過使用樁件(參見本書前面關於「仿件對象」的那一章),你能夠更好的與供應商解耦:供應商的工做就是讓測試可以運行於包的實際實現上;你的工做則是讓測試可以運行於你本身的代碼上。在你拿到包的實際實現前,使用樁件對象。經過這種方式,兩個團隊能夠互相獨立的進行開發。

第 13 章 Logging (日誌記錄)

PHPUnit 能夠生成幾種類型的日誌文件。

測試結果 (XML)

PHPUnit 所生成的測試結果 XML 日誌文件是基於 JUnit task for Apache Ant 所使用的 XML 日誌的。下面的例子展現了 ArrayTest 中的測試所生成的 XML 日誌文件:

<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="ArrayTest"
             file="/home/sb/ArrayTest.php"
             tests="2"
             assertions="2"
             failures="0"
             errors="0"
             time="0.016030">
    <testcase name="testNewArrayIsEmpty"
              class="ArrayTest"
              file="/home/sb/ArrayTest.php"
              line="6"
              assertions="1"
              time="0.008044"/>
    <testcase name="testArrayContainsAnElement"
              class="ArrayTest"
              file="/home/sb/ArrayTest.php"
              line="15"
              assertions="1"
              time="0.007986"/>
  </testsuite>
</testsuites>

如下 XML 日誌文件是由名爲 FailureErrorTest 的測試用例類中的兩個測試 testFailure 和 testError 所生成的,展現了失敗和錯誤是如何表示的:

<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="FailureErrorTest"
             file="/home/sb/FailureErrorTest.php"
             tests="2"
             assertions="1"
             failures="1"
             errors="1"
             time="0.019744">
    <testcase name="testFailure"
              class="FailureErrorTest"
              file="/home/sb/FailureErrorTest.php"
              line="6"
              assertions="1"
              time="0.011456">
      <failure type="PHPUnit_Framework_ExpectationFailedException">
testFailure(FailureErrorTest)
Failed asserting that &lt;integer:2&gt; matches expected value &lt;integer:1&gt;.

/home/sb/FailureErrorTest.php:8
</failure>
    </testcase>
    <testcase name="testError"
              class="FailureErrorTest"
              file="/home/sb/FailureErrorTest.php"
              line="11"
              assertions="0"
              time="0.008288">
      <error type="Exception">testError(FailureErrorTest)
Exception:

/home/sb/FailureErrorTest.php:13
</error>
    </testcase>
  </testsuite>
</testsuites>

測試結果 (TAP)

Test Anything Protocol (TAP) 是 Perl 與測試模塊之間所使用的簡單的基於文本的接口。下面的例子展現了 ArrayTest 中的測試所生成的 TAP 日誌文件:

TAP version 13
ok 1 - testNewArrayIsEmpty(ArrayTest)
ok 2 - testArrayContainsAnElement(ArrayTest)
1..2

如下 TAP 日誌文件是由名爲 FailureErrorTest 的測試用例類中的兩個測試 testFailure 和 testError 所生成的,展現了失敗和錯誤是如何表示的:

TAP version 13
not ok 1 - Failure: testFailure(FailureErrorTest)
  ---
  message: 'Failed asserting that <integer:2> matches expected value <integer:1>.'
  severity: fail
  data:
    got: 2
    expected: 1
  ...
not ok 2 - Error: testError(FailureErrorTest)
1..2

測試結果 (JSON)

JavaScript 對象表示法 (JSON)是輕量級的數據交換格式。下面的例子展現了 ArrayTest 中的測試所生成的 JSON 訊息:

{"event":"suiteStart","suite":"ArrayTest","tests":2}
{"event":"test","suite":"ArrayTest",
 "test":"testNewArrayIsEmpty(ArrayTest)","status":"pass",
 "time":0.000460147858,"trace":[],"message":""}
{"event":"test","suite":"ArrayTest",
 "test":"testArrayContainsAnElement(ArrayTest)","status":"pass",
 "time":0.000422954559,"trace":[],"message":""}

如下 JSON 訊息是由名爲 FailureErrorTest 的測試用例類中的兩個測試 testFailure 和 testError 所生成的,展現了失敗和錯誤是如何表示的:

{"event":"suiteStart","suite":"FailureErrorTest","tests":2}
{"event":"test","suite":"FailureErrorTest",
 "test":"testFailure(FailureErrorTest)","status":"fail",
 "time":0.0082459449768066,"trace":[],
 "message":"Failed asserting that <integer:2> is equal to <integer:1>."}
{"event":"test","suite":"FailureErrorTest",
 "test":"testError(FailureErrorTest)","status":"error",
 "time":0.0083680152893066,"trace":[],"message":""}

代碼覆蓋率 (XML)

PHPUnit 所生成的 XML 格式代碼覆蓋率信息日誌記錄不嚴格地基於 Clover. 所使用的 XML 日誌的。下面的例子展現了 BankAccountTest 中的測試所生成的 XML 日誌文件:

<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1184835473" phpunit="3.6.0">
  <project name="BankAccountTest" timestamp="1184835473">
    <file name="/home/sb/BankAccount.php">
      <class name="BankAccountException">
        <metrics methods="0" coveredmethods="0" statements="0"
                 coveredstatements="0" elements="0" coveredelements="0"/>
      </class>
      <class name="BankAccount">
        <metrics methods="4" coveredmethods="4" statements="13"
                 coveredstatements="5" elements="17" coveredelements="9"/>
      </class>
      <line num="77" type="method" count="3"/>
      <line num="79" type="stmt" count="3"/>
      <line num="89" type="method" count="2"/>
      <line num="91" type="stmt" count="2"/>
      <line num="92" type="stmt" count="0"/>
      <line num="93" type="stmt" count="0"/>
      <line num="94" type="stmt" count="2"/>
      <line num="96" type="stmt" count="0"/>
      <line num="105" type="method" count="1"/>
      <line num="107" type="stmt" count="1"/>
      <line num="109" type="stmt" count="0"/>
      <line num="119" type="method" count="1"/>
      <line num="121" type="stmt" count="1"/>
      <line num="123" type="stmt" count="0"/>
      <metrics loc="126" ncloc="37" classes="2" methods="4" coveredmethods="4"
               statements="13" coveredstatements="5" elements="17"
               coveredelements="9"/>
    </file>
    <metrics files="1" loc="126" ncloc="37" classes="2" methods="4"
             coveredmethods="4" statements="13" coveredstatements="5"
             elements="17" coveredelements="9"/>
  </project>
</coverage>

代碼覆蓋率 (TEXT)

以易於常人瞭解(human-readable)的格式生成代碼覆蓋率,輸出到命令行或保存成文本文件。這個輸出格式旨在爲工做於少許類時提供快捷的覆蓋狀況概覽。對於更大的項目,這個輸出有助於對項目的覆蓋狀況有一個快速的概覽,或者配合 --filter 功能使用也會頗有用。若從命令行調用而且寫入到 php://stdout--colors 設置會很是好用。從命令行調用時,寫入到標準輸出是默認選項。默認狀況下,只會顯示至少有一行被覆蓋的文件。這隻能經過 XML 配置選項 showUncoveredFiles 來改變。參見 「Logging (日誌記錄)」一節。默認狀況下,在詳細報告中會顯示全部文件以及它們的覆蓋狀況。這能夠經過 XML 配置選項 showOnlySummary 來改變。.

第 14 章 擴展 PHPUnit

能夠用多種方式對 PHPUnit 進行擴展,使編寫測試更容易,以及對運行測試所獲得的反饋進行定製。擴展 PHPUnit 時,通常從這些點入手:

PHPUnit\Framework\TestCase 的子類

將自定義的斷言和工具方法寫在 PHPUnit\Framework\TestCase 的一個抽象子類中,而後從這個抽象子類派生你的測試用例類。這是擴展 PHPUnit 的最容易的方法。

編寫自定義斷言

編寫自定義斷言時,最佳實踐是遵循 PHPUnit 自有斷言的實現方式。正如 例 14.1中所示,assertTrue() 方法只是對 isTrue() 和 assertThat() 方法的外包覆:isTrue() 建立了一個匹配器對象,將其傳遞給 assertThat() 進行評定。

例 14.1: PHPUnit_Framework_Assert 類的 assertTrue() 與 isTrue() 方法

<?php
use PHPUnit\Framework\TestCase;

abstract class PHPUnit_Framework_Assert
{
    // ...

    /**
     * 斷言某個條件爲真。
     *
     * @param  boolean $condition
     * @param  string  $message
     * @throws PHPUnit_Framework_AssertionFailedError
     */
    public static function assertTrue($condition, $message = '')
    {
        self::assertThat($condition, self::isTrue(), $message);
    }

    // ...

    /**
     * 返回一個 PHPUnit_Framework_Constraint_IsTrue 匹配器對象
     *
     * @return PHPUnit_Framework_Constraint_IsTrue
     * @since  Method available since Release 3.3.0
     */
    public static function isTrue()
    {
        return new PHPUnit_Framework_Constraint_IsTrue;
    }

    // ...
}?>

例 14.2展現了 PHPUnit_Framework_Constraint_IsTrue 是如何擴展針對匹配器對象(或約束)的抽象基類 PHPUnit_Framework_Constraint 的。

例 14.2: PHPUnit_Framework_Constraint_IsTrue 類

<?php
use PHPUnit\Framework\TestCase;

class PHPUnit_Framework_Constraint_IsTrue extends PHPUnit_Framework_Constraint
{
    /**
     * 對參數 $other 進行約束評定。若是符合約束,
     * 返回 TRUE,不然返回 FALSE。
     *
     * @param mixed $other Value or object to evaluate.
     * @return bool
     */
    public function matches($other)
    {
        return $other === true;
    }

    /**
     * 返回表明此約束的字符串。
     *
     * @return string
     */
    public function toString()
    {
        return 'is true';
    }
}?>

在實現 assertTrue() 和 isTrue() 方法及 PHPUnit_Framework_Constraint_IsTrue 類時所付出的努力帶來了一些好處,assertThat() 可以自動負責起斷言的評定與任務簿記(例如爲了統計目的而對其進行計數)工做。此外, isTrue() 方法還能夠在配置仿件對象時用來做爲匹配器。

實現 PHPUnit_Framework_TestListener

例 14.3展現了 PHPUnit_Framework_TestListener 接口的一個簡單實現。

例 14.3: 簡單的測試監聽器

<?php
use PHPUnit\Framework\TestCase;

class SimpleTestListener implements PHPUnit_Framework_TestListener
{
    public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("Error while running test '%s'.\n", $test->getName());
    }

    public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time)
    {
        printf("Test '%s' failed.\n", $test->getName());
    }

    public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("Test '%s' is incomplete.\n", $test->getName());
    }

    public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("Test '%s' is deemed risky.\n", $test->getName());
    }

    public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("Test '%s' has been skipped.\n", $test->getName());
    }

    public function startTest(PHPUnit_Framework_Test $test)
    {
        printf("Test '%s' started.\n", $test->getName());
    }

    public function endTest(PHPUnit_Framework_Test $test, $time)
    {
        printf("Test '%s' ended.\n", $test->getName());
    }

    public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        printf("TestSuite '%s' started.\n", $suite->getName());
    }

    public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        printf("TestSuite '%s' ended.\n", $suite->getName());
    }
}
?>

例 14.4展現瞭如何從抽象類 PHPUnit_Framework_BaseTestListener 派生子類,這個抽象類爲全部接口方法提供了空白實現,這樣你就只須要指定那些在你的使用情境下有意義的接口方法。

例 14.4: 使用測試監聽器基類

<?php
use PHPUnit\Framework\TestCase;

class ShortTestListener extends PHPUnit_Framework_BaseTestListener
{
    public function endTest(PHPUnit_Framework_Test $test, $time)
    {
        printf("Test '%s' ended.\n", $test->getName());
    }
}
?>

「測試監聽器」一節中能夠看到如何配置 PHPUnit 來將測試監聽器附加到測試執行過程上。

從 PHPUnit_Extensions_TestDecorator 派生子類

能夠將測試用例或者測試套件包裝在 PHPUnit_Extensions_TestDecorator 的子類中並運用 Decorator(修飾器)設計模式來在測試運行先後執行一些動做。

PHPUnit 了包含了一個具體的測試修飾器:PHPUnit_Extensions_RepeatedTest。它用於重複運行某個測試,而且只在所有循環中都成功時計爲成功。

例 14.5展現了測試修飾器 PHPUnit_Extensions_RepeatedTest 的一個刪減版本,用以說明如何編寫你本身的測試修飾器。

例 14.5: RepeatedTest 修飾器

<?php
use PHPUnit\Framework\TestCase;

require_once 'PHPUnit/Extensions/TestDecorator.php';

class PHPUnit_Extensions_RepeatedTest extends PHPUnit_Extensions_TestDecorator
{
    private $timesRepeat = 1;

    public function __construct(PHPUnit_Framework_Test $test, $timesRepeat = 1)
    {
        parent::__construct($test);

        if (is_integer($timesRepeat) &&
            $timesRepeat >= 0) {
            $this->timesRepeat = $timesRepeat;
        }
    }

    public function count()
    {
        return $this->timesRepeat * $this->test->count();
    }

    public function run(PHPUnit_Framework_TestResult $result = null)
    {
        if ($result === null) {
            $result = $this->createResult();
        }

        for ($i = 0; $i < $this->timesRepeat && !$result->shouldStop(); $i++) {
            $this->test->run($result);
        }

        return $result;
    }
}
?>

實現 PHPUnit_Framework_Test

PHPUnit_Framework_Test 接口是比較狹義的,十分容易實現。舉例來講,你能夠自行爲 PHPUnit_Framework_Test 編寫一個相似於 PHPUnit\Framework\TestCase 的實現來運行數據驅動測試

例 14.6展現了一個數據驅動的測試用例類,對來自 CSV 文件內的值進行比較。這個文件內的每一個行看起來相似於 foo;bar,第一個值是指望值,第二個值則是實際值。

例 14.6: 一個數據驅動的測試

<?php
use PHPUnit\Framework\TestCase;

class DataDrivenTest implements PHPUnit_Framework_Test
{
    private $lines;

    public function __construct($dataFile)
    {
        $this->lines = file($dataFile);
    }

    public function count()
    {
        return 1;
    }

    public function run(PHPUnit_Framework_TestResult $result = null)
    {
        if ($result === null) {
            $result = new PHPUnit_Framework_TestResult;
        }

        foreach ($this->lines as $line) {
            $result->startTest($this);
            PHP_Timer::start();
            $stopTime = null;

            list($expected, $actual) = explode(';', $line);

            try {
                PHPUnit_Framework_Assert::assertEquals(
                  trim($expected), trim($actual)
                );
            }

            catch (PHPUnit_Framework_AssertionFailedError $e) {
                $stopTime = PHP_Timer::stop();
                $result->addFailure($this, $e, $stopTime);
            }

            catch (Exception $e) {
                $stopTime = PHP_Timer::stop();
                $result->addError($this, $e, $stopTime);
            }

            if ($stopTime === null) {
                $stopTime = PHP_Timer::stop();
            }

            $result->endTest($this, $stopTime);
        }

        return $result;
    }
}

$test = new DataDrivenTest('data_file.csv');
$result = PHPUnit_TextUI_TestRunner::run($test);
?>
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

.F

Time: 0 seconds

There was 1 failure:

1) DataDrivenTest
Failed asserting that two strings are equal.
expected string <bar>
difference      <  x>
got string      <baz>
/home/sb/DataDrivenTest.php:32
/home/sb/DataDrivenTest.php:53

FAILURES!
Tests: 2, Failures: 1.

附錄 A. 斷言

本附錄列舉可用的各類斷言方法。

assertArrayHasKey()

assertArrayHasKey(mixed $key, array $array[, string $message = ''])

當 $array 不包含 $key 時報告錯誤,錯誤訊息由 $message 指定。

assertArrayNotHasKey() 是與之相反的斷言,接受相同的參數。

例 A.1: assertArrayHasKey() 的用法

<?php
use PHPUnit\Framework\TestCase;

class ArrayHasKeyTest extends TestCase
{
    public function testFailure()
    {
        $this->assertArrayHasKey('foo', ['bar' => 'baz']);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ArrayHasKeyTest::testFailure
Failed asserting that an array has the key 'foo'.

/home/sb/ArrayHasKeyTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ArrayHasKeyTest

assertClassHasAttribute()

assertClassHasAttribute(string $attributeName, string $className[, string $message = ''])

當 $className::attributeName 不存在時報告錯誤,錯誤訊息由 $message 指定。

assertClassNotHasAttribute() 是與之相反的斷言,接受相同的參數。

例 A.2: assertClassHasAttribute() 的用法

<?php
use PHPUnit\Framework\TestCase;

class ClassHasAttributeTest extends TestCase
{
    public function testFailure()
    {
        $this->assertClassHasAttribute('foo', stdClass::class);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ClassHasAttributeTest::testFailure
Failed asserting that class "stdClass" has attribute "foo".

/home/sb/ClassHasAttributeTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ClassHasAttributeTest

assertArraySubset()

assertArraySubset(array $subset, array $array[, bool $strict = '', string $message = ''])

當 $array 不包含 $subset 時報告錯誤,錯誤訊息由 $message 指定。

$strict 是一個標誌,用於代表是否須要對數組中的對象進行全等斷定。

例 A.3: assertArraySubset() 的用法

<?php
use PHPUnit\Framework\TestCase;

class ArraySubsetTest extends TestCase
{
    public function testFailure()
    {
        $this->assertArraySubset(['config' => ['key-a', 'key-b']], ['config' => ['key-a']]);
    }
}
?>

PHPUnit 4.4.0 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) Epilog\EpilogTest::testNoFollowOption
Failed asserting that an array has the subset Array &0 (
    'config' => Array &1 (
        0 => 'key-a'
        1 => 'key-b'
    )
).

/home/sb/ArraySubsetTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ArrayHasKeyTest

assertClassHasStaticAttribute()

assertClassHasStaticAttribute(string $attributeName, string $className[, string $message = ''])

當 $className::attributeName 不存在時報告錯誤,錯誤訊息由 $message 指定。

assertClassNotHasStaticAttribute() 是與之相反的斷言,接受相同的參數。

例 A.4: assertClassHasStaticAttribute() 的用法

<?php
use PHPUnit\Framework\TestCase;

class ClassHasStaticAttributeTest extends TestCase
{
    public function testFailure()
    {
        $this->assertClassHasStaticAttribute('foo', stdClass::class);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ClassHasStaticAttributeTest::testFailure
Failed asserting that class "stdClass" has static attribute "foo".

/home/sb/ClassHasStaticAttributeTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ClassHasStaticAttributeTest

assertContains()

assertContains(mixed $needle, Iterator|array $haystack[, string $message = ''])

當 $needle 不是 $haystack的元素時報告錯誤,錯誤訊息由 $message 指定。

assertNotContains() 是與之相反的斷言,接受相同的參數。

assertAttributeContains() 和 assertAttributeNotContains() 是便捷包裝(convenience wrapper),以某個類或對象的 publicprotected 或 private 屬性爲搜索範圍。

例 A.5: assertContains() 的用法

<?php
use PHPUnit\Framework\TestCase;

class ContainsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertContains(4, [1, 2, 3]);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ContainsTest::testFailure
Failed asserting that an array contains 4.

/home/sb/ContainsTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ContainsTest

assertContains(string $needle, string $haystack[, string $message = '', boolean $ignoreCase = false])

當 $needle 不是 $haystack 的子字符串時報告錯誤,錯誤訊息由 $message 指定。

若是 $ignoreCase 爲 true,測試將按大小寫不敏感的方式進行。

例 A.6: assertContains() 的用法

<?php
use PHPUnit\Framework\TestCase;

class ContainsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertContains('baz', 'foobar');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ContainsTest::testFailure
Failed asserting that 'foobar' contains "baz".

/home/sb/ContainsTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ContainsTest

例 A.7: 帶有 $ignoreCase 參數的 assertContains() 的用法

<?php
use PHPUnit\Framework\TestCase;

class ContainsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertContains('foo', 'FooBar');
    }

    public function testOK()
    {
        $this->assertContains('foo', 'FooBar', '', true);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F.

Time: 0 seconds, Memory: 2.75Mb

There was 1 failure:

1) ContainsTest::testFailure
Failed asserting that 'FooBar' contains "foo".

/home/sb/ContainsTest.php:6

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.phpunit ContainsTest

assertContainsOnly()

assertContainsOnly(string $type, Iterator|array $haystack[, boolean $isNativeType = null, string $message = ''])

當 $haystack 並不是僅包含類型爲 $type 的變量時報告錯誤,錯誤訊息由 $message 指定。

$isNativeType 是一個標誌,用來代表 $type 是不是原生 PHP 類型。

assertNotContainsOnly() 是與之相反的斷言,並接受相同的參數。

assertAttributeContainsOnly() 和 assertAttributeNotContainsOnly() 是便捷包裝(convenience wrapper),以某個類或對象的 publicprotected 或 private 屬性爲搜索範圍。

例 A.8: assertContainsOnly() 的用法

<?php
use PHPUnit\Framework\TestCase;

class ContainsOnlyTest extends TestCase
{
    public function testFailure()
    {
        $this->assertContainsOnly('string', ['1', '2', 3]);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ContainsOnlyTest::testFailure
Failed asserting that Array (
    0 => '1'
    1 => '2'
    2 => 3
) contains only values of type "string".

/home/sb/ContainsOnlyTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ContainsOnlyTest

assertContainsOnlyInstancesOf()

assertContainsOnlyInstancesOf(string $classname, Traversable|array $haystack[, string $message = ''])

當 $haystack 並不是僅包含類 $classname 的實例時報告錯誤,錯誤訊息由 $message 指定。

例 A.9: assertContainsOnlyInstancesOf() 的用法

<?php
use PHPUnit\Framework\TestCase;

class ContainsOnlyInstancesOfTest extends TestCase
{
    public function testFailure()
    {
        $this->assertContainsOnlyInstancesOf(
            Foo::class,
            [new Foo, new Bar, new Foo]
        );
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ContainsOnlyInstancesOfTest::testFailure
Failed asserting that Array ([0]=> Bar Object(...)) is an instance of class "Foo".

/home/sb/ContainsOnlyInstancesOfTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ContainsOnlyInstancesOfTest

assertCount()

assertCount($expectedCount, $haystack[, string $message = ''])

當 $haystack 中的元素數量不是 $expectedCount 時報告錯誤,錯誤訊息由 $message 指定。

assertNotCount() 是與之相反的斷言,接受相同的參數。

例 A.10: assertCount() 的用法

<?php
use PHPUnit\Framework\TestCase;

class CountTest extends TestCase
{
    public function testFailure()
    {
        $this->assertCount(0, ['foo']);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) CountTest::testFailure
Failed asserting that actual size 1 matches expected size 0.

/home/sb/CountTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit CountTest

assertDirectoryExists()

assertDirectoryExists(string $directory[, string $message = ''])

當 $directory 所指定的目錄不存在時報告錯誤,錯誤訊息由 $message 指定。

assertDirectoryNotExists() 是與之相反的斷言,並接受相同的參數。

例 A.11: assertDirectoryExists() 的用法

<?php
use PHPUnit\Framework\TestCase;

class DirectoryExistsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertDirectoryExists('/path/to/directory');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) DirectoryExistsTest::testFailure
Failed asserting that directory "/path/to/directory" exists.

/home/sb/DirectoryExistsTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit DirectoryExistsTest

assertDirectoryIsReadable()

assertDirectoryIsReadable(string $directory[, string $message = ''])

當 $directory 所指定的目錄不是個目錄或不可讀時報告錯誤,錯誤訊息由 $message 指定。

assertDirectoryNotIsReadable() 是與之相反的斷言,並接受相同的參數。

例 A.12: assertDirectoryIsReadable() 的用法

<?php
use PHPUnit\Framework\TestCase;

class DirectoryIsReadableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertDirectoryIsReadable('/path/to/directory');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) DirectoryIsReadableTest::testFailure
Failed asserting that "/path/to/directory" is readable.

/home/sb/DirectoryIsReadableTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit DirectoryIsReadableTest

assertDirectoryIsWritable()

assertDirectoryIsWritable(string $directory[, string $message = ''])

當 $directory 所指定的目錄不是個目錄或不可寫時報告錯誤,錯誤訊息由 $message 指定。

assertDirectoryNotIsWritable() 是與之相反的斷言,並接受相同的參數。

例 A.13: assertDirectoryIsWritable() 的用法

<?php
use PHPUnit\Framework\TestCase;

class DirectoryIsWritableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertDirectoryIsWritable('/path/to/directory');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) DirectoryIsWritableTest::testFailure
Failed asserting that "/path/to/directory" is writable.

/home/sb/DirectoryIsWritableTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit DirectoryIsWritableTest

assertEmpty()

assertEmpty(mixed $actual[, string $message = ''])

當 $actual 非空時報告錯誤,錯誤訊息由 $message 指定。

assertNotEmpty() 是與之相反的斷言,接受相同的參數。

assertAttributeEmpty() 和 assertAttributeNotEmpty() 是便捷包裝(convenience wrapper),能夠應用於某個類或對象的某個 publicprotected 或 private 屬性。

例 A.14: assertEmpty() 的用法

<?php
use PHPUnit\Framework\TestCase;

class EmptyTest extends TestCase
{
    public function testFailure()
    {
        $this->assertEmpty(['foo']);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) EmptyTest::testFailure
Failed asserting that an array is empty.

/home/sb/EmptyTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit EmptyTest

assertEqualXMLStructure()

assertEqualXMLStructure(DOMElement $expectedElement, DOMElement $actualElement[, boolean $checkAttributes = false, string $message = ''])

當 $actualElement 中 DOMElement 的 XML 結構與 $expectedElement 中 DOMElement的 XML 結構不相同時報告錯誤,錯誤訊息由 $message 指定。

例 A.15: assertEqualXMLStructure() 的用法

<?php
use PHPUnit\Framework\TestCase;

class EqualXMLStructureTest extends TestCase
{
    public function testFailureWithDifferentNodeNames()
    {
        $expected = new DOMElement('foo');
        $actual = new DOMElement('bar');

        $this->assertEqualXMLStructure($expected, $actual);
    }

    public function testFailureWithDifferentNodeAttributes()
    {
        $expected = new DOMDocument;
        $expected->loadXML('<foo bar="true" />');

        $actual = new DOMDocument;
        $actual->loadXML('<foo/>');

        $this->assertEqualXMLStructure(
          $expected->firstChild, $actual->firstChild, true
        );
    }

    public function testFailureWithDifferentChildrenCount()
    {
        $expected = new DOMDocument;
        $expected->loadXML('<foo><bar/><bar/><bar/></foo>');

        $actual = new DOMDocument;
        $actual->loadXML('<foo><bar/></foo>');

        $this->assertEqualXMLStructure(
          $expected->firstChild, $actual->firstChild
        );
    }

    public function testFailureWithDifferentChildren()
    {
        $expected = new DOMDocument;
        $expected->loadXML('<foo><bar/><bar/><bar/></foo>');

        $actual = new DOMDocument;
        $actual->loadXML('<foo><baz/><baz/><baz/></foo>');

        $this->assertEqualXMLStructure(
          $expected->firstChild, $actual->firstChild
        );
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

FFFF

Time: 0 seconds, Memory: 5.75Mb

There were 4 failures:

1) EqualXMLStructureTest::testFailureWithDifferentNodeNames
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'foo'
+'bar'

/home/sb/EqualXMLStructureTest.php:9

2) EqualXMLStructureTest::testFailureWithDifferentNodeAttributes
Number of attributes on node "foo" does not match
Failed asserting that 0 matches expected 1.

/home/sb/EqualXMLStructureTest.php:22

3) EqualXMLStructureTest::testFailureWithDifferentChildrenCount
Number of child nodes of "foo" differs
Failed asserting that 1 matches expected 3.

/home/sb/EqualXMLStructureTest.php:35

4) EqualXMLStructureTest::testFailureWithDifferentChildren
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'

/home/sb/EqualXMLStructureTest.php:48

FAILURES!
Tests: 4, Assertions: 8, Failures: 4.phpunit EqualXMLStructureTest

assertEquals()

assertEquals(mixed $expected, mixed $actual[, string $message = ''])

當兩個變量 $expected 和 $actual 不相等時報告錯誤,錯誤訊息由 $message 指定。

assertNotEquals() 是與之相反的斷言,接受相同的參數。

assertAttributeEquals() 和 assertAttributeNotEquals() 是便捷包裝(convenience wrapper),以某個類或對象的某個 publicprotected 或 private 屬性做爲實際值來進行比較。

例 A.16: assertEquals() 的用法

<?php
use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertEquals(1, 0);
    }

    public function testFailure2()
    {
        $this->assertEquals('bar', 'baz');
    }

    public function testFailure3()
    {
        $this->assertEquals("foo\nbar\nbaz\n", "foo\nbah\nbaz\n");
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

FFF

Time: 0 seconds, Memory: 5.25Mb

There were 3 failures:

1) EqualsTest::testFailure
Failed asserting that 0 matches expected 1.

/home/sb/EqualsTest.php:6

2) EqualsTest::testFailure2
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'

/home/sb/EqualsTest.php:11

3) EqualsTest::testFailure3
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
 'foo
-bar
+bah
 baz
 '

/home/sb/EqualsTest.php:16

FAILURES!
Tests: 3, Assertions: 3, Failures: 3.phpunit EqualsTest

若是 $expected 和 $actual 是某些特定的類型,將使用更加專門的比較方式,參閱下文。

assertEquals(float $expected, float $actual[, string $message = '', float $delta = 0])

當兩個浮點數 $expected 和 $actual 之間的差值(的絕對值)大於 $delta 時報告錯誤,錯誤訊息由 $message 指定。

關於爲何 $delta 參數是必須的,請閱讀《關於浮點運算,每一位計算機科學從業人員都應該知道的事實》。

例 A.17: 將assertEquals()用於浮點數時的用法

<?php
use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase
{
    public function testSuccess()
    {
        $this->assertEquals(1.0, 1.1, '', 0.2);
    }

    public function testFailure()
    {
        $this->assertEquals(1.0, 1.1);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

.F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) EqualsTest::testFailure
Failed asserting that 1.1 matches expected 1.0.

/home/sb/EqualsTest.php:11

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.phpunit EqualsTest

assertEquals(DOMDocument $expected, DOMDocument $actual[, string $message = ''])

當 $expected 和 $actual 這兩個 DOMDocument 對象所表示的 XML 文檔對應的無註釋規範形式不相同時報告錯誤,錯誤訊息由 $message 指定。

例 A.18: assertEquals()應用於 DOMDocument 對象時的用法

<?php
use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase
{
    public function testFailure()
    {
        $expected = new DOMDocument;
        $expected->loadXML('<foo><bar/></foo>');

        $actual = new DOMDocument;
        $actual->loadXML('<bar><foo/></bar>');

        $this->assertEquals($expected, $actual);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) EqualsTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
 <?xml version="1.0"?>
-<foo>
-  <bar/>
-</foo>
+<bar>
+  <foo/>
+</bar>

/home/sb/EqualsTest.php:12

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit EqualsTest

assertEquals(object $expected, object $actual[, string $message = ''])

當 $expected 和 $actual 這兩個對象的屬性值不相等時報告錯誤,錯誤訊息由 $message 指定。

例 A.19: assertEquals()應用於對象時的用法

<?php
use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase
{
    public function testFailure()
    {
        $expected = new stdClass;
        $expected->foo = 'foo';
        $expected->bar = 'bar';

        $actual = new stdClass;
        $actual->foo = 'bar';
        $actual->baz = 'bar';

        $this->assertEquals($expected, $actual);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) EqualsTest::testFailure
Failed asserting that two objects are equal.
--- Expected
+++ Actual
@@ @@
 stdClass Object (
-    'foo' => 'foo'
-    'bar' => 'bar'
+    'foo' => 'bar'
+    'baz' => 'bar'
 )

/home/sb/EqualsTest.php:14

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit EqualsTest

assertEquals(array $expected, array $actual[, string $message = ''])

當 $expected 和 $actual 這兩個數組不相等時報告錯誤,錯誤訊息由 $message 指定。

例 A.20: assertEquals() 應用於數組時的用法

<?php
use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertEquals(['a', 'b', 'c'], ['a', 'c', 'd']);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) EqualsTest::testFailure
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
     0 => 'a'
-    1 => 'b'
-    2 => 'c'
+    1 => 'c'
+    2 => 'd'
 )

/home/sb/EqualsTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit EqualsTest

assertFalse()

assertFalse(bool $condition[, string $message = ''])

當 $condition 爲 true 時報告錯誤,錯誤訊息由 $message 指定。

assertNotFalse() 是與之相反的斷言,接受相同的參數。

例 A.21: assertFalse() 的用法

<?php
use PHPUnit\Framework\TestCase;

class FalseTest extends TestCase
{
    public function testFailure()
    {
        $this->assertFalse(true);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) FalseTest::testFailure
Failed asserting that true is false.

/home/sb/FalseTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit FalseTest

assertFileEquals()

assertFileEquals(string $expected, string $actual[, string $message = ''])

當 $expected 所指定的文件與 $actual 所指定的文件內容不一樣時報告錯誤,錯誤訊息由 $message 指定。

assertFileNotEquals() 是與之相反的斷言,接受相同的參數。

例 A.22: assertFileEquals() 的用法

<?php
use PHPUnit\Framework\TestCase;

class FileEqualsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertFileEquals('/home/sb/expected', '/home/sb/actual');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) FileEqualsTest::testFailure
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'expected
+'actual
 '

/home/sb/FileEqualsTest.php:6

FAILURES!
Tests: 1, Assertions: 3, Failures: 1.phpunit FileEqualsTest

assertFileExists()

assertFileExists(string $filename[, string $message = ''])

當 $filename 所指定的文件不存在時報告錯誤,錯誤訊息由 $message 指定。

assertFileNotExists() 是與之相反的斷言,接受相同的參數。

例 A.23: assertFileExists() 的用法

<?php
use PHPUnit\Framework\TestCase;

class FileExistsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertFileExists('/path/to/file');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) FileExistsTest::testFailure
Failed asserting that file "/path/to/file" exists.

/home/sb/FileExistsTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit FileExistsTest

assertFileIsReadable()

assertFileIsReadable(string $filename[, string $message = ''])

當 $filename 所指定的文件不是個文件或不可讀時報告錯誤,錯誤訊息由 $message 指定。

assertFileNotIsReadable() 是與之相反的斷言,並接受相同的參數。

例 A.24: assertFileIsReadable() 的用法

<?php
use PHPUnit\Framework\TestCase;

class FileIsReadableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertFileIsReadable('/path/to/file');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) FileIsReadableTest::testFailure
Failed asserting that "/path/to/file" is readable.

/home/sb/FileIsReadableTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit FileIsReadableTest

assertFileIsWritable()

assertFileIsWritable(string $filename[, string $message = ''])

當 $filename 所指定的文件不是個文件或不可寫時報告錯誤,錯誤訊息由 $message 指定。

assertFileNotIsWritable() 是與之相反的斷言,並接受相同的參數。

例 A.25: assertFileIsWritable() 的用法

<?php
use PHPUnit\Framework\TestCase;

class FileIsWritableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertFileIsWritable('/path/to/file');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) FileIsWritableTest::testFailure
Failed asserting that "/path/to/file" is writable.

/home/sb/FileIsWritableTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit FileIsWritableTest

assertGreaterThan()

assertGreaterThan(mixed $expected, mixed $actual[, string $message = ''])

當 $actual 的值不大於 $expected 的值時報告錯誤,錯誤訊息由 $message 指定。

assertAttributeGreaterThan() 是便捷包裝(convenience wrapper),以某個類或對象的某個 publicprotected 或 private 屬性做爲實際值來進行比較。

例 A.26: assertGreaterThan() 的用法

<?php
use PHPUnit\Framework\TestCase;

class GreaterThanTest extends TestCase
{
    public function testFailure()
    {
        $this->assertGreaterThan(2, 1);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) GreaterThanTest::testFailure
Failed asserting that 1 is greater than 2.

/home/sb/GreaterThanTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit GreaterThanTest

assertGreaterThanOrEqual()

assertGreaterThanOrEqual(mixed $expected, mixed $actual[, string $message = ''])

當 $actual 的值不大於且不等於 $expected 的值時報告錯誤,錯誤訊息由 $message 指定。

assertAttributeGreaterThanOrEqual() 是便捷包裝(convenience wrapper),以某個類或對象的某個 publicprotected 或 private 屬性做爲實際值來進行比較。

例 A.27: assertGreaterThanOrEqual() 的用法

<?php
use PHPUnit\Framework\TestCase;

class GreatThanOrEqualTest extends TestCase
{
    public function testFailure()
    {
        $this->assertGreaterThanOrEqual(2, 1);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) GreatThanOrEqualTest::testFailure
Failed asserting that 1 is equal to 2 or is greater than 2.

/home/sb/GreaterThanOrEqualTest.php:6

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.phpunit GreaterThanOrEqualTest

assertInfinite()

assertInfinite(mixed $variable[, string $message = ''])

當 $actual 不是 INF 時報告錯誤,錯誤訊息由 $message 指定。

assertFinite() 是與之相反的斷言,接受相同的參數。

例 A.28: assertInfinite() 的用法

<?php
use PHPUnit\Framework\TestCase;

class InfiniteTest extends TestCase
{
    public function testFailure()
    {
        $this->assertInfinite(1);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) InfiniteTest::testFailure
Failed asserting that 1 is infinite.

/home/sb/InfiniteTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit InfiniteTest

assertInstanceOf()

assertInstanceOf($expected, $actual[, $message = ''])

當 $actual 不是 $expected 的實例時報告錯誤,錯誤訊息由 $message 指定。

assertNotInstanceOf() 是與之相反的斷言,接受相同的參數。

assertAttributeInstanceOf() 和 assertAttributeNotInstanceOf() 是便捷包裝(convenience wrapper),能夠應用於某個類或對象的某個 publicprotected 或 private 屬性。

例 A.29: assertInstanceOf() 的用法

<?php
use PHPUnit\Framework\TestCase;

class InstanceOfTest extends TestCase
{
    public function testFailure()
    {
        $this->assertInstanceOf(RuntimeException::class, new Exception);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) InstanceOfTest::testFailure
Failed asserting that Exception Object (...) is an instance of class "RuntimeException".

/home/sb/InstanceOfTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit InstanceOfTest

assertInternalType()

assertInternalType($expected, $actual[, $message = ''])

當 $actual 不是 $expected 所指明的類型時報告錯誤,錯誤訊息由 $message 指定。

assertNotInternalType() 是與之相反的斷言,接受相同的參數。

assertAttributeInternalType() 和 assertAttributeNotInternalType() 是便捷包裝(convenience wrapper),能夠應用於某個類或對象的某個 publicprotected 或 private 屬性。

例 A.30: assertInternalType() 的用法

<?php
use PHPUnit\Framework\TestCase;

class InternalTypeTest extends TestCase
{
    public function testFailure()
    {
        $this->assertInternalType('string', 42);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) InternalTypeTest::testFailure
Failed asserting that 42 is of type "string".

/home/sb/InternalTypeTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit InternalTypeTest

assertIsReadable()

assertIsReadable(string $filename[, string $message = ''])

當 $filename 所指定的文件或目錄不可讀時報告錯誤,錯誤訊息由 $message 指定。

assertNotIsReadable() 是與之相反的斷言,並接受相同的參數。

例 A.31: assertIsReadable() 的用法

<?php
use PHPUnit\Framework\TestCase;

class IsReadableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertIsReadable('/path/to/unreadable');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) IsReadableTest::testFailure
Failed asserting that "/path/to/unreadable" is readable.

/home/sb/IsReadableTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit IsReadableTest

assertIsWritable()

assertIsWritable(string $filename[, string $message = ''])

當 $filename 所指定的文件或目錄不可寫時報告錯誤,錯誤訊息由 $message 指定。

assertNotIsWritable() 是與之相反的斷言,並接受相同的參數。

例 A.32: assertIsWritable() 的用法

<?php
use PHPUnit\Framework\TestCase;

class IsWritableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertIsWritable('/path/to/unwritable');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) IsWritableTest::testFailure
Failed asserting that "/path/to/unwritable" is writable.

/home/sb/IsWritableTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit IsWritableTest

assertJsonFileEqualsJsonFile()

assertJsonFileEqualsJsonFile(mixed $expectedFile, mixed $actualFile[, string $message = ''])

當 $actualFile 對應的值與 $expectedFile 對應的值不匹配時報告錯誤,錯誤訊息由 $message 指定。

例 A.33: assertJsonFileEqualsJsonFile() 的用法

<?php
use PHPUnit\Framework\TestCase;

class JsonFileEqualsJsonFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertJsonFileEqualsJsonFile(
          'path/to/fixture/file', 'path/to/actual/file');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) JsonFileEqualsJsonFile::testFailure
Failed asserting that '{"Mascot":"Tux"}' matches JSON string "["Mascott", "Tux", "OS", "Linux"]".

/home/sb/JsonFileEqualsJsonFileTest.php:5

FAILURES!
Tests: 1, Assertions: 3, Failures: 1.phpunit JsonFileEqualsJsonFileTest

assertJsonStringEqualsJsonFile()

assertJsonStringEqualsJsonFile(mixed $expectedFile, mixed $actualJson[, string $message = ''])

當 $actualJson 對應的值與 $expectedFile 對應的值不匹配時報告錯誤,錯誤訊息由 $message 指定。

例 A.34: assertJsonStringEqualsJsonFile() 的用法

<?php
use PHPUnit\Framework\TestCase;

class JsonStringEqualsJsonFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertJsonStringEqualsJsonFile(
            'path/to/fixture/file', json_encode(['Mascot' => 'ux'])
        );
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) JsonStringEqualsJsonFile::testFailure
Failed asserting that '{"Mascot":"ux"}' matches JSON string "{"Mascott":"Tux"}".

/home/sb/JsonStringEqualsJsonFileTest.php:5

FAILURES!
Tests: 1, Assertions: 3, Failures: 1.phpunit JsonStringEqualsJsonFileTest

assertJsonStringEqualsJsonString()

assertJsonStringEqualsJsonString(mixed $expectedJson, mixed $actualJson[, string $message = ''])

當 $actualJson 對應的值與 $expectedJson 對應的值不匹配時報告錯誤,錯誤訊息由 $message 指定。

例 A.35: assertJsonStringEqualsJsonString() 的用法

<?php
use PHPUnit\Framework\TestCase;

class JsonStringEqualsJsonStringTest extends TestCase
{
    public function testFailure()
    {
        $this->assertJsonStringEqualsJsonString(
            json_encode(['Mascot' => 'Tux']),
            json_encode(['Mascot' => 'ux'])
        );
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) JsonStringEqualsJsonStringTest::testFailure
Failed asserting that two objects are equal.
--- Expected
+++ Actual
@@ @@
 stdClass Object (
 -    'Mascot' => 'Tux'
 +    'Mascot' => 'ux'
)

/home/sb/JsonStringEqualsJsonStringTest.php:5

FAILURES!
Tests: 1, Assertions: 3, Failures: 1.phpunit JsonStringEqualsJsonStringTest

assertLessThan()

assertLessThan(mixed $expected, mixed $actual[, string $message = ''])

當 $actual 的值不小於 $expected 的值時報告錯誤,錯誤訊息由 $message 指定。

assertAttributeLessThan() 是便捷包裝(convenience wrapper),以某個類或對象的某個 publicprotected 或 private 屬性做爲實際值來進行比較。

例 A.36: assertLessThan() 的用法

<?php
use PHPUnit\Framework\TestCase;

class LessThanTest extends TestCase
{
    public function testFailure()
    {
        $this->assertLessThan(1, 2);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) LessThanTest::testFailure
Failed asserting that 2 is less than 1.

/home/sb/LessThanTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit LessThanTest

assertLessThanOrEqual()

assertLessThanOrEqual(mixed $expected, mixed $actual[, string $message = ''])

當 $actual 的值不小於且不等於 $expected 的值時報告錯誤,錯誤訊息由 $message 指定。

assertAttributeLessThanOrEqual() 是便捷包裝(convenience wrapper),以某個類或對象的某個 publicprotected 或 private 屬性做爲實際值來進行比較。

例 A.37: assertLessThanOrEqual() 的用法

<?php
use PHPUnit\Framework\TestCase;

class LessThanOrEqualTest extends TestCase
{
    public function testFailure()
    {
        $this->assertLessThanOrEqual(1, 2);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) LessThanOrEqualTest::testFailure
Failed asserting that 2 is equal to 1 or is less than 1.

/home/sb/LessThanOrEqualTest.php:6

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.phpunit LessThanOrEqualTest

assertNan()

assertNan(mixed $variable[, string $message = ''])

當 $variable 不是 NAN 時報告錯誤,錯誤訊息由 $message 指定。

例 A.38: assertNan() 的用法

<?php
use PHPUnit\Framework\TestCase;

class NanTest extends TestCase
{
    public function testFailure()
    {
        $this->assertNan(1);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) NanTest::testFailure
Failed asserting that 1 is nan.

/home/sb/NanTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit NanTest

assertNull()

assertNull(mixed $variable[, string $message = ''])

當 $actual 不是 null 時報告錯誤,錯誤訊息由 $message 指定。

assertNotNull() 是與之相反的斷言,接受相同的參數。

例 A.39: assertNull() 的使用

<?php
use PHPUnit\Framework\TestCase;

class NullTest extends TestCase
{
    public function testFailure()
    {
        $this->assertNull('foo');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) NullTest::testFailure
Failed asserting that 'foo' is null.

/home/sb/NotNullTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit NotNullTest

assertObjectHasAttribute()

assertObjectHasAttribute(string $attributeName, object $object[, string $message = ''])

當 $object->attributeName 不存在時報告錯誤,錯誤訊息由 $message 指定。

assertObjectNotHasAttribute() 是與之相反的斷言,接受相同的參數。

例 A.40: assertObjectHasAttribute() 的用法

<?php
use PHPUnit\Framework\TestCase;

class ObjectHasAttributeTest extends TestCase
{
    public function testFailure()
    {
        $this->assertObjectHasAttribute('foo', new stdClass);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ObjectHasAttributeTest::testFailure
Failed asserting that object of class "stdClass" has attribute "foo".

/home/sb/ObjectHasAttributeTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit ObjectHasAttributeTest

assertRegExp()

assertRegExp(string $pattern, string $string[, string $message = ''])

當 $string 不匹配於正則表達式 $pattern 時報告錯誤,錯誤訊息由 $message 指定。

assertNotRegExp() 是與之相反的斷言,接受相同的參數。

例 A.41: assertRegExp() 的用法

<?php
use PHPUnit\Framework\TestCase;

class RegExpTest extends TestCase
{
    public function testFailure()
    {
        $this->assertRegExp('/foo/', 'bar');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) RegExpTest::testFailure
Failed asserting that 'bar' matches PCRE pattern "/foo/".

/home/sb/RegExpTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit RegExpTest

assertStringMatchesFormat()

assertStringMatchesFormat(string $format, string $string[, string $message = ''])

當 $string 不匹配於 $format 定義的格式時報告錯誤,錯誤訊息由 $message 指定。

assertStringNotMatchesFormat() 是與之相反的斷言,接受相同的參數。

例 A.42: assertStringMatchesFormat() 的用法

<?php
use PHPUnit\Framework\TestCase;

class StringMatchesFormatTest extends TestCase
{
    public function testFailure()
    {
        $this->assertStringMatchesFormat('%i', 'foo');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) StringMatchesFormatTest::testFailure
Failed asserting that 'foo' matches PCRE pattern "/^[+-]?\d+$/s".

/home/sb/StringMatchesFormatTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit StringMatchesFormatTest

格式定義字符串中能夠使用以下佔位符:

  • %e:表示目錄分隔符,例如在 Linux 系統中是 /

  • %s:一個或多個除了換行符之外的任意字符(非空白字符或者空白字符)。

  • %S:零個或多個除了換行符之外的任意字符(非空白字符或者空白字符)。

  • %a:一個或多個包括換行符在內的任意字符(非空白字符或者空白字符)。

  • %A:零個或多個包括換行符在內的任意字符(非空白字符或者空白字符)。

  • %w:零個或多個空白字符。

  • %i:帶符號整數值,例如 +3142-3142

  • %d:無符號整數值,例如 123456

  • %x:一個或多個十六進制字符。所謂十六進制字符,指的是在如下範圍內的字符:0-9a-fA-F

  • %f:浮點數,例如 3.142-3.1423.142E-103.142e+10

  • %c:單個任意字符。

assertStringMatchesFormatFile()

assertStringMatchesFormatFile(string $formatFile, string $string[, string $message = ''])

當 $string 不匹配於 $formatFile 的內容所定義的格式時報告錯誤,錯誤訊息由 $message 指定。

assertStringNotMatchesFormatFile() 是與之相反的斷言,接受相同的參數。

例 A.43: assertStringMatchesFormatFile() 的用法

<?php
use PHPUnit\Framework\TestCase;

class StringMatchesFormatFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertStringMatchesFormatFile('/path/to/expected.txt', 'foo');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) StringMatchesFormatFileTest::testFailure
Failed asserting that 'foo' matches PCRE pattern "/^[+-]?\d+
$/s".

/home/sb/StringMatchesFormatFileTest.php:6

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.phpunit StringMatchesFormatFileTest

assertSame()

assertSame(mixed $expected, mixed $actual[, string $message = ''])

當兩個變量 $expected 和 $actual 的值與類型不徹底相同時報告錯誤,錯誤訊息由 $message 指定。

assertNotSame() 是與之相反的斷言,接受相同的參數。

assertAttributeSame() 和 assertAttributeNotSame() 是便捷包裝(convenience wrapper),以某個類或對象的某個 publicprotected 或 private 屬性做爲實際值來進行比較。

例 A.44: assertSame() 的用法

<?php
use PHPUnit\Framework\TestCase;

class SameTest extends TestCase
{
    public function testFailure()
    {
        $this->assertSame('2204', 2204);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) SameTest::testFailure
Failed asserting that 2204 is identical to '2204'.

/home/sb/SameTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit SameTest

assertSame(object $expected, object $actual[, string $message = ''])

當兩個變量 $expected 和 $actual 不是指向同一個對象的引用時報告錯誤,錯誤訊息由 $message 指定。

例 A.45: assertSame() 應用於對象時的用法

<?php
use PHPUnit\Framework\TestCase;

class SameTest extends TestCase
{
    public function testFailure()
    {
        $this->assertSame(new stdClass, new stdClass);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) SameTest::testFailure
Failed asserting that two variables reference the same object.

/home/sb/SameTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit SameTest

assertStringEndsWith()

assertStringEndsWith(string $suffix, string $string[, string $message = ''])

當 $string 不以 $suffix 結尾時報告錯誤,錯誤訊息由 $message 指定。

assertStringEndsNotWith() 是與之相反的斷言,接受相同的參數。

例 A.46: assertStringEndsWith() 的用法

<?php
use PHPUnit\Framework\TestCase;

class StringEndsWithTest extends TestCase
{
    public function testFailure()
    {
        $this->assertStringEndsWith('suffix', 'foo');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 1 second, Memory: 5.00Mb

There was 1 failure:

1) StringEndsWithTest::testFailure
Failed asserting that 'foo' ends with "suffix".

/home/sb/StringEndsWithTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit StringEndsWithTest

assertStringEqualsFile()

assertStringEqualsFile(string $expectedFile, string $actualString[, string $message = ''])

當 $expectedFile 所指定的文件其內容不是 $actualString 時報告錯誤,錯誤訊息由 $message 指定。

assertStringNotEqualsFile() 是與之相反的斷言,接受相同的參數。

例 A.47: assertStringEqualsFile() 的用法

<?php
use PHPUnit\Framework\TestCase;

class StringEqualsFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertStringEqualsFile('/home/sb/expected', 'actual');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) StringEqualsFileTest::testFailure
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'expected
-'
+'actual'

/home/sb/StringEqualsFileTest.php:6

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.phpunit StringEqualsFileTest

assertStringStartsWith()

assertStringStartsWith(string $prefix, string $string[, string $message = ''])

當 $string 不以 $prefix 開頭時報告錯誤,錯誤訊息由 $message 指定。

assertStringStartsNotWith() 是與之相反的斷言,並接受相同的參數。

例 A.48: assertStringStartsWith() 的用法

<?php
use PHPUnit\Framework\TestCase;

class StringStartsWithTest extends TestCase
{
    public function testFailure()
    {
        $this->assertStringStartsWith('prefix', 'foo');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) StringStartsWithTest::testFailure
Failed asserting that 'foo' starts with "prefix".

/home/sb/StringStartsWithTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit StringStartsWithTest

assertThat()

能夠用 PHPUnit_Framework_Constraint 類來訂立更加複雜的斷言。隨後能夠用 assertThat() 方法來評定這些斷言。例 A.49 展現瞭如何用 logicalNot() 和 equalTo() 約束條件來表達與assertNotEquals() 等價的斷言。

assertThat(mixed $value, PHPUnit_Framework_Constraint $constraint[, $message = ''])

當 $value 不符合約束條件 $constraint 時報告錯誤,錯誤訊息由 $message 指定。

例 A.49: assertThat() 的用法

<?php
use PHPUnit\Framework\TestCase;

class BiscuitTest extends TestCase
{
    public function testEquals()
    {
        $theBiscuit = new Biscuit('Ginger');
        $myBiscuit  = new Biscuit('Ginger');

        $this->assertThat(
          $theBiscuit,
          $this->logicalNot(
            $this->equalTo($myBiscuit)
          )
        );
    }
}
?>

表 A.1列舉了全部可用的 PHPUnit_Framework_Constraint 類。

表 A.1. 約束條件

約束條件 含義
PHPUnit_Framework_Constraint_Attribute attribute(PHPUnit_Framework_Constraint $constraint, $attributeName) 此約束將另一個約束應用於某個類或對象的某個屬性。
PHPUnit_Framework_Constraint_IsAnything anything() 此約束接受任意輸入值。
PHPUnit_Framework_Constraint_ArrayHasKey arrayHasKey(mixed $key) 此約束斷言所評定的數組擁有指定鍵名。
PHPUnit_Framework_Constraint_TraversableContains contains(mixed $value) 此約束斷言所評定的 array 或實現了 Iterator 接口的對象包含有給定值。
PHPUnit_Framework_Constraint_TraversableContainsOnly containsOnly(string $type) 此約束斷言所評定的 array 或實現了 Iterator 接口的對象僅包含給定類型的值。
PHPUnit_Framework_Constraint_TraversableContainsOnly containsOnlyInstancesOf(string $classname) 此約束斷言所評定的 array 或實現了 Iterator 接口的對象僅包含給定類名的類的實例。
PHPUnit_Framework_Constraint_IsEqual equalTo($value, $delta = 0, $maxDepth = 10) 此約束檢驗一個值是否等於另一個。
PHPUnit_Framework_Constraint_Attribute attributeEqualTo($attributeName, $value, $delta = 0, $maxDepth = 10) 此約束檢驗一個值是否等於某個類或對象的某個屬性。
PHPUnit_Framework_Constraint_DirectoryExists directoryExists() 此約束檢驗所評定的目錄是否存在。
PHPUnit_Framework_Constraint_FileExists fileExists() 此約束檢驗所評定的文件名對應的文件是否存在。
PHPUnit_Framework_Constraint_IsReadable isReadable() 此約束檢驗所評定的文件名對應的文件是否可讀。
PHPUnit_Framework_Constraint_IsWritable isWritable() 此約束檢驗所評定的文件名對應的文件是否可寫。
PHPUnit_Framework_Constraint_GreaterThan greaterThan(mixed $value) 此約束斷言所評定的值大於給定值。
PHPUnit_Framework_Constraint_Or greaterThanOrEqual(mixed $value) 此約束斷言所評定的值大於或等於給定值。
PHPUnit_Framework_Constraint_ClassHasAttribute classHasAttribute(string $attributeName) 此約束斷言所評定的類具備給定屬性。
PHPUnit_Framework_Constraint_ClassHasStaticAttribute classHasStaticAttribute(string $attributeName) 此約束斷言所評定的類具備給定靜態屬性。
PHPUnit_Framework_Constraint_ObjectHasAttribute hasAttribute(string $attributeName) 此約束斷言所評定的對象具備給定屬性。
PHPUnit_Framework_Constraint_IsIdentical identicalTo(mixed $value) 此約束斷言所評定的值與另一個值全等。
PHPUnit_Framework_Constraint_IsFalse isFalse() 此約束斷言所評定的值爲 false
PHPUnit_Framework_Constraint_IsInstanceOf isInstanceOf(string $className) 此約束斷言所評定的對象是給定類的實例。
PHPUnit_Framework_Constraint_IsNull isNull() 此約束斷言所評定的值爲 null
PHPUnit_Framework_Constraint_IsTrue isTrue() 此約束斷言所評定的值爲 true
PHPUnit_Framework_Constraint_IsType isType(string $type) 此約束斷言所評定的值是指定類型的。
PHPUnit_Framework_Constraint_LessThan lessThan(mixed $value) 此約束斷言所評定的值小於給定值。
PHPUnit_Framework_Constraint_Or lessThanOrEqual(mixed $value) 此約束斷言所評定的值小於或等於給定值。
logicalAnd() 邏輯與(AND)。
logicalNot(PHPUnit_Framework_Constraint $constraint) 邏輯非(NOT)。
logicalOr() 邏輯或(OR)。
logicalXor() 邏輯異或(XOR)。
PHPUnit_Framework_Constraint_PCREMatch matchesRegularExpression(string $pattern) 此約束斷言所評定的字符串匹配於正則表達式。
PHPUnit_Framework_Constraint_StringContains stringContains(string $string, bool $case) 此約束斷言所評定的字符串包含指定字符串。
PHPUnit_Framework_Constraint_StringEndsWith stringEndsWith(string $suffix) 此約束斷言所評定的字符串以給定後綴結尾。
PHPUnit_Framework_Constraint_StringStartsWith stringStartsWith(string $prefix) 此約束斷言所評定的字符串以給定前綴開頭。

assertTrue()

assertTrue(bool $condition[, string $message = ''])

當 $condition 爲 false 時報告錯誤,錯誤訊息由 $message 指定。

assertNotTrue() 是與之相反的斷言,接受相同的參數。

例 A.50: assertTrue() 的用法

<?php
use PHPUnit\Framework\TestCase;

class TrueTest extends TestCase
{
    public function testFailure()
    {
        $this->assertTrue(false);
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) TrueTest::testFailure
Failed asserting that false is true.

/home/sb/TrueTest.php:6

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit TrueTest

assertXmlFileEqualsXmlFile()

assertXmlFileEqualsXmlFile(string $expectedFile, string $actualFile[, string $message = ''])

當 $actualFile 對應的 XML 文檔與 $expectedFile 對應的 XML 文檔不相同時報告錯誤,錯誤訊息由 $message 指定。

assertXmlFileNotEqualsXmlFile() 是與之相反的斷言,接受相同的參數。

例 A.51: assertXmlFileEqualsXmlFile() 的用法

<?php
use PHPUnit\Framework\TestCase;

class XmlFileEqualsXmlFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertXmlFileEqualsXmlFile(
          '/home/sb/expected.xml', '/home/sb/actual.xml');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) XmlFileEqualsXmlFileTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
 <?xml version="1.0"?>
 <foo>
-  <bar/>
+  <baz/>
 </foo>

/home/sb/XmlFileEqualsXmlFileTest.php:7

FAILURES!
Tests: 1, Assertions: 3, Failures: 1.phpunit XmlFileEqualsXmlFileTest

assertXmlStringEqualsXmlFile()

assertXmlStringEqualsXmlFile(string $expectedFile, string $actualXml[, string $message = ''])

當 $actualXml 對應的 XML 文檔與 $expectedFile 對應的 XML 文檔不相同時報告錯誤,錯誤訊息由 $message 指定。

assertXmlStringNotEqualsXmlFile() 是與之相反的斷言,並接受相同的參數。

例 A.52: assertXmlStringEqualsXmlFile() 的用法

<?php
use PHPUnit\Framework\TestCase;

class XmlStringEqualsXmlFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertXmlStringEqualsXmlFile(
          '/home/sb/expected.xml', '<foo><baz/></foo>');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) XmlStringEqualsXmlFileTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
 <?xml version="1.0"?>
 <foo>
-  <bar/>
+  <baz/>
 </foo>

/home/sb/XmlStringEqualsXmlFileTest.php:7

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.phpunit XmlStringEqualsXmlFileTest

assertXmlStringEqualsXmlString()

assertXmlStringEqualsXmlString(string $expectedXml, string $actualXml[, string $message = ''])

當 $actualXml 對應的 XML 文檔與 $expectedXml 對應的 XML 文檔不相同時報告錯誤,錯誤訊息由 $message 指定。

assertXmlStringNotEqualsXmlString() 是與之相反的斷言,接受相同的參數。

例 A.53: assertXmlStringEqualsXmlString() 的用法

<?php
use PHPUnit\Framework\TestCase;

class XmlStringEqualsXmlStringTest extends TestCase
{
    public function testFailure()
    {
        $this->assertXmlStringEqualsXmlString(
          '<foo><bar/></foo>', '<foo><baz/></foo>');
    }
}
?>

PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) XmlStringEqualsXmlStringTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
 <?xml version="1.0"?>
 <foo>
-  <bar/>
+  <baz/>
 </foo>

/home/sb/XmlStringEqualsXmlStringTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.phpunit XmlStringEqualsXmlStringTest

附錄 B. 標註

所謂標註,是指某些編程語言中容許加在源代碼中的一種特殊格式的語法元數據。PHP 並無專門的語言特性來支持對源代碼進行標註,然而 PHP 社區早已經造成慣例,經過在文檔註釋塊中使用諸如 @annotation arguments 這樣的標籤來爲源代碼加上標註。在 PHP 中,文檔註釋塊是可反射的:能夠對函數、方法、類以及屬性調用相應級別的反射 API getDocComment() 方法來獲取相應的文檔註釋塊。諸如 PHPUnit 這樣的應用程序在運行時用這些信息來配置其行爲。

注意

PHP中的文檔註釋塊必須以 /** 開頭,以 */ 結尾。任何其餘形式的註釋中出現的標註都將被忽略。

本附錄列出了 PHPUnit 所支持的全部標註種類。

@author

@author 標註是 @group 標註(參見 「@group」一節)的別名,容許基於做者對測試進行過濾。

@after

@after 標註用於指明此方法應當在測試用例類中的每一個測試方法運行完成以後調用。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @after
     */
    public function tearDownSomeFixtures()
    {
        // ...
    }

    /**
     * @after
     */
    public function tearDownSomeOtherFixtures()
    {
        // ...
    }
}

 

@afterClass

@afterClass 標註用於指明此靜態方法應該於測試類中的全部測試方法都運行完成以後調用,用於清理共享基境。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @afterClass
     */
    public static function tearDownSomeSharedFixtures()
    {
        // ...
    }

    /**
     * @afterClass
     */
    public static function tearDownSomeOtherSharedFixtures()
    {
        // ...
    }
}

 

@backupGlobals

全局變量的備份與還原操做能夠對某個測試用例類中的全部測試完全禁用,像這樣:

use PHPUnit\Framework\TestCase;

/**
 * @backupGlobals disabled
 */
class MyTest extends TestCase
{
    // ...
}

 

@backupGlobals 標註也能夠用在測試方法這一級別。這樣能夠對備份與還原操做進行更細粒度的配置:

use PHPUnit\Framework\TestCase;

/**
 * @backupGlobals disabled
 */
class MyTest extends TestCase
{
    /**
     * @backupGlobals enabled
     */
    public function testThatInteractsWithGlobalVariables()
    {
        // ...
    }
}

 

@backupStaticAttributes

若是指定了 @backupStaticAttributes 標註,那麼將在每一個測試以前備份全部已聲明的類的靜態屬性的值,並在測試完成以後所有恢復。它能夠用在測試用例類或測試方法級別:

use PHPUnit\Framework\TestCase;

/**
 * @backupStaticAttributes enabled
 */
class MyTest extends TestCase
{
    /**
     * @backupStaticAttributes disabled
     */
    public function testThatInteractsWithStaticAttributes()
    {
        // ...
    }
}

 

注意

受限於 PHP 的內部實現,在某些狀況下即便使用了 @backupStaticAttributes 也可能有個別靜態值出現意料外的延續,並污染後繼測試。

詳細信息參見 「全局狀態」一節

@before

@before 標註用於指明此方法應當在測試用例類中的每一個測試方法開始運行以前調用。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @before
     */
    public function setupSomeFixtures()
    {
        // ...
    }

    /**
     * @before
     */
    public function setupSomeOtherFixtures()
    {
        // ...
    }
}

 

@beforeClass

@beforeClass 標註用於指明此靜態方法應該於測試類中的全部測試方法都運行完成以後調用,用於創建共享基境。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @beforeClass
     */
    public static function setUpSomeSharedFixtures()
    {
        // ...
    }

    /**
     * @beforeClass
     */
    public static function setUpSomeOtherSharedFixtures()
    {
        // ...
    }
}

 

@codeCoverageIgnore*

@codeCoverageIgnore@codeCoverageIgnoreStart and @codeCoverageIgnoreEnd 標註用於從覆蓋率分析中排除掉某些代碼行。

用法參見「略過代碼塊」一節

@covers

在測試代碼中用 @covers 標註來指明測試方法想要對哪些方法進行測試:

/**
 * @covers BankAccount::getBalance
 */
public function testBalanceIsInitiallyZero()
{
    $this->assertEquals(0, $this->ba->getBalance());
}

 

若是提供了此標註,則代碼覆蓋率信息中只考慮指定的這些方法。

表 B.1列出了 @covers 標註的語法。

表 B.1. 用於指明測試覆蓋哪些方法的標註

Annotation (標註) 描述
@covers ClassName::methodName 指明所標註的測試方法覆蓋指定的方法。
@covers ClassName 指明所標註的測試方法覆蓋給定類的所有方法。
@covers ClassName<extended> 指明所標註的測試方法覆蓋給定類以及其全部父類與接口的所有方法。
@covers ClassName::<public> 指明所標註的測試方法覆蓋給定類的全部 public 方法。
@covers ClassName::<protected> 指明所標註的測試方法覆蓋給定類的全部 protected 方法。
@covers ClassName::<private> 指明所標註的測試方法覆蓋給定類的全部 private 方法。
@covers ClassName::<!public> 指明所標註的測試方法覆蓋給定類的全部非 public 方法。
@covers ClassName::<!protected> 指明所標註的測試方法覆蓋給定類的全部非 protected 方法。
@covers ClassName::<!private> 指明所標註的測試方法覆蓋給定類的全部非 private 方法。
@covers ::functionName 指明所標註的測試方法覆蓋給定的全局函數。

@coversDefaultClass

@coversDefaultClass 標註用於指定一個默認的命名空間或類名,這樣就不用在每一個 @covers 標註中重複長名稱。參見例 B.1

例 B.1: 用 @coversDefaultClass 縮短標註

<?php
use PHPUnit\Framework\TestCase;

/**
 * @coversDefaultClass \Foo\CoveredClass
 */
class CoversDefaultClassTest extends TestCase
{
    /**
     * @covers ::publicMethod
     */
    public function testSomething()
    {
        $o = new Foo\CoveredClass;
        $o->publicMethod();
    }
}
?>

@coversNothing

在測試代碼中用 @coversNothing 標註來指明所標註的測試用例不須要記錄任何代碼覆蓋率信息。

這能夠用於集成測試。例子可參見例 11.3

這個標註能夠用在類級別或者方法級別,而且會覆蓋掉任何 @covers 標註。

@dataProvider

測試方法能夠接受任意參數。這些參數能夠由數據供給器方法(例 2.5中的 provider())提供。所要使用的數據供給器方法用 @dataProvider 標註來指定。

更多細節參見「數據供給器」一節

@depends

PHPUnit支持對測試方法之間的顯式依賴關係進行聲明。這種依賴關係並非定義在測試方法的執行順序中,而是容許生產者(producer)返回一個測試基境(fixture)的實例,並將此實例傳遞給依賴於它的消費者(consumer)們。例 2.2展現瞭如何用 @depends 標註來表達測試方法之間的依賴關係。

更多細節參見「測試的依賴關係」一節

@expectedException

例 2.10展現瞭如何用 @expectedException 標註來測試被測代碼中是否拋出了異常。

更多細節參見「對異常進行測試」一節

@expectedExceptionCode

將 @expectedExceptionCode 標註與 @expectedException 聯合使用,能夠對拋出異常的代碼做出斷言,這樣能夠縮小具體異常的範圍。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @expectedException     MyException
     * @expectedExceptionCode 20
     */
    public function testExceptionHasErrorcode20()
    {
        throw new MyException('Some Message', 20);
    }
}

爲了方便測試並減小冗餘,能夠用"@expectedExceptionCode ClassName::CONST"這樣的語法將指定類常量做爲 @expectedExceptionCode

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
      * @expectedException     MyException
      * @expectedExceptionCode MyClass::ERRORCODE
      */
    public function testExceptionHasErrorcode20()
    {
      throw new MyException('Some Message', 20);
    }
}
class MyClass
{
    const ERRORCODE = 20;
}

 

@expectedExceptionMessage

@expectedExceptionMessage 標註的運做方式相似於 @expectedExceptionCode ,用它能夠對異常的錯誤訊息做出斷言。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @expectedException        MyException
     * @expectedExceptionMessage Some Message
     */
    public function testExceptionHasRightMessage()
    {
        throw new MyException('Some Message', 20);
    }
}

預期訊息能夠是異常訊息的子串。在只須要斷言傳入的特定名稱或參數確實出現於異常中時這個特性頗有用,這樣就無需在測試中關注完整的異常訊息。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
     /**
      * @expectedException        MyException
      * @expectedExceptionMessage broken
      */
     public function testExceptionHasRightMessage()
     {
         $param = "broken";
         throw new MyException('Invalid parameter "'.$param.'".', 20);
     }
}

爲了方便測試同時減小冗餘,能夠用"@expectedExceptionMessage ClassName::CONST"這樣的語法將指定類常量做爲 @expectedExceptionMessage。在「@expectedExceptionCode」一節中能夠看到範例。

@expectedExceptionMessageRegExp

預期訊息也能夠經過 @expectedExceptionMessageRegExp 標註以正則表達式來指定。當沒法用子串來完成對給定訊息的匹配時,這種方式就很是有用了。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
     /**
      * @expectedException              MyException
      * @expectedExceptionMessageRegExp /Argument \d+ can not be an? \w+/
      */
     public function testExceptionHasRightMessage()
     {
         throw new MyException('Argument 2 can not be an integer');
     }
}

 

@group

測試能夠用 @group 標註來標記爲屬於一個或多個組,就像這樣:

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @group specification
     */
    public function testSomething()
    {
    }

    /**
     * @group regresssion
     * @group bug2204
     */
    public function testSomethingElse()
    {
    }
}

 

測試能夠基於組來選擇性的執行,使用命令行測試執行器的 --group and --exclude-group 選項,或者使用對應的 XML 配置文件指令。

@large

@large 標註是 @group large 的別名。

若是安裝了 PHP_Invoker 組件包並啓用了嚴格模式,一個執行時間超過60秒的大型(large)測試將視爲失敗。這個超時限制能夠經過 XML 配置文件的 timeoutForLargeTests 屬性進行配置。

@medium

@medium 標註是 @group medium 的別名。中型(medium)測試不能依賴於標記爲 @large 的測試。

若是安裝了 PHP_Invoker 組件包並啓用了嚴格模式,一個執行時間超過10秒的中型(medium)測試將視爲失敗。這個超時限制能夠經過 XML 配置文件的 timeoutForMediumTests 屬性進行配置。

@preserveGlobalState

在單獨的進程中運行測試時,PHPUnit 會嘗試保持來自父進程的全局狀態(經過在父進程序列化全局狀態而後在子進程反序列化的方式)。這當父進程包含非可序列化的全局內容時可能會致使問題。爲了修正這種問題,能夠用 @preserveGlobalState 標註來禁止 PHPUnit 保持全局狀態。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @runInSeparateProcess
     * @preserveGlobalState disabled
     */
    public function testInSeparateProcess()
    {
        // ...
    }
}

 

@requires

@requires 標註用於在常規前提條件(例如 PHP 版本或所安裝的擴展)不知足時跳過測試。

完整的可能用法以及例子見表 7.3

@runTestsInSeparateProcesses

指明單個測試類內的全部測試要各自運行在獨立的 PHP 進程中。

use PHPUnit\Framework\TestCase;

/**
 * @runTestsInSeparateProcesses
 */
class MyTest extends TestCase
{
    // ...
}

注意:「@preserveGlobalState」一節 默認狀況下,PHPUnit 會嘗試經過在父進程序列化全局狀態而後在子進程反序列化的方式在子進程中保持來自父進程的全局狀態。這當父進程包含非可序列化的全局內容時可能會致使問題。關於如何修正此問題的信息參見「@preserveGlobalState」一節

@runInSeparateProcess

明某個測試要運行在獨立的 PHP 進程中。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @runInSeparateProcess
     */
    public function testInSeparateProcess()
    {
        // ...
    }
}

注意:「@preserveGlobalState」一節 默認狀況下,PHPUnit 會嘗試經過在父進程序列化全局狀態而後在子進程反序列化的方式在子進程中保持來自父進程的全局狀態。這當父進程包含非可序列化的全局內容時可能會致使問題。關於如何修正此問題的信息參見「@preserveGlobalState」一節

@small

@small 標註是 @group small 的別名。小型(small)測試不能依賴於標記爲 @medium 或 @large 的測試。

若是安裝了 PHP_Invoker 組件包並啓用了嚴格模式,一個執行時間超過1秒的小型(small)測試將會視爲失敗。這個超時限制能夠經過 XML 配置文件的 timeoutForSmallTests 屬性進行配置。

注意

須要啓用運行時間限制的測試必須顯式地標註爲 @small@medium 或 @large

@test

除了用 test 做爲測試方法名稱的前綴外,還能夠在方法的文檔註釋塊中用 @test 標註來將其標記爲測試方法。

/**
 * @test
 */
public function initialBalanceShouldBe0()
{
    $this->assertEquals(0, $this->ba->getBalance());
}

 

@testdox

 

 
 

@ticket

 

 
 

@uses

@uses 標註用來指明那些將會在測試中執行到但同時又不打算讓其被測試所覆蓋的代碼。在對代碼單元進行測試時所必須的值對象就是個很好的例子。

/**
 * @covers BankAccount::deposit
 * @uses   Money
 */
public function testMoneyCanBeDepositedInAccount()
{
    // ...
}

 

在嚴格覆蓋模式中,意外覆蓋的代碼將致使測試斷定爲失敗,這個標註就顯得特別有用。關於嚴格覆蓋模式的更多信息,參見「意外的代碼覆蓋」一節

附錄 C. XML 配置文件

PHPUnit

<phpunit> 元素的屬性用於配置 PHPUnit 的核心功能。

<phpunit
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd"
         backupGlobals="true"
         backupStaticAttributes="false"
         <!--bootstrap="/path/to/bootstrap.php"-->
         cacheTokens="false"
         colors="false"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         forceCoversAnnotation="false"
         mapTestClassNameToCoveredClassName="false"
         printerClass="PHPUnit_TextUI_ResultPrinter"
         <!--printerFile="/path/to/ResultPrinter.php"-->
         processIsolation="false"
         stopOnError="false"
         stopOnFailure="false"
         stopOnIncomplete="false"
         stopOnSkipped="false"
         stopOnRisky="false"
         testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader"
         <!--testSuiteLoaderFile="/path/to/StandardTestSuiteLoader.php"-->
         timeoutForSmallTests="1"
         timeoutForMediumTests="10"
         timeoutForLargeTests="60"
         verbose="false">
  <!-- ... -->
</phpunit>

以上 XML 配置對應於在「命令行選項」一節描述過的 TextUI 測試執行器的默認行爲。

其餘那些不能用命令行選項來配置的選項有:

convertErrorsToExceptions

默認狀況下,PHPUnit 將會安插一個錯誤處理函數來將如下錯誤轉換爲異常:

  • E_WARNING
  • E_NOTICE
  • E_USER_ERROR
  • E_USER_WARNING
  • E_USER_NOTICE
  • E_STRICT
  • E_RECOVERABLE_ERROR
  • E_DEPRECATED
  • E_USER_DEPRECATED

將 convertErrorsToExceptions 設爲 false 能夠禁用此功能。

convertNoticesToExceptions

此選項設置爲 false 時,由 convertErrorsToExceptions 安插的錯誤處理函數不會將 E_NOTICEE_USER_NOTICEE_STRICT 錯誤轉換爲異常。

convertWarningsToExceptions

此選項設置爲 false 時,由 convertErrorsToExceptions 安插的錯誤處理函數不會將 E_WARNING 或 E_USER_WARNING 錯誤轉換爲異常。

forceCoversAnnotation

只記錄使用了 @covers 標註(文檔參見「@covers」一節)的測試的代碼覆蓋率。

timeoutForLargeTests

若是實行了基於測試規模的時間限制,那麼此屬性爲全部標記爲 @large 的測試設定超時限制。在配置的時間限制內未執行完畢的測試將視爲失敗。

timeoutForMediumTests

若是實行了基於測試規模的時間限制,那麼此屬性爲全部標記爲 @medium 的測試設定超時限制。在配置的時間限制內未執行完畢的測試將視爲失敗。

timeoutForSmallTests

若是實行了基於測試規模的時間限制,那麼此屬性爲全部未標記爲 @medium 或 @large 的測試設定超時限制。在配置的時間限制內未執行完畢的測試將視爲失敗。

測試套件

帶有一個或多個 <testsuite> 子元素的 <testsuites> 元素用於將測試套件及測試用例組合出新的測試套件。

<testsuites>
  <testsuite name="My Test Suite">
    <directory>/path/to/*Test.php files</directory>
    <file>/path/to/MyTest.php</file>
    <exclude>/path/to/exclude</exclude>
  </testsuite>
</testsuites>

能夠用 phpVersion 和 phpVersionOperator 屬性來指定 PHP 版本需求。在如下例子中,僅當 PHP 版本至少爲 5.3.0 時纔會將 /path/to/*Test.php 文件與 /path/to/MyTest.php 文件添加到測試套件中。

  <testsuites>
    <testsuite name="My Test Suite">
      <directory suffix="Test.php" phpVersion="5.3.0" phpVersionOperator=">=">/path/to/files</directory>
      <file phpVersion="5.3.0" phpVersionOperator=">=">/path/to/MyTest.php</file>
    </testsuite>
  </testsuites>

phpVersionOperator 屬性是可選的,其默認值爲 >=

分組

<groups> 元素及其 <include><exclude><group> 子元素用於從帶有 @group 標註(相關文檔參見 「@group」一節)的測試中選擇須要運行(或不運行)的分組。

<groups>
  <include>
    <group>name</group>
  </include>
  <exclude>
    <group>name</group>
  </exclude>
</groups>

以上 XML 配置對應於以以下選項調用 TextUI 測試執行器:

  • --group name

  • --exclude-group name

Whitelisting Files for Code Coverage

<filter> 元素及其子元素用於配置代碼覆蓋率報告所使用的白名單。

<filter>
  <whitelist processUncoveredFilesFromWhitelist="true">
    <directory suffix=".php">/path/to/files</directory>
    <file>/path/to/file</file>
    <exclude>
      <directory suffix=".php">/path/to/files</directory>
      <file>/path/to/file</file>
    </exclude>
  </whitelist>
</filter>

Logging (日誌記錄)

<logging> 元素及其 <log> 子元素用於配置測試執行期間的日誌記錄。

<logging>
  <log type="coverage-html" target="/tmp/report" lowUpperBound="35"
       highLowerBound="70"/>
  <log type="coverage-clover" target="/tmp/coverage.xml"/>
  <log type="coverage-php" target="/tmp/coverage.serialized"/>
  <log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
  <log type="json" target="/tmp/logfile.json"/>
  <log type="tap" target="/tmp/logfile.tap"/>
  <log type="junit" target="/tmp/logfile.xml" logIncompleteSkipped="false"/>
  <log type="testdox-html" target="/tmp/testdox.html"/>
  <log type="testdox-text" target="/tmp/testdox.txt"/>
</logging>

以上 XML 配置對應於以以下選項調用 TextUI 測試執行器:

  • --coverage-html /tmp/report

  • --coverage-clover /tmp/coverage.xml

  • --coverage-php /tmp/coverage.serialized

  • --coverage-text

  • --log-json /tmp/logfile.json

  • > /tmp/logfile.txt

  • --log-tap /tmp/logfile.tap

  • --log-junit /tmp/logfile.xml

  • --testdox-html /tmp/testdox.html

  • --testdox-text /tmp/testdox.txt

lowUpperBoundhighLowerBoundlogIncompleteSkipped 及 showUncoveredFiles 屬性沒有等價的 TextUI 測試執行器選項。

  • lowUpperBound:視爲「低」覆蓋率的最大覆蓋率百分比。

  • highLowerBound:視爲「高」覆蓋率的最小覆蓋率百分比。

  • showUncoveredFiles:在 --coverage-text 輸出中顯示全部符合白名單的文件,不只限於有覆蓋率信息的那些。

  • showOnlySummary:在 --coverage-text 輸出中只顯示摘要。

測試監聽器

<listeners> 元素及其 <listener> 子元素用於在測試執行期間附加額外的測試監聽器。

<listeners>
  <listener class="MyListener" file="/optional/path/to/MyListener.php">
    <arguments>
      <array>
        <element key="0">
          <string>Sebastian</string>
        </element>
      </array>
      <integer>22</integer>
      <string>April</string>
      <double>19.78</double>
      <null/>
      <object class="stdClass"/>
    </arguments>
  </listener>
</listeners>

以上 XML 配置對應於將 $listener 對象(見下文)附到測試執行過程上。

$listener = new MyListener(
    ['Sebastian'],
    22,
    'April',
    19.78,
    null,
    new stdClass
);

設定 PHP INI 設置、常量、全局變量

<php> 元素及其子元素用於配置 PHP 設置、常量以及全局變量。同時也可用於向 include_path 前部置入內容。

<php>
  <includePath>.</includePath>
  <ini name="foo" value="bar"/>
  <const name="foo" value="bar"/>
  <var name="foo" value="bar"/>
  <env name="foo" value="bar"/>
  <post name="foo" value="bar"/>
  <get name="foo" value="bar"/>
  <cookie name="foo" value="bar"/>
  <server name="foo" value="bar"/>
  <files name="foo" value="bar"/>
  <request name="foo" value="bar"/>
</php>

以上 XML 配置對應於以下 PHP 代碼:

ini_set('foo', 'bar');
define('foo', 'bar');
$GLOBALS['foo'] = 'bar';
$_ENV['foo'] = 'bar';
$_POST['foo'] = 'bar';
$_GET['foo'] = 'bar';
$_COOKIE['foo'] = 'bar';
$_SERVER['foo'] = 'bar';
$_FILES['foo'] = 'bar';
$_REQUEST['foo'] = 'bar';

附錄 D. 索引

 

符號

$backupGlobalsBlacklist, 全局狀態
$backupStaticAttributesBlacklist, 全局狀態
@author, 命令行選項@author
@backupGlobals, 全局狀態@backupGlobals
@backupStaticAttributes, 全局狀態@backupStaticAttributes
@codeCoverageIgnore, 略過代碼塊@codeCoverageIgnore*
@codeCoverageIgnoreEnd, 略過代碼塊@codeCoverageIgnore*
@codeCoverageIgnoreStart, 略過代碼塊@codeCoverageIgnore*
@covers, 指明要覆蓋的方法@covers
@coversDefaultClass, @coversDefaultClass
@coversNothing, 指明要覆蓋的方法@coversNothing
@dataProvider, 數據供給器@dataProvider
@depends, 測試的依賴關係數據供給器@depends
@expectedException, 對異常進行測試@expectedException
@expectedExceptionCode, @expectedExceptionCode
@expectedExceptionMessage, @expectedExceptionMessage
@expectedExceptionMessageRegExp, @expectedExceptionMessageRegExp
@group, 命令行選項@group
@large, @large
@medium, @medium
@preserveGlobalState, @preserveGlobalState
@requires, @requires
@runInSeparateProcess, @runInSeparateProcess
@runTestsInSeparateProcesses, @runTestsInSeparateProcesses
@small, @small
@test, 編寫 PHPUnit 測試@test
@testdox, @testdox
@ticket, @ticket
@uses, @uses
變動風險反模式(CRAP)指數(Change Risk Anti-Patterns (CRAP) Index), 用於代碼覆蓋率的軟件衡量標準
異常, 對異常進行測試
敏捷文檔, 命令行選項敏捷文檔
測試的依賴關係, 測試的依賴關係

A

Annotation (標註), 編寫 PHPUnit 測試測試的依賴關係數據供給器對異常進行測試命令行選項略過代碼塊指明要覆蓋的方法標註
anything(), assertThat()
arrayHasKey(), assertThat()
assertArrayHasKey(), assertArrayHasKey()
assertArrayNotHasKey(), assertArrayHasKey()
assertArraySubset(), assertArraySubset()
assertAttributeContains(), assertContains()
assertAttributeContainsOnly(), assertContainsOnly()
assertAttributeEmpty(), assertEmpty()
assertAttributeEquals(), assertEquals()
assertAttributeGreaterThan(), assertGreaterThan()
assertAttributeGreaterThanOrEqual(), assertGreaterThanOrEqual()
assertAttributeInstanceOf(), assertInstanceOf()
assertAttributeInternalType(), assertInternalType()
assertAttributeLessThan(), assertLessThan()
assertAttributeLessThanOrEqual(), assertLessThanOrEqual()
assertAttributeNotContains(), assertContains()
assertAttributeNotContainsOnly(), assertContainsOnly()
assertAttributeNotEmpty(), assertEmpty()
assertAttributeNotEquals(), assertEquals()
assertAttributeNotInstanceOf(), assertInstanceOf()
assertAttributeNotInternalType(), assertInternalType()
assertAttributeNotSame(), assertSame()
assertAttributeSame(), assertSame()
assertClassHasAttribute(), assertClassHasAttribute()
assertClassHasStaticAttribute(), assertClassHasStaticAttribute()
assertClassNotHasAttribute(), assertClassHasAttribute()
assertClassNotHasStaticAttribute(), assertClassHasStaticAttribute()
assertContains(), assertContains()
assertContainsOnly(), assertContainsOnly()
assertContainsOnlyInstancesOf(), assertContainsOnlyInstancesOf()
assertCount(), assertCount()
assertDirectoryExists(), assertDirectoryExists()
assertDirectoryIsReadable(), assertDirectoryIsReadable()
assertDirectoryIsWritable(), assertDirectoryIsWritable()
assertDirectoryNotExists(), assertDirectoryExists()
assertDirectoryNotIsReadable(), assertDirectoryIsReadable()
assertDirectoryNotIsWritable(), assertDirectoryIsWritable()
assertEmpty(), assertEmpty()
assertEquals(), assertEquals()
assertEqualXMLStructure(), assertEqualXMLStructure()
assertFalse(), assertFalse()
assertFileEquals(), assertFileEquals()
assertFileExists(), assertFileExists()
assertFileIsReadable(), assertFileIsReadable()
assertFileIsWritable(), assertFileIsWritable()
assertFileNotEquals(), assertFileEquals()
assertFileNotExists(), assertFileExists()
assertFileNotIsReadable(), assertFileIsReadable()
assertFileNotIsWritable(), assertFileIsWritable()
assertFinite(), assertInfinite()
assertGreaterThan(), assertGreaterThan()
assertGreaterThanOrEqual(), assertGreaterThanOrEqual()
assertInfinite(), assertInfinite()
assertInstanceOf(), assertInstanceOf()
assertInternalType(), assertInternalType()
assertIsReadable(), assertIsReadable()
assertIsWritable(), assertIsWritable()
assertJsonFileEqualsJsonFile(), assertJsonFileEqualsJsonFile()
assertJsonFileNotEqualsJsonFile(), assertJsonFileEqualsJsonFile()
assertJsonStringEqualsJsonFile(), assertJsonStringEqualsJsonFile()
assertJsonStringEqualsJsonString(), assertJsonStringEqualsJsonString()
assertJsonStringNotEqualsJsonFile(), assertJsonStringEqualsJsonFile()
assertJsonStringNotEqualsJsonString(), assertJsonStringEqualsJsonString()
assertLessThan(), assertLessThan()
assertLessThanOrEqual(), assertLessThanOrEqual()
assertNan(), assertNan()
assertNotContains(), assertContains()
assertNotContainsOnly(), assertContainsOnly()
assertNotCount(), assertCount()
assertNotEmpty(), assertEmpty()
assertNotEquals(), assertEquals()
assertNotInstanceOf(), assertInstanceOf()
assertNotInternalType(), assertInternalType()
assertNotIsReadable(), assertIsReadable()
assertNotIsWritable(), assertIsWritable()
assertNotNull(), assertNull()
assertNotRegExp(), assertRegExp()
assertNotSame(), assertSame()
assertNull(), assertNull()
assertObjectHasAttribute(), assertObjectHasAttribute()
assertObjectNotHasAttribute(), assertObjectHasAttribute()
assertPostConditions(), 基境(fixture)
assertPreConditions(), 基境(fixture)
assertRegExp(), assertRegExp()
assertSame(), assertSame()
assertStringEndsNotWith(), assertStringEndsWith()
assertStringEndsWith(), assertStringEndsWith()
assertStringEqualsFile(), assertStringEqualsFile()
assertStringMatchesFormat(), assertStringMatchesFormat()
assertStringMatchesFormatFile(), assertStringMatchesFormatFile()
assertStringNotEqualsFile(), assertStringEqualsFile()
assertStringNotMatchesFormat(), assertStringMatchesFormat()
assertStringNotMatchesFormatFile(), assertStringMatchesFormatFile()
assertStringStartsNotWith(), assertStringStartsWith()
assertStringStartsWith(), assertStringStartsWith()
assertThat(), assertThat()
assertTrue(), assertTrue()
assertXmlFileEqualsXmlFile(), assertXmlFileEqualsXmlFile()
assertXmlFileNotEqualsXmlFile(), assertXmlFileEqualsXmlFile()
assertXmlStringEqualsXmlFile(), assertXmlStringEqualsXmlFile()
assertXmlStringEqualsXmlString(), assertXmlStringEqualsXmlString()
assertXmlStringNotEqualsXmlFile(), assertXmlStringEqualsXmlFile()
assertXmlStringNotEqualsXmlString(), assertXmlStringEqualsXmlString()
attribute(), assertThat()
attributeEqualTo(), assertThat()
Automated Documentation (自動文檔), 敏捷文檔

C

classHasAttribute(), assertThat()
classHasStaticAttribute(), assertThat()
Code Coverage (代碼覆蓋率), 命令行選項代碼覆蓋率分析@coversWhitelisting Files for Code Coverage
Branch Coverage (分支覆蓋率), 用於代碼覆蓋率的軟件衡量標準
Class and Trait Coverage (類與特質覆蓋率), 用於代碼覆蓋率的軟件衡量標準
Function and Method Coverage (函數與方法覆蓋率), 用於代碼覆蓋率的軟件衡量標準
Line Coverage (行覆蓋率), 用於代碼覆蓋率的軟件衡量標準
Opcode Coverage (Opcode 覆蓋率), 用於代碼覆蓋率的軟件衡量標準
Path Coverage (路徑覆蓋率), 用於代碼覆蓋率的軟件衡量標準
Whitelist (白名單), 將文件列入白名單
Configuration (配置), 命令行選項
Constant (常量), 設定 PHP INI 設置、常量、全局變量
contains(), assertThat()
containsOnly(), assertThat()
containsOnlyInstancesOf(), assertThat()
createMock(), Stubs (樁件)

D

Data-Driven Tests (數據驅動測試), 實現 PHPUnit_Framework_Test
Defect Localization (缺陷定位), 測試的依賴關係
Depended-On Component (依賴組件), 測試替身
directoryExists(), assertThat()
Documenting Assumptions (將假設文檔化), 敏捷文檔

E

equalTo(), assertThat()
Error Handler (錯誤處理), 對 PHP 錯誤進行測試
Error (錯誤), 命令行測試執行器
expectException(), 對異常進行測試
expectExceptionCode(), 對異常進行測試
expectExceptionMessage(), 對異常進行測試
expectExceptionMessageRegExp(), 對異常進行測試
Extreme Programming (極限編程), 敏捷文檔

F

Failure (失敗), 命令行測試執行器
fileExists(), assertThat()
Fixture (基境), 基境(fixture)
Fluent Interface (流暢式接口), Stubs (樁件)

G

getMockBuilder(), 仿件對象(Mock Object)
getMockForAbstractClass(), 對特質(Trait)與抽象類進行模仿
getMockForTrait(), 對特質(Trait)與抽象類進行模仿
getMockFromWsdl(), 對 Web 服務(Web Services)進行上樁或模仿
Global Variable (全局變量), 全局狀態設定 PHP INI 設置、常量、全局變量
greaterThan(), assertThat()
greaterThanOrEqual(), assertThat()

H

hasAttribute(), assertThat()

I

identicalTo(), assertThat()
include_path, 命令行選項
Incomplete Test (未完成的測試), 未完成的測試
isFalse(), assertThat()
isInstanceOf(), assertThat()
isNull(), assertThat()
isReadable(), assertThat()
isTrue(), assertThat()
isType(), assertThat()
isWritable(), assertThat()

J

JSON, 命令行選項

L

lessThan(), assertThat()
lessThanOrEqual(), assertThat()
Logfile (日誌文件), 命令行選項
Logging (日誌記錄), Logging (日誌記錄)Logging (日誌記錄)
logicalAnd(), assertThat()
logicalNot(), assertThat()
logicalOr(), assertThat()
logicalXor(), assertThat()

M

matchesRegularExpression(), assertThat()
method(), Stubs (樁件)
Mock Object (仿件對象), 仿件對象(Mock Object)

O

onConsecutiveCalls(), Stubs (樁件)
onNotSuccessfulTest(), 基境(fixture)

P

PHP Error (PHP 錯誤), 對 PHP 錯誤進行測試
PHP Notice (PHP 通知), 對 PHP 錯誤進行測試
PHP Warning (PHP 警告), 對 PHP 錯誤進行測試
php.ini, 設定 PHP INI 設置、常量、全局變量
PHPUnit\Framework\TestCase, 編寫 PHPUnit 測試PHPUnit\Framework\TestCase 的子類
PHPUnit_Extensions_RepeatedTest, 從 PHPUnit_Extensions_TestDecorator 派生子類
PHPUnit_Extensions_TestDecorator, 從 PHPUnit_Extensions_TestDecorator 派生子類
PHPUnit_Framework_BaseTestListener, 實現 PHPUnit_Framework_TestListener
PHPUnit_Framework_Error, 對 PHP 錯誤進行測試
PHPUnit_Framework_Error_Notice, 對 PHP 錯誤進行測試
PHPUnit_Framework_Error_Warning, 對 PHP 錯誤進行測試
PHPUnit_Framework_IncompleteTest, 未完成的測試
PHPUnit_Framework_IncompleteTestError, 未完成的測試
PHPUnit_Framework_Test, 實現 PHPUnit_Framework_Test
PHPUnit_Framework_TestListener, 命令行選項實現 PHPUnit_Framework_TestListener測試監聽器
PHPUnit_Runner_TestSuiteLoader, 命令行選項
PHPUnit_Util_Printer, 命令行選項
PHP_Invoker, @large@medium@small
Process Isolation (進程隔離), 命令行選項

R

Refactoring (重構), 在開發過程當中
Report (報告), 命令行選項
returnArgument(), Stubs (樁件)
returnCallback(), Stubs (樁件)
returnSelf(), Stubs (樁件)
returnValueMap(), Stubs (樁件)

S

setUp(), 基境(fixture)
setUpBeforeClass, 基境共享
setUpBeforeClass(), 基境(fixture)
stringContains(), assertThat()
stringEndsWith(), assertThat()
stringStartsWith(), assertThat()
Stub (樁件), Stubs (樁件)
Stubs (樁件), 跨團隊測試
System Under Test (被測系統), 測試替身

T

tearDown(), 基境(fixture)
tearDownAfterClass, 基境共享
tearDownAfterClass(), 基境(fixture)
Template Method (模板方法), 基境(fixture)
Test Double (測試替身), 測試替身
Test Groups (測試分組), 命令行選項分組
Test Isolation (測試隔離), 命令行選項全局狀態
Test Listener (測試監聽器), 測試監聽器
Test Suite (測試套件), 組織測試測試套件
TestDox, 敏捷文檔@testdox
throwException(), Stubs (樁件)
timeoutForLargeTests, @large
timeoutForMediumTests, @medium
timeoutForSmallTests, @small

W

Whitelist (白名單), Whitelisting Files for Code Coverage
will(), Stubs (樁件)
willReturn(), Stubs (樁件)

X

Xdebug, 代碼覆蓋率分析
XML Configuration (XML 配置), 用 XML 配置來編排測試套件

附錄 E. 參考書目

[Astels2003Test Driven DevelopmentDavid Astels. 版權 © 2003. Prentice Hall. ISBN 0131016490.

[Beck2002Test Driven Development by ExampleKent Beck. 版權 © 2002. Addison-Wesley. ISBN 0-321-14653-0.

[Meszaros2007xUnit Test Patterns: Refactoring Test CodeGerard Meszaros. 版權 © 2007. Addison-Wesley. ISBN 978-0131495050.

附錄 F. 版權

Copyright (c) 2005-2017 Sebastian Bergmann.

此做品依照 Creative Commons Attribution 3.0
Unported License 受權。

如下是此受權許可協議的摘要信息,完整的法律文件附在其後。

--------------------------------------------------------------------

您能夠自由地:

    * 分享 - 複製、分發、傳播此做品
    * 重組 - 創做演繹此做品

唯須遵照下列條件:

姓名標示。弄必須按照做者或者版權人指定的方式表彰其姓名(但不得以任何方式暗示他們承認你或你使用本做品的方式)。

    * 在再使用或者發行本做品時,您必須向他人明示本做品使用的許可協議條款。明示的最佳方法是附上本網頁的連接。

    * 若您得到著做權人准許,則上述全部條件均可予以避免除。

    * 此協議對做者的人身權不構成任何損害與限制。

合理使用及其餘權利不受許可協議影響。

以上是易於常人瞭解的法律條文(完整的受權許可協議)摘要。

====================================================================

Creative Commons Legal Code
Attribution 3.0 Unported

CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO
WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS
LIABILITY FOR DAMAGES RESULTING FROM ITS USE.

License

THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS
PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE
WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW
IS PROHIBITED.

BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS
LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF
SUCH TERMS AND CONDITIONS.

1. Definitions

   a. "Adaptation" means a work based upon the Work, or upon the
      Work and other pre-existing works, such as a translation,
      adaptation, derivative work, arrangement of music or other
      alterations of a literary or artistic work, or phonogram or
      performance and includes cinematographic adaptations or any
      other form in which the Work may be recast, transformed, or
      adapted including in any form recognizably derived from the
      original, except that a work that constitutes a Collection
      will not be considered an Adaptation for the purpose of this
      License. For the avoidance of doubt, where the Work is a
      musical work, performance or phonogram, the synchronization of
      the Work in timed-relation with a moving image ("synching")
      will be considered an Adaptation for the purpose of this
      License.

   b. "Collection" means a collection of literary or artistic works,
      such as encyclopedias and anthologies, or performances,
      phonograms or broadcasts, or other works or subject matter
      other than works listed in Section 1(f) below, which, by
      reason of the selection and arrangement of their contents,
      constitute intellectual creations, in which the Work is
      included in its entirety in unmodified form along with one or
      more other contributions, each constituting separate and
      independent works in themselves, which together are assembled
      into a collective whole. A work that constitutes a Collection
      will not be considered an Adaptation (as defined above) for
      the purposes of this License.

   c. "Distribute" means to make available to the public the
      original and copies of the Work or Adaptation, as appropriate,
      through sale or other transfer of ownership.

   d. "Licensor" means the individual, individuals, entity or
      entities that offer(s) the Work under the terms of this License.

   e. "Original Author" means, in the case of a literary or artistic
      work, the individual, individuals, entity or entities who
      created the Work or if no individual or entity can be
      identified, the publisher; and in addition (i) in the case of
      a performance the actors, singers, musicians, dancers, and
      other persons who act, sing, deliver, declaim, play in,
      interpret or otherwise perform literary or artistic works or
      expressions of folklore; (ii) in the case of a phonogram the
      producer being the person or legal entity who first fixes the
      sounds of a performance or other sounds; and, (iii) in the
      case of broadcasts, the organization that transmits the
      broadcast.

   f. "Work" means the literary and/or artistic work offered under
      the terms of this License including without limitation any
      production in the literary, scientific and artistic domain,
      whatever may be the mode or form of its expression including
      digital form, such as a book, pamphlet and other writing; a
      lecture, address, sermon or other work of the same nature; a
      dramatic or dramatico-musical work; a choreographic work or
      entertainment in dumb show; a musical composition with or
      without words; a cinematographic work to which are assimilated
      works expressed by a process analogous to cinematography; a
      work of drawing, painting, architecture, sculpture, engraving
      or lithography; a photographic work to which are assimilated
      works expressed by a process analogous to photography; a work
      of applied art; an illustration, map, plan, sketch or three-
      dimensional work relative to geography, topography,
      architecture or science; a performance; a broadcast; a
      phonogram; a compilation of data to the extent it is protected
      as a copyrightable work; or a work performed by a variety or
      circus performer to the extent it is not otherwise considered
      a literary or artistic work.

   g. "You" means an individual or entity exercising rights under
      this License who has not previously violated the terms of
      this License with respect to the Work, or who has received
      express permission from the Licensor to exercise rights under
      this License despite a previous violation.

   h. "Publicly Perform" means to perform public recitations of the
      Work and to communicate to the public those public
      recitations, by any means or process, including by wire or
      wireless means or public digital performances; to make
      available to the public Works in such a way that members of
      the public may access these Works from a place and at a place
      individually chosen by them; to perform the Work to the public
      by any means or process and the communication to the public of
      the performances of the Work, including by public digital
      performance; to broadcast and rebroadcast the Work by any
      means including signs, sounds or images.

   i. "Reproduce" means to make copies of the Work by any means
      including without limitation by sound or visual recordings and
      the right of fixation and reproducing fixations of the Work,
      including storage of a protected performance or phonogram in
      digital form or other electronic medium.

2. Fair Dealing Rights. Nothing in this License is intended to
   reduce, limit, or restrict any uses free from copyright or rights
   arising from limitations or exceptions that are provided for in
   connection with the copyright protection under copyright law or
   other applicable laws.

3. License Grant. Subject to the terms and conditions of this
   License, Licensor hereby grants You a worldwide, royalty-free,
   non-exclusive, perpetual (for the duration of the applicable
   copyright) license to exercise the rights in the Work as stated
   below:

   a. to Reproduce the Work, to incorporate the Work into one or
      more Collections, and to Reproduce the Work as incorporated
      in the Collections;

   b. to create and Reproduce Adaptations provided that any such
      Adaptation, including any translation in any medium, takes
      reasonable steps to clearly label, demarcate or otherwise
      identify that changes were made to the original Work. For
      example, a translation could be marked "The original work was
      translated from English to Spanish," or a modification could
      indicate "The original work has been modified.";

   c. to Distribute and Publicly Perform the Work including as
      incorporated in Collections; and,

   d. to Distribute and Publicly Perform Adaptations.

   e. For the avoidance of doubt:

      i. Non-waivable Compulsory License Schemes. In those
         jurisdictions in which the right to collect royalties
         through any statutory or compulsory licensing scheme cannot
         be waived, the Licensor reserves the exclusive right to
         collect such royalties for any exercise by You of the
         rights granted under this License;

      ii. Waivable Compulsory License Schemes. In those
          jurisdictions in which the right to collect royalties
          through any statutory or compulsory licensing scheme can
          be waived, the Licensor waives the exclusive right to
          collect such royalties for any exercise by You of the
          rights granted under this License; and,

      iii. Voluntary License Schemes. The Licensor waives the right
           to collect royalties, whether individually or, in the
           event that the Licensor is a member of a collecting
           society that administers voluntary licensing schemes, via
           that society, from any exercise by You of the rights
           granted under this License.

The above rights may be exercised in all media and formats whether
now known or hereafter devised. The above rights include the right
to make such modifications as are technically necessary to exercise
the rights in other media and formats. Subject to Section 8(f), all
rights not expressly granted by Licensor are hereby reserved.

4. Restrictions. The license granted in Section 3 above is expressly
   made subject to and limited by the following restrictions:

   a. You may Distribute or Publicly Perform the Work only under the
      terms of this License. You must include a copy of, or the
      Uniform Resource Identifier (URI) for, this License with every
      copy of the Work You Distribute or Publicly Perform. You may
      not offer or impose any terms on the Work that restrict the
      terms of this License or the ability of the recipient of the
      Work to exercise the rights granted to that recipient under
      the terms of the License. You may not sublicense the Work. You
      must keep intact all notices that refer to this License and to
      the disclaimer of warranties with every copy of the Work You
      Distribute or Publicly Perform. When You Distribute or
      Publicly Perform the Work, You may not impose any effective
      technological measures on the Work that restrict the ability
      of a recipient of the Work from You to exercise the rights
      granted to that recipient under the terms of the License. This
      Section 4(a) applies to the Work as incorporated in a
      Collection, but this does not require the Collection apart
      from the Work itself to be made subject to the terms of this
      License. If You create a Collection, upon notice from any
      Licensor You must, to the extent practicable, remove from the
      Collection any credit as required by Section 4(b), as
      requested. If You create an Adaptation, upon notice from any
      Licensor You must, to the extent practicable, remove from the
      Adaptation any credit as required by Section 4(b), as requested.

   b. If You Distribute, or Publicly Perform the Work or any
      Adaptations or Collections, You must, unless a request has
      been made pursuant to Section 4(a), keep intact all copyright
      notices for the Work and provide, reasonable to the medium or
      means You are utilizing: (i) the name of the Original Author
      (or pseudonym, if applicable) if supplied, and/or if the
      Original Author and/or Licensor designate another party or
      parties (e.g., a sponsor institute, publishing entity,
      journal) for attribution ("Attribution Parties") in Licensor's
      copyright notice, terms of service or by other reasonable
      means, the name of such party or parties; (ii) the title of
      the Work if supplied; (iii) to the extent reasonably
      practicable, the URI, if any, that Licensor specifies to be
      associated with the Work, unless such URI does not refer to
      the copyright notice or licensing information for the Work;
      and (iv), consistent with Section 3(b), in the case of an
      Adaptation, a credit identifying the use of the Work in the
      Adaptation (e.g., "French translation of the Work by Original
      Author," or "Screenplay based on original Work by Original
      Author"). The credit required by this Section 4 (b) may be
      implemented in any reasonable manner; provided, however, that
      in the case of a Adaptation or Collection, at a minimum such
      credit will appear, if a credit for all contributing authors
      of the Adaptation or Collection appears, then as part of these
      credits and in a manner at least as prominent as the credits
      for the other contributing authors. For the avoidance of
      doubt, You may only use the credit required by this Section
      for the purpose of attribution in the manner set out above
      and, by exercising Your rights under this License, You may not
      implicitly or explicitly assert or imply any connection with,
      sponsorship or endorsement by the Original Author, Licensor
      and/or Attribution Parties, as appropriate, of You or Your use
      of the Work, without the separate, express prior written
      permission of the Original Author, Licensor and/or
      Attribution Parties.

   c. Except as otherwise agreed in writing by the Licensor or as
      may be otherwise permitted by applicable law, if You
      Reproduce, Distribute or Publicly Perform the Work either by
      itself or as part of any Adaptations or Collections, You must
      not distort, mutilate, modify or take other derogatory action
      in relation to the Work which would be prejudicial to the
      Original Author's honor or reputation. Licensor agrees that in
      those jurisdictions (e.g. Japan), in which any exercise of the
      right granted in Section 3(b) of this License (the right to
      make Adaptations) would be deemed to be a distortion,
      mutilation, modification or other derogatory action
      prejudicial to the Original Author's honor and reputation, the
      Licensor will waive or not assert, as appropriate, this
      Section, to the fullest extent permitted by the applicable
      national law, to enable You to reasonably exercise Your right
      under Section 3(b) of this License (right to make Adaptations)
      but not otherwise.

5. Representations, Warranties and Disclaimer

UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT
DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF
IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.

6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY
   APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
   LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE
   OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF
   THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY
   OF SUCH DAMAGES.

7. Termination

   a. This License and the rights granted hereunder will terminate
      automatically upon any breach by You of the terms of this
      License. Individuals or entities who have received Adaptations
      or Collections from You under this License, however, will not
      have their licenses terminated provided such individuals or
      entities remain in full compliance with those licenses.
      Sections 1, 2, 5, 6, 7, and 8 will survive any termination of
      this License.

   b. Subject to the above terms and conditions, the license granted
      here is perpetual (for the duration of the applicable
      copyright in the Work). Notwithstanding the above, Licensor
      reserves the right to release the Work under different license
      terms or to stop distributing the Work at any time; provided,
      however that any such election will not serve to withdraw this
      License (or any other license that has been, or is required to
      be, granted under the terms of this License), and this License
      will continue in full force and effect unless terminated as
      stated above.

8. Miscellaneous

   a. Each time You Distribute or Publicly Perform the Work or a
      Collection, the Licensor offers to the recipient a license to
      the Work on the same terms and conditions as the license
      granted to You under this License.

   b. Each time You Distribute or Publicly Perform an Adaptation,
      Licensor offers to the recipient a license to the original
      Work on the same terms and conditions as the license granted
      to You under this License.

   c. If any provision of this License is invalid or unenforceable
      under applicable law, it shall not affect the validity or
      enforceability of the remainder of the terms of this License,
      and without further action by the parties to this agreement,
      such provision shall be reformed to the minimum extent
      necessary to make such provision valid and enforceable.

   d. No term or provision of this License shall be deemed waived
      and no breach consented to unless such waiver or consent shall
      be in writing and signed by the party to be charged with such
      waiver or consent.

   e. This License constitutes the entire agreement between the
      parties with respect to the Work licensed here. There are no
      understandings, agreements or representations with respect to
      the Work not specified here. Licensor shall not be bound by
      any additional provisions that may appear in any communication
      from You. This License may not be modified without the mutual
      written agreement of the Licensor and You.

   f. The rights granted under, and the subject matter referenced,
      in this License were drafted utilizing the terminology of the
      Berne Convention for the Protection of Literary and Artistic
      Works (as amended on September 28, 1979), the Rome Convention
      of 1961, the WIPO Copyright Treaty of 1996, the WIPO
      Performances and Phonograms Treaty of 1996 and the Universal
      Copyright Convention (as revised on July 24, 1971). These
      rights and subject matter take effect in the relevant
      jurisdiction in which the License terms are sought to be
      enforced according to the corresponding provisions of the
      implementation of those treaty provisions in the applicable
      national law. If the standard suite of rights granted under
      applicable copyright law includes additional rights not
      granted under this License, such additional rights are deemed
      to be included in the License; this License is not intended to
      restrict the license of any rights under applicable law.

Creative Commons is not a party to this License, and makes no
warranty whatsoever in connection with the Work. Creative Commons
will not be liable to You or any party on any legal theory for any
damages whatsoever, including without limitation any general,
special, incidental or consequential damages arising in connection
to this license. Notwithstanding the foregoing two (2) sentences,
if Creative Commons has expressly identified itself as the Licensor
hereunder, it shall have all rights and obligations of Licensor.

Except for the limited purpose of indicating to the public that the
Work is licensed under the CCPL, Creative Commons does not authorize
the use by either party of the trademark "Creative Commons" or any
related trademark or logo of Creative Commons without the prior
written consent of Creative Commons. Any permitted use will be in
compliance with Creative Commons' then-current trademark usage
guidelines, as may be published on its website or otherwise made
available upon request from time to time. For the avoidance of
doubt, this trademark restriction does not form part of this
License.

Creative Commons may be contacted at http://creativecommons.org/.

====================================================================
相關文章
相關標籤/搜索