安裝phpunit
phar
$ wget https://phar.phpunit.de/phpunit-latest.phar
$ mv phpunit-latest.phar phpunit
$ chmod +x phpunit
$ ./phpunit --version
PHPUnit x.y.z by Sebastian Bergmann and contributors.
composer
composer require --dev phpunit/phpunit ^latest
執行
PS I:\src\php-shiyanchang> .\vendor\bin\phpunit --version
PHPUnit 7.0.0 by Sebastian Bergmann and contributors.
編寫phpunit測試代碼
依賴關系
生產者(producer),是能生成被測單元并將其作為返回值的測試方法。
消費者(consumer),是依賴于一個或多個生產者及其返回值的測試方法。
用 @depends 標注來表示依賴關系
declare(strict_types=1);
use \PHPUnit\Framework\TestCase;
final class StackTest extends TestCase
{
public function testEmpty(): array
{
$stack = [];
$this->assertEmpty($stack);
return $stack;
}
/**
* @depends testEmpty
*/
public function testPush(array $stack): array
{
array_push($stack, 'foo');
$this->assertSame('foo', $stack[count($stack)-1]);
$this->assertNotEmpty($stack);
return $stack;
}
/**
* @return string
*/
public function testGetName() :string
{
$string = "tsingchan";
$this->assertStringStartsWith("tsing", $string);
return $string;
}
/**
* @depends testPush
* @depends testGetName
*/
public function testPop(array $stack,string $name): void
{
$stack[]=$name;
$this->assertSame('tsingchan', array_pop($stack));
$this->assertSame('foo', array_pop($stack));
$this->assertEmpty($stack);
// $this->assertNotEmpty($stack);
}
}
數據供給器
用 @dataProvider 標注來指定要使用的數據供給器方法。
數據供給器方法必須聲明為 public,其返回值要么是一個數組,其每個元素也是數組;要么是一個實現了 Iterator 接口的對象,在對它進行迭代時每步產生一個數組。每個數組都是測試數據集的一部分,將以它的內容作為參數來調用測試方法。
當使用到大量數據集時,最好逐個用字符串鍵名對其命名,避免用默認的數字鍵名。這樣輸出信息會更加詳細些,其中將包含打斷測試的數據集所對應的名稱。
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DataTest extends TestCase
{
/**
* @dataProvider additionProvider1
* @dataProvider additionProvider2
*/
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
public function additionProvider1(): array
{
return [
'adding zeros' => [0, 0, 0],
'zero plus one' => [0, 1, 1],
'one plus zero' => [1, 0, 1],
'one plus one' => [1, 1, 3]
];
}
public function additionProvider2(): array
{
return [
'0+0' => [1, 2, 3],
'0+1' => [2, 2, 4],
'2+3' => [2, 3, 5],
'3+2' => [3, 2, 5]
];
}
}
注意:所有數據供給器方法的執行都是在對 setUpBeforeClass() 靜態方法的調用和第一次對 setUp() 方法的調用之前完成的。因此,無法在數據供給器中使用創建于這兩個方法內的變量。這是必須的,這樣 PHPUnit 才能計算測試的總數量。
異常判斷
用 @expectException 標注來測試被測代碼中是否拋出了異常。也可以使用方法:$this->expectException(TypeError::class);
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ExceptionTest extends TestCase
{
/**
* @expectedException TypeError
* @return void
*/
public function testException(): void
{
// $this->expectException(TypeError::class); //注解 @exceptionException和這行的效果是一樣的
$this->add("a");//這里可以調用業務邏輯類的代碼,這里add只是為了范例方便
}
public function add(int $num):int{
return $num;
}
}
除了 expectException() 方法外,還有 expectExceptionCode()、expectExceptionMessage() 和 expectExceptionMessageMatches() 方法可以用于為被測代碼所拋出的異常建立預期。
對輸出進行測試
有時候,想要斷言(比如說)某方法的運行過程中生成了預期的輸出(例如,通過 echo 或 print)。PHPUnit\Framework\TestCase 類使用 PHP 的輸出緩沖特性來為此提供必要的功能支持。
何用 expectOutputString() 方法來設定所預期的輸出。如果沒有產生預期的輸出,測試將計為失敗。
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class OutputTest extends TestCase
{
public function testExpectFooActualFoo(): void
{
$this->expectOutputString('foo');
print 'foo';
}
public function testExpectBarActualBaz(): void
{
$this->expectOutputString('bar');
print 'baz';
}
}
執行phpunit命令行
命令行選項
$ phpunit --help
PHPUnit latest.0 by Sebastian Bergmann and contributors.
Usage:
phpunit [options] 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
--coverage-cache <dir> Cache static analysis results
--warm-coverage-cache Warm static analysis cache
--coverage-filter <dir> Include <dir> in code coverage analysis
--path-coverage Perform path coverage analysis
--disable-coverage-ignore Disable annotations for ignoring code coverage
--no-coverage Ignore code coverage configuration
Logging Options:
--log-junit <file> Log test execution in JUnit XML format to file
--log-teamcity <file> Log test execution in TeamCity format to file
--testdox-html <file> Write agile documentation in HTML format to file
--testdox-text <file> Write agile documentation in Text format to file
--testdox-xml <file> Write agile documentation in XML format to file
--reverse-list Print defects in reverse order
--no-logging Ignore logging configuration
Test Selection Options:
--filter <pattern> Filter which tests to run
--testsuite <name> Filter which testsuite to run
--group <name> Only runs tests from the specified group(s)
--exclude-group <name> Exclude tests from the specified group(s)
--list-groups List available test groups
--list-suites List available test suites
--list-tests List available tests
--list-tests-xml <file> List available tests in XML format
--test-suffix <suffixes> Only search for test in files with specified suffix(es). Default: Test.php,.phpt
Test Execution Options:
--dont-report-useless-tests Do not report tests that do not test anything
--strict-coverage Be strict about @covers annotation usage
--strict-global-state Be strict about changes to global state
--disallow-test-output Be strict about output during tests
--disallow-resource-usage Be strict about resource usage during small tests
--enforce-time-limit Enforce time limit based on test size
--default-time-limit <sec> Timeout in seconds for tests without @small, @medium or @large
--disallow-todo-tests Disallow @todo-annotated tests
--process-isolation Run each test in a separate PHP process
--globals-backup 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-defect Stop execution upon first not-passed test
--stop-on-error Stop execution upon first error
--stop-on-failure Stop execution upon first error or failure
--stop-on-warning Stop execution upon first warning
--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
--fail-on-incomplete Treat incomplete tests as failures
--fail-on-risky Treat risky tests as failures
--fail-on-skipped Treat skipped tests as failures
--fail-on-warning Treat tests with warnings as failures
-v|--verbose Output more verbose information
--debug Display debugging information
--repeat <times> Runs the test(s) repeatedly
--teamcity Report test execution progress in TeamCity format
--testdox Report test execution progress in TestDox format
--testdox-group Only include tests from the specified group(s)
--testdox-exclude-group Exclude tests from the specified group(s)
--no-interaction Disable TestDox progress animation
--printer <printer> TestListener implementation to use
--order-by <order> Run tests in order: default|defects|duration|no-depends|random|reverse|size
--random-order-seed <N> Use a specific random seed <N> for random order
--cache-result Write test results to cache file
--do-not-cache-result Do not write test results to cache file
Configuration Options:
--prepend <file> A PHP script that is included as early as possible
--bootstrap <file> A PHP script that is included before the tests run
-c|--configuration <file> Read configuration from XML file
--no-configuration Ignore default configuration file (phpunit.xml)
--extensions <extensions> A comma separated list of PHPUnit extensions to load
--no-extensions Do not load PHPUnit extensions
--include-path <path(s)> Prepend PHP's include_path with given path(s)
-d <key[=value]> Sets a php.ini value
--cache-result-file <file> Specify result cache path and filename
--generate-configuration Generate configuration file with suggested settings
--migrate-configuration Migrate configuration file to current format
Miscellaneous Options:
-h|--help Prints this usage information
--version Prints the version and exits
--atleast-version <min> Checks that version is greater than min and exits
--check-version Check whether PHPUnit is the latest version
TestDox
組織測試方式
支持文件或XML編排測試套件
如果有IDE集成了測試套件,IDE上就會有個按鈕是執行所有測試類文件。
跳過測試
用 @requires 來跳過測試
類型 | 可能值 | 示例 | 其他示例 | |
---|---|---|---|---|
PHP | 任意 PHP 版本號以及可選的運算符 | @requires PHP 7.1.20 | @requires PHP >= 7.2 | |
PHPUnit | 任意 PHPUnit 版本號以及可選的運算符 | @requires PHPUnit 7.3.1 | @requires PHPUnit < 8 | |
OS | 與 PHP_OS 匹配的正則表達式 | @requires OS Linux | @requires OS WIN32 | WINNT |
OSFAMILY | 任意 OS family | @requires OSFAMILY Solaris | @requires OSFAMILY Windows | |
function | 任意 function_exists 的有效參數 | @requires function imap_open | @requires function ReflectionMethod::setAccessible | |
extension | 任意擴展名以及可選的版本號和可選的運算符 | @requires extension mysqli | @requires extension redis >= 2.2.0 |
HP、PHPUnit 和擴展的版本約束支持以下運算符:<、<=、>、>=、=、==、!=、<>。
版本是用 PHP 的 version_compare 函數進行比較的。除了其他事情之外,這意味著 = 和 == 運算符只能用于完整的 X.Y.Z 版本號,只用 X.Y 是不行的。
use PHPUnit\Framework\TestCase;
/**
* @requires extension mysqli
*/
final class DatabaseTest extends TestCase
{
/**
* @requires PHP >= 7.0
*/
public function testConnection(): void
{
// 測試需要 mysqli 擴展,并且要求 PHP >= 7.0
}
// ... 其他需要 mysqli 擴展的測試
}
測試替身
有時候對被測系統進行測試是很困難的,因為它依賴于其他無法在測試環境中使用的組件。這有可能是因為這些組件不可用,它們不會返回測試所需要的結果,或者執行它們會有不良副作用。
如果在編寫測試時無法使用(或選擇不使用)實際的依賴組件,可以用測試替身來代替。測試替身不需要和真正的依賴組件有完全一樣的的行為方式;它只需要提供和真正的組件同樣的 API 即可,這樣被測系統就會以為它是真正的組件!
PHPUnit 提供的 createStub(type) 和 getMockBuilder($type) 方法可以在測試中用來自動生成對象,此對象可以充當任意指定原版類型(接口或類名)的測試替身。在任何預期或要求使用原版類的實例對象的上下文中都可以使用這個測試替身對象來代替。
createStub(type) 方法直接返回指定類型(接口或類)的測試替身對象實例。
打樁Stubs
將對象替換為(可選地)返回配置好的返回值的測試替身的實踐方法稱為打樁(stubbing)。可以用樁件(Stub)來“替換掉被測系統所依賴的實際組件,這樣測試就有了對被測系統的間接輸入的控制點。這使得測試能強制安排被測系統的執行路徑,否則被測系統可能無法執行”
業務邏輯類,上樁類:SomeClass:
declare(strict_types=1);
class SomeClass
{
public function doSomething()
{
// 隨便做點什么。
}
}
測試類對某個方法的調用進行上樁,返回固定值:
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testStub(): void
{
// 為 SomeClass 類創建樁件。
$stub = $this->createStub(SomeClass::class);
// 配置樁件。
$stub->method('doSomething')
->willReturn('foo');
// 現在調用 $stub->doSomething() 會返回 'foo'。
$this->assertSame('foo', $stub->doSomething());
}
用 willReturn(this->returnValue($value))。而在這個長點的語法中,可以使用變量,從而實現更復雜的上樁行為。
用returnArgument返回方法參數:
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testReturnArgumentStub(): void
{
// 為 SomeClass 類創建樁件。
$stub = $this->createStub(SomeClass::class);
// 配置樁件。
$stub->method('doSomething')
->will($this->returnArgument(0));
// $stub->doSomething('foo') 返回 'foo'
$this->assertSame('foo', $stub->doSomething('foo'));
// $stub->doSomething('bar') 返回 'bar'
$this->assertSame('bar', $stub->doSomething('bar'));
}
}
用 returnSelf()返回樁對象:
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testReturnSelf(): void
{
// 為 SomeClass 類創建樁件。
$stub = $this->createStub(SomeClass::class);
// 配置樁件。
$stub->method('doSomething')
->will($this->returnSelf());
// $stub->doSomething() 返回 $stub
$this->assertSame($stub, $stub->doSomething());
}
}
更多對方法的調用上樁的方式詳見:8. 測試替身 — PHPUnit latest 手冊
仿件對象(Mock Object)
仿件對象是為了模仿被調用對象。
業務邏輯代碼采用了觀察者模式:
declare(strict_types=1);
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)
{
// 隨便做點什么
}
// 其他方法。
}
首先用 PHPUnit\Framework\TestCase 類提供的 createMock() 方法來為 Observer 建立仿件對象。
由于關注的是檢驗某個方法是否被調用,以及調用時具體所使用的參數,因此引入 expects() 與 with() 方法來指明此交互應該是什么樣的。
php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class SubjectTest extends TestCase
{
public function testObserversAreUpdated(): void
{
// 為 Observer 類建立仿件
// 只模仿 update() 方法。
$observer = $this->createMock(Observer::class);
// 為 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();
}
}