在編寫測(cè)試時(shí),最費(fèi)時(shí)的部分之一是編寫代碼來將整個(gè)場(chǎng)景設(shè)置成某個(gè)已知的狀態(tài),并在測(cè)試結(jié)束后將其復(fù)原到初始狀態(tài)。這個(gè)已知的狀態(tài)稱為測(cè)試的基境(?fixture
?)。
在用 PHPUnit 測(cè)試數(shù)組操作中,基境就是存儲(chǔ)在 ?$stack
? 變量中的數(shù)組。然而,絕大多數(shù)時(shí)候基境均遠(yuǎn)比一個(gè)簡(jiǎn)單數(shù)組要復(fù)雜,用于建立基境的代碼量也會(huì)隨之增長(zhǎng)。測(cè)試的真正內(nèi)容就被淹沒于建立基境帶來的干擾中。當(dāng)編寫多個(gè)需要類似基境的測(cè)試時(shí)這個(gè)問題就變得更糟糕了。如果沒有來自于測(cè)試框架的幫助,就不得不在寫每一個(gè)測(cè)試時(shí)都將建立基境的代碼重復(fù)一次。
PHPUnit 支持共享建立基境的代碼。在運(yùn)行某個(gè)測(cè)試方法前,會(huì)調(diào)用一個(gè)名叫 ?setUp()
? 的模板方法。?setUp()
? 是創(chuàng)建測(cè)試所用對(duì)象的地方。當(dāng)測(cè)試方法運(yùn)行結(jié)束后,不管是成功還是失敗,都會(huì)調(diào)用另外一個(gè)名叫 ?tearDown()
? 的模板方法。?tearDown()
? 是清理測(cè)試所用對(duì)象的地方。
在用 ?@depends
? 標(biāo)注來表示依賴關(guān)系中,我們?cè)跍y(cè)試之間運(yùn)用生產(chǎn)者-消費(fèi)者關(guān)系來共享基境。這并非總是預(yù)期的方式,甚至有時(shí)是不可能的。示例 4.1 展示了另外一個(gè)編寫測(cè)試 ?StackTest
?的方式。在這個(gè)方式中,不再重用基境本身,而是重用建立基境的代碼。首先聲明一個(gè)實(shí)例變量,?$stack
?,用來替代方法內(nèi)的局部變量。然后把 ?array
?基境的建立放到 ?setUp()
? 方法中。最后,從測(cè)試方法中去除冗余代碼,在 ?assertSame()
? 斷言方法中使用新引入的實(shí)例變量 ?$this->stack
? 替代方法內(nèi)的局部變量 ?$stack
?。
示例 4.1 用 setUp() 來創(chuàng)建堆棧基境
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StackTest extends TestCase
{
private $stack;
protected function setUp(): void
{
$this->stack = [];
}
public function testEmpty(): void
{
$this->assertTrue(empty($this->stack));
}
public function testPush(): void
{
array_push($this->stack, 'foo');
$this->assertSame('foo', $this->stack[count($this->stack)-1]);
$this->assertFalse(empty($this->stack));
}
public function testPop(): void
{
array_push($this->stack, 'foo');
$this->assertSame('foo', array_pop($this->stack));
$this->assertTrue(empty($this->stack));
}
}
測(cè)試類的每個(gè)測(cè)試方法都會(huì)運(yùn)行一次 ?setUp()
? 和 ?tearDown()
? 模板方法(同時(shí),每個(gè)測(cè)試方法都是在一個(gè)全新的測(cè)試類實(shí)例上運(yùn)行的)。
另外,?setUpBeforeClass()
? 與 ?tearDownAfterClass()
? 模板方法將分別在測(cè)試用例類的第一個(gè)測(cè)試運(yùn)行之前和測(cè)試用例類的最后一個(gè)測(cè)試運(yùn)行之后調(diào)用。
下面這個(gè)例子中展示了測(cè)試用例類中所有可用的模板方法。
示例 4.2 展示所有可用模板方法的示例
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class TemplateMethodsTest extends TestCase
{
public static function setUpBeforeClass(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function setUp(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function assertPreConditions(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
public function testOne(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
$this->assertTrue(true);
}
public function testTwo(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
$this->assertTrue(false);
}
protected function assertPostConditions(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function tearDown(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
public static function tearDownAfterClass(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function onNotSuccessfulTest(Throwable $t): void
{
fwrite(STDOUT, __METHOD__ . "\n");
throw $t;
}
}
$ phpunit TemplateMethodsTest
PHPUnit latest.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.
理論上說,?setUp()
? 和 ?tearDown()
? 是精確對(duì)稱的,但是實(shí)踐中并非如此。實(shí)際上,只有在 ?setUp(
?) 中分配了諸如文件或套接字之類的外部資源時(shí)才需要實(shí)現(xiàn) ?tearDown()
? 。如果 ?setUp()
? 中只創(chuàng)建純 PHP 對(duì)象,通??梢月赃^ ?tearDown()
?。不過,如果在 ?setUp()
? 中創(chuàng)建了大量對(duì)象,你可能想要在 ?tearDown()
? 中 ?unset()
? 指向這些對(duì)象的變量,這樣它們就可以被垃圾回收機(jī)制回收掉。對(duì)測(cè)試用例對(duì)象的垃圾回收動(dòng)作則是不可預(yù)知的。
如果擁有兩個(gè)測(cè)試,它們的基境建立工作略有不同,該怎么辦?有兩種可能:
setUp()
? 代碼僅有微小差異,把有差異的代碼內(nèi)容從 ?setUp()
? 移到測(cè)試方法內(nèi)。setUp()
? 是確實(shí)不一樣,那么需要另外一個(gè)測(cè)試用例類。參考基境建立工作的不同之處來命名這個(gè)類。有幾個(gè)好的理由來在測(cè)試之間共享基境,但是大部分情況下,在測(cè)試之間共享基境的需求都源于某個(gè)未解決的設(shè)計(jì)問題。
一個(gè)有實(shí)際意義的多測(cè)試間共享基境的例子是數(shù)據(jù)庫連接:只登錄數(shù)據(jù)庫一次,然后重用此連接,而不是每個(gè)測(cè)試都建立一個(gè)新的數(shù)據(jù)庫連接。這樣能加快測(cè)試的運(yùn)行。
示例 4.3 中用 ?setUpBeforeClass()
? 和 ?tearDownAfterClass()
? 模板方法來分別在測(cè)試用例類的第一個(gè)測(cè)試之前和最后一個(gè)測(cè)試之后連接與斷開數(shù)據(jù)庫。
示例 4.3 在同一個(gè)測(cè)試套件內(nèi)的不同測(cè)試之間共享基境
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DatabaseTest extends TestCase
{
private static $dbh;
public static function setUpBeforeClass(): void
{
self::$dbh = new PDO('sqlite::memory:');
}
public static function tearDownAfterClass(): void
{
self::$dbh = null;
}
}
需要反復(fù)強(qiáng)調(diào)的是:在測(cè)試之間共享基境會(huì)降低測(cè)試的價(jià)值。潛在的設(shè)計(jì)問題是對(duì)象之間并非松散耦合。如果解決掉潛在的設(shè)計(jì)問題并使用樁件(stub)來編寫測(cè)試,就能達(dá)成更好的結(jié)果,而不是在測(cè)試之間產(chǎn)生運(yùn)行時(shí)依賴并錯(cuò)過改進(jìn)設(shè)計(jì)的機(jī)會(huì)。
使用單件(singleton)的代碼很難測(cè)試。使用全局變量的代碼也一樣。通常情況下,欲測(cè)代碼和全局變量之間會(huì)強(qiáng)烈耦合,并且其創(chuàng)建無法控制。另外一個(gè)問題是,一個(gè)測(cè)試對(duì)全局變量的改變可能會(huì)破壞另外一個(gè)測(cè)試。
在 PHP 中,全局變量是這樣運(yùn)作的:
$foo = 'bar';
?實(shí)際上是存儲(chǔ)為 ?$GLOBALS['foo'] = 'bar';
?的。$GLOBALS
?這個(gè)變量是一種被稱為超全局變量的變量。$foo
?,可以直接訪問 ?$GLOBALS['foo']
?,或者用 ?global $foo;
? 來創(chuàng)建一個(gè)引用全局變量的局部變量。除了全局變量,類的靜態(tài)屬性也是一種全局狀態(tài)。
在版本 6 之前,默認(rèn)情況下,PHPUnit 用一種更改全局變量與超全局變量(?$GLOBALS
?、?$_ENV
?、?$_POST
?、?$_GET
?、?$_COOKIE
?、?$_SERVER
?、?$_FILES
?、?$_REQUEST
?)不會(huì)影響到其他測(cè)試的方式來運(yùn)行所有測(cè)試。
在版本 6 中,默認(rèn)情況下 PHPUnit 不再對(duì)全局變量和超全局變量進(jìn)行這種備份與恢復(fù)的操作。可以用 ?--globals-backup
? 選項(xiàng)或在 XML 配置文件中用 ?backupGlobals="true"
? 將其激活。
通過用 ?--static-backup
? 選項(xiàng)或在 XML 配置文件中設(shè)置 ?backupStaticAttributes="true"
?,可以將此隔離擴(kuò)展到類的靜態(tài)屬性。
注解:
對(duì)全局變量和類的靜態(tài)屬性的備份與還原操作使用了 ?serialize()
? 與 ?unserialize()
?
某些類的實(shí)例對(duì)象(比如 ?PDO
?)無法序列化,因此如果把這樣一個(gè)對(duì)象存放在比如說 ?$GLOBALS
? 數(shù)組內(nèi)時(shí),備份操作就會(huì)出問題。
?@backupGlobals
? 標(biāo)注可以用來控制對(duì)全局變量的備份與還原操作。另外,還可以提供一個(gè)全局變量的名單,名單中的全局變量將被排除于備份與還原操作之外,就像這樣:
final class MyTest extends TestCase
{
protected $backupGlobalsExcludeList = ['globalVariable'];
// ...
}
注解:
在方法(例如 ?setUp()
? 方法)內(nèi)對(duì) ?$backupGlobalsBlacklist
? 屬性進(jìn)行設(shè)置是無效的。
?@backupStaticAttributes
? 標(biāo)注可以用于在每個(gè)測(cè)試之前備份所有已聲明類的靜態(tài)屬性值并在其后恢復(fù)。
它所處理的并不只是測(cè)試類自身,而是在測(cè)試開始時(shí)已聲明的所有類。它只作用于靜態(tài)類屬性,不作用于函數(shù)內(nèi)聲明的靜態(tài)變量。
注解:
只有啟用了 ?@backupStaticAttributes
? 的測(cè)試方法才會(huì)在方法之前執(zhí)行此操作。如果在此之前運(yùn)行的某個(gè)沒有啟用 ?@backupStaticAttributes
? 的測(cè)試方法改變了靜態(tài)屬性的值,那么被備份及還原的將會(huì)是這個(gè)改變后的值——而非初始聲明時(shí)提供的默認(rèn)值。PHP 并不額外記錄任何靜態(tài)變量的聲明時(shí)提供的初始默認(rèn)值。
同樣的情況也發(fā)生于測(cè)試內(nèi)部新加載/聲明的類的靜態(tài)屬性上。它們也無法在測(cè)試結(jié)束之后復(fù)原為聲明時(shí)提供的原始默認(rèn)值,因?yàn)闊o從得知這些默認(rèn)值。這些被修改過的值會(huì)泄漏到后繼測(cè)試中。
對(duì)單元測(cè)試而言,推薦在 ?setUp()
? 中顯式的重置測(cè)試中使用到的靜態(tài)屬性(最好同時(shí)在 ?tearDown()
? 中執(zhí)行重置,這樣就保證不會(huì)影響到后繼的測(cè)試)。
可以提供名單來將靜態(tài)屬性從備份與還原操作中排除出去:
final class MyTest extends TestCase
{
protected $backupStaticAttributesExcludeList = [
'className' => ['attributeName']
];
// ...
}
注解:
在方法(例如 ?setUp()
? 方法)內(nèi)對(duì) ?$backupStaticAttributesExcludeList
? 屬性進(jìn)行設(shè)置是無效的。
更多建議: