PHP單元測(cè)試-mock和數(shù)據(jù)庫(kù)測(cè)試

在計(jì)算機(jī)編程中,單元測(cè)試(英語(yǔ):Unit Testing)又稱(chēng)為模塊測(cè)試, 是針對(duì)程序模塊(軟件設(shè)計(jì)的最小單位)來(lái)進(jìn)行正確性檢驗(yàn)的測(cè)試工作开缎。程序單元是應(yīng)用的最小可測(cè)試部件裸卫。在過(guò)程化編程中哗蜈,一個(gè)單元就是單個(gè)程序、函數(shù)涝焙、過(guò)程等卑笨;對(duì)于面向?qū)ο缶幊蹋钚卧褪欠椒刈玻ɑ?lèi)(超類(lèi))赤兴、抽象類(lèi)、或者派生類(lèi)(子類(lèi))中的方法隧哮。

本文主要是根據(jù)PHPUnit(The PHP Testing Framework)文檔結(jié)合實(shí)例簡(jiǎn)單介紹一下 PHP 單元測(cè)試中 mock 和數(shù)據(jù)庫(kù)測(cè)試桶良。如果你是初次接觸單測(cè)的話建議先看一下 PHPUnit 文檔中的入門(mén)章節(jié)

通常來(lái)說(shuō)是開(kāi)發(fā)程序和單測(cè)是同步進(jìn)行的近迁,項(xiàng)目提測(cè)的時(shí)候核心模塊都需要包括單測(cè)(報(bào)告)艺普,但這個(gè)要求在不同的公司、部門(mén)、項(xiàng)目組要求不一樣歧譬。雖然單測(cè)會(huì)占用一定的開(kāi)發(fā)時(shí)間但總的來(lái)說(shuō)單測(cè)是利遠(yuǎn)大于弊岸浑,最大的好處是自己或者別人后續(xù)更新模塊功能時(shí)不用擔(dān)心對(duì)原有功能造成了影響而不知情。

下面是一個(gè)具體的例子瑰步,在 web 開(kāi)發(fā)中這樣一個(gè)場(chǎng)景可能很常見(jiàn):PHP 提供一個(gè)帳號(hào)注冊(cè)的接口供前端調(diào)用矢洲,接口先檢驗(yàn)一下此用戶(hù)名是否已經(jīng)存在,不存在的話插入數(shù)據(jù)庫(kù)缩焦,返回注冊(cè)成功读虏。接口代碼是這樣的:

<?php

require_once "lib/Join.php";

require_once "lib/Db.php";

use web\lib\Db;

use web\lib\Join;

username =_POST['username'];

password =_POST['password'];

$db = new Db('user');

join = new Join(db->connect());

echo json_encode(join->signIn(username, $password));

主要調(diào)用了 Join 類(lèi)的 signIn 方法。我們來(lái)看看 Join 類(lèi)是啥樣:

<?php

namespace web\lib;

require_once "Db.php";

class Join

{

private  $db;

function  __construct(Db  $db)

{

    $this->db  =  $db;

}

public  function  signIn($userName,  $password)

{

    if  ($this->db->exists('user',  ['username'  =>  $userName]))  {

        return  [

            'code'  =>  1,

            'msg'  =>  "user has exists",

        ];

    }

    else  {

        $this->db->insert('user',  [

            'username'  =>  $userName,

            'password'  =>  $password,

        ]);

        return  [

            'code'  =>  0,

            'msg'  =>  "success",

        ];

    }

}

}

|

邏輯很簡(jiǎn)單袁滥,先調(diào)用 Db 類(lèi)的 exists 方法判斷用戶(hù)名是否存在盖桥,不存在的話使用 insert 方法插入數(shù)據(jù)。Join 類(lèi)是這次業(yè)務(wù)新加的题翻,比較重要揩徊,需要單測(cè)來(lái)保障質(zhì)量,但這里用到了個(gè) Db 類(lèi)嵌赠,這個(gè)庫(kù)是以前就有的(坑)塑荒,可能會(huì)影響本模塊單測(cè)的正確性,而且 Db 類(lèi)需要連接數(shù)據(jù)庫(kù)姜挺,比較麻煩齿税,這種場(chǎng)景就需要 mock 了。本文說(shuō)的 mock 是廣義上的炊豪,包括 Stubs(樁件)和仿件對(duì)象(Mock Object)凌箕。

將對(duì)象替換為(可選地)返回配置好的返回值的測(cè)試替身的實(shí)踐方法稱(chēng)為上樁(stubbing)×镌冢可以用樁件(stub)來(lái)“替換掉被測(cè)系統(tǒng)所依賴(lài)的實(shí)際組件陌知,這樣測(cè)試就有了對(duì)被測(cè)系統(tǒng)的間接輸入的控制點(diǎn)他托。這使得測(cè)試能強(qiáng)制安排被測(cè)系統(tǒng)的執(zhí)行路徑掖肋,否則被測(cè)系統(tǒng)可能無(wú)法執(zhí)行”。

將對(duì)象替換為能驗(yàn)證預(yù)期行為(例如斷言某個(gè)方法必會(huì)被調(diào)用)的測(cè)試替身的實(shí)踐方法稱(chēng)為模仿(mocking)赏参。

我們這里應(yīng)用的是打樁的概念志笼。signIn 方法有兩個(gè)分支:用戶(hù)名存在和不存在。所以我們需要讓 Db 類(lèi)的 exists 方法在輸入某個(gè)(些)用戶(hù)名的時(shí)候返回 true把篓。主要使用 PHPUnit_Framework_TestCase 類(lèi)提供的 getMockBuilder() 方法來(lái)建立一個(gè)樁件對(duì)象:

// 為Db類(lèi)創(chuàng)建樁件

db =this->getMockBuilder('web\lib\Db')

->getMock();

代碼看上去很像是實(shí)例化了一個(gè)類(lèi)纫溃,其實(shí)原理也和這個(gè)差不多,PHPUnit 通過(guò)反射機(jī)制獲取到類(lèi)及其方法的信息韧掩,然后使用內(nèi)置模板生成一個(gè)新類(lèi)紊浩。我們需要 mock 掉 insertexists 方法:

db =this->getMockBuilder('web\lib\Db')

        ->disableOriginalConstructor()

        ->setMethods(['insert',  'exists'])

        ->getMock();

這里使用了樁件生成器的 setMethods() 方法來(lái)設(shè)置哪些方法被上樁,以下是生成器提供的方法列表:

  • setMethods(array $methods) 可以在仿件生成器對(duì)象上調(diào)用,來(lái)指定哪些方法將被替換為可配置的測(cè)試替身坊谁。其他方法的行為不會(huì)有所改變费彼。如果調(diào)用 setMethods(null),那么沒(méi)有方法會(huì)被替換口芍。
  • setConstructorArgs(array $args) 可用于向原版類(lèi)的構(gòu)造函數(shù)(默認(rèn)情況下不會(huì)被替換為偽實(shí)現(xiàn))提供參數(shù)數(shù)組箍铲。
  • setMockClassName($name) 可用于指定生成的測(cè)試替身類(lèi)的類(lèi)名。
  • disableOriginalConstructor() 參數(shù)可用于禁用對(duì)原版類(lèi)的構(gòu)造方法的調(diào)用鬓椭。
  • disableOriginalClone() 可用于禁用對(duì)原版類(lèi)的克隆方法的調(diào)用颠猴。
  • disableAutoload()可用于在測(cè)試替身類(lèi)的生成期間禁用 __autoload()

然后分別設(shè)置兩個(gè)方法的參數(shù)和返回值小染。這里 insert 操作比較簡(jiǎn)單翘瓮,可以用 willReturn($value) 返回簡(jiǎn)單值:

$db->method('insert')

->willReturn(true);

上面的例子中,使用了 willReturn($value) 返回簡(jiǎn)單值裤翩。這個(gè)簡(jiǎn)短的語(yǔ)法相當(dāng)于 will($this->returnValue($value))春畔。而在這個(gè)長(zhǎng)點(diǎn)的語(yǔ)法中,可以使用變量岛都,從而實(shí)現(xiàn)更復(fù)雜的上樁行為律姨。我們這里的需求是需要根據(jù)預(yù)定義的參數(shù)清單來(lái)返回不同的值,顯然這是一個(gè)映射(map)臼疫,PHPUnit 提供現(xiàn)成的 returnValueMap()方法來(lái)做這個(gè)事情:

// mock method multiple calls with different arguments

$map = [

['user',  ['username'  =>  'yaozhen'],  true],

[$this->anything(),  $this->anything(),  false],

];

$db->method('exists')

->will($this->returnValueMap($map));

完整的單測(cè)代碼:
<?php
namespace web\test;

require_once "../lib/Join.php";

use web\lib\Join;

use PHPUnit_Framework_TestCase;

class JoinTest extends PHPUnit_Framework_TestCase

{

public  function  testSignIn()

{

    // class mock

    $db  =  $this->getMockBuilder('web\lib\Db')

        ->disableOriginalConstructor()

        ->setMethods(['insert',  'exists'])

        ->getMock();

    // function mock

    $db->method('insert')

        ->willReturn(true);

    // mock method multiple calls with different arguments

    $map  =  [

        ['user',  ['username'  =>  'yaozhen'],  true],

        [$this->anything(),  $this->anything(),  false],

    ];

    $db->method('exists')

        ->will($this->returnValueMap($map));

    $join  =  new  Join($db);

    $this->assertEquals(['code'  =>  1,  'msg'  =>  'user has exists'],  $join->signIn('yaozhen',  'pwd'));

    $this->assertEquals(['code'  =>  0,  'msg'  =>  'success'],  $join->signIn('zhangsan',  'pwd'));

}

}

|

這樣就可以很好的測(cè)試 signIn 接口而不用擔(dān)心被依賴(lài)的第三方接口/類(lèi)庫(kù)影響择份。當(dāng)然這只是個(gè)很小的例子,實(shí)際操作比這個(gè)復(fù)雜烫堤,但基本原理類(lèi)似荣赶,結(jié)合實(shí)際、對(duì)照文檔一般的問(wèn)題就能解決了鸽斟,若不能簡(jiǎn)單的解決則說(shuō)明代碼不夠優(yōu)雅拔创,可測(cè)性低,上面的例子也可以看出 db 實(shí)例是通過(guò)依賴(lài)注入的方式當(dāng)做參數(shù)傳入到 Join 類(lèi)中富蓄,這樣即降低了代碼間的耦合也提高了可測(cè)性剩燥。

不過(guò)需要注意的是 finalprivatestatic 方法無(wú)法(其實(shí)可以立倍,但 PHPUnit 不支持)對(duì)其進(jìn)行上樁(stub)或模仿(mock)灭红,關(guān)于這一點(diǎn)是個(gè)經(jīng)常被問(wèn)到的問(wèn)題。不支持 private 方法是因?yàn)檎G闆r下你不需要這樣做口注,因?yàn)槟愣紵o(wú)法調(diào)用私有方法变擒,參考:http://stackoverflow.com/questions/5937845/mock-private-method-with-phpunit。不支持 static 方法是因?yàn)?a target="_blank" rel="nofollow">靜態(tài)方法死于可測(cè)性:靜態(tài)方法調(diào)用很靈活寝志,如果一個(gè)靜態(tài)調(diào)用另一個(gè)靜態(tài)方法就沒(méi)辦法控制被調(diào)方法的依賴(lài)娇斑。其實(shí)在早期版本(<3.9)中是支持靜態(tài)方法的 mock 的策添,但后來(lái)作者經(jīng)過(guò)考慮移除了這一功能(staticExpects 方法),也不提供其它方法支持這一功能:

當(dāng)然這些限制都是可以突破的毫缆,但不建議這樣做舰攒,還是好好改改你的代碼吧,提高可測(cè)性悔醋,這也是單測(cè)帶來(lái)的收益之一摩窃,讓你發(fā)現(xiàn)代碼中不足之處。

上面我們通過(guò)單測(cè)保障了 Join 類(lèi)的代碼質(zhì)量芬骄,但 Db 類(lèi)老是有 bug猾愿,而且每次改動(dòng)還擔(dān)心影響之前的功能。現(xiàn)在考慮把 Db 類(lèi)也加上單測(cè)账阻,但數(shù)據(jù)庫(kù)測(cè)試如何做呢蒂秘?其實(shí)就和平常的測(cè)試一樣,連上數(shù)據(jù)庫(kù)淘太,然后運(yùn)行一下代碼姻僧,最后查看庫(kù)里的數(shù)據(jù)是否符合預(yù)期。PHPUnit 將這個(gè)過(guò)程抽象了出來(lái)蒲牧,提供了相應(yīng)的方法撇贺,下面是具體例子:

一般使用 PHPUnit 時(shí)都是繼承 PHPUnit_Framework_TestCase 方法,但這里是繼承 PHPUnit_Extensions_Database_TestCase 并還需實(shí)現(xiàn)兩個(gè)抽象方法冰抢,getConnection()getDataSet()松嘶。

實(shí)現(xiàn) getConnection():為了讓清理與載入基境的功能正常運(yùn)作,PHPUnit 數(shù)據(jù)庫(kù)擴(kuò)展模塊需要用 PDO 庫(kù)來(lái)實(shí)現(xiàn)跨供應(yīng)商抽象訪問(wèn)數(shù)據(jù)庫(kù)連接挎扰。重要的是要注意到翠订,使用 PHPUnit 的數(shù)據(jù)庫(kù)擴(kuò)展模塊并不要求應(yīng)用程序本身基于 PDO,PDO 連接僅僅用于清理和建立基境遵倦。簡(jiǎn)單來(lái)說(shuō)就是連接上測(cè)試的數(shù)據(jù)庫(kù):

/**

  • connect Database

  • @return \PHPUnit_Extensions_Database_DB_DefaultDatabaseConnection

*/

public function getConnection()

{

$db  =  new  PDO("mysql:host=127.0.0.1;dbname=user",  "root",  "");

return  $this->createDefaultDBConnection($db,  "user");

}

|

實(shí)現(xiàn) getDataSet():getDataSet() 方法定義了在每個(gè)測(cè)試執(zhí)行之前的數(shù)據(jù)庫(kù)初始狀態(tài)應(yīng)該是什么樣尽超。簡(jiǎn)單來(lái)說(shuō)就是每個(gè)單測(cè) case 運(yùn)行之前數(shù)據(jù)庫(kù)的基境。

什么是基境(fixture)梧躺?

基境(fixture)是對(duì)開(kāi)始執(zhí)行某個(gè)測(cè)試時(shí)應(yīng)用程序和數(shù)據(jù)庫(kù)所處初始狀態(tài)的描述似谁。

那么如何創(chuàng)建一個(gè)數(shù)據(jù)集(DataSet)呢,主要有 3 中方法:

  • 基于文件的 DataSet 和 DataTable
  • 基于查詢(xún)的 DataSet 和 DataTable
  • 篩選與組合 DataSet 和 DataTable

這里說(shuō)一下最常用的基于文件的數(shù)據(jù)集燥狰,其它幾種方法的使用請(qǐng)查看文檔棘脐。

XML DataSet (XML 數(shù)據(jù)集):在根節(jié)點(diǎn) <dataset> 內(nèi),可以指定 <table>龙致、<column><row>顷链、<value><null /> 標(biāo)簽目代。例:

<dataset>

<table name="user">

    <column>id</column>

    <column>username</column>

    <column>password</column>

    <row>

        <value>1</value>

        <value>yaozhen</value>

        <value>yaozhenpwd</value>

    </row>

    <row>

        <value>2</value>

        <value>yaozhen2</value>

        <value>yaozhenpwd2</value>

    </row>

</table>

</dataset>

/**

  • Set up fixture

  • @return \PHPUnit_Extensions_Database_DataSet_XmlDataSet

*/

public function getDataSet()

{

return  $this->createXMLDataSet("user.xml");

}

|

注:PHPUnit 假設(shè)在測(cè)試運(yùn)行之前數(shù)據(jù)庫(kù)以及其中的所有表(table)、觸發(fā)器(trigger)、序列(Sequence)和視圖(view)都已經(jīng)創(chuàng)建好榛了。這意味著開(kāi)發(fā)者必須在運(yùn)行測(cè)試套件之前確保數(shù)據(jù)庫(kù)已經(jīng)正確建立在讶。每個(gè)測(cè)試方法運(yùn)行之前都會(huì)調(diào)用此方法清空數(shù)據(jù)庫(kù)中的數(shù)據(jù),然后導(dǎo)入設(shè)置好的數(shù)據(jù)集霜大。

基礎(chǔ)數(shù)據(jù)設(shè)置好后就可以進(jìn)行實(shí)際的操作了构哺,正常實(shí)例化 Db 類(lèi)然后調(diào)用 insert 方法插入數(shù)據(jù):

/**

  • test insert one row

*/

public function testOneInsert()

{

$dbObj  =  new  Db('user');

$dbObj->connect();

$dbObj->insert('user',  [

    'username'  =>  'yaozhen_new_insert',

    'password'  =>  'yaozhenpwd',

]);

}

|

安裝單測(cè)的基本套路,現(xiàn)在就需要對(duì)結(jié)果進(jìn)行斷言了战坤,PHPUnit 提供了 3 類(lèi)數(shù)據(jù)斷言 API:

  1. 對(duì)表中數(shù)據(jù)行的數(shù)量作出斷言
  2. 對(duì)表的狀態(tài)作出斷言
  3. 對(duì)查詢(xún)的結(jié)果作出斷言

這里使用行數(shù)斷言和結(jié)果斷言:

/**

  • test insert one row

*/

public function testOneInsert()

{

$dbObj  =  new  Db('user');

$dbObj->connect();

$dbObj->insert('user',  [

    'username'  =>  'yaozhen_new_insert',

    'password'  =>  'yaozhenpwd',

]);

// table rows num Assertions

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

// Asserting the State of Multiple Tables

$queryTable  =  $this->getConnection()->createQueryTable('user',  "SELECT * FROM user");

$arrayDateSet  =  new  My_DbUnit_ArrayDataSet(['user'  =>  [

        ['id'  =>  1,  'username'  =>  'yaozhen',  'password'  =>  'yaozhenpwd'],

        ['id'  =>  2,  'username'  =>  'yaozhen2',  'password'  =>  'yaozhenpwd2'],

        ['id'  =>  3,  'username'  =>  'yaozhen_new_insert',  'password'  =>  'yaozhenpwd'],

    ],

]);

$expectedTable  =  $arrayDateSet->getTable("user");

$this->assertTablesEqual($expectedTable,  $queryTable);

}

|

查看源碼很容易發(fā)現(xiàn) getRowCount() 方法在底層是通過(guò)執(zhí)行的 SELECT COUNT(*) 來(lái)計(jì)算行數(shù)曙强,createQueryTable() 方法底層也是執(zhí)行傳入的 SQL,可以看出這和我們平常手動(dòng)自測(cè)是一樣途茫。細(xì)心的讀者可能會(huì)發(fā)現(xiàn)數(shù)據(jù)結(jié)果斷言的時(shí)候使用了一個(gè) My_DbUnit_ArrayDataSet 類(lèi)碟嘴,這是一個(gè)自定義的數(shù)組 DataSet 類(lèi),方便產(chǎn)出自己期望的數(shù)據(jù)集囊卜,實(shí)現(xiàn)也非常簡(jiǎn)單:

<?php

namespace web\test;

use PHPUnit_Extensions_Database_DataSet_AbstractDataSet;

use PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData;

use PHPUnit_Extensions_Database_DataSet_DefaultTable;

use PHPUnit_Extensions_Database_DataSet_DefaultTableIterator;

use InvalidArgumentException;

class My_DbUnit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet

{

/**

 * @var array

 */

protected  $tables  =  array();

/**

 * @param array $data

 */

public  function  __construct(array  $data)

{

    foreach  ($data  AS  $tableName  =>  $rows)  {

        $columns  =  array();

        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];

}

}

|

數(shù)據(jù)庫(kù)測(cè)試基本的步驟也就是這些娜扇,其它幾個(gè)方法的單測(cè)很容易舉一反三:

<?php

namespace web\test;

require_once "../lib/Db.php";

require_once "My_DbUnit_ArrayDataSet.php";

use web\lib\Db;

use \PDO;

use PHPUnit_Extensions_Database_TestCase;

class DbTest extends PHPUnit_Extensions_Database_TestCase

{

/**

 * connect Database

 *

 * @return \PHPUnit_Extensions_Database_DB_DefaultDatabaseConnection

 */

public  function  getConnection()

{

    $db  =  new  PDO("mysql:host=127.0.0.1;dbname=user",  "root",  "");

    return  $this->createDefaultDBConnection($db,  "user");

}

/**

 * Set up fixture

 *

 * @return \PHPUnit_Extensions_Database_DataSet_XmlDataSet

 */

public  function  getDataSet()

{

    return  $this->createXMLDataSet("user.xml");

}

public  function  testConnect()

{

    $dbObj  =  new  Db('user');

    $this->assertNotFalse($dbObj->connect());

    $dbObj  =  new  Db('user',  '127.0.0.1',  'root',  '123456');

    $dbObj->connect();

    $this->assertFalse($dbObj->connect());

    $this->assertEquals("Access denied for user 'root'@'localhost' (using password: YES)",  $dbObj->getLastError());

}

/**

 * test insert one row

 */

public  function  testOneInsert()

{

    $dbObj  =  new  Db('user');

    $dbObj->connect();

    $dbObj->insert('user',  [

        'username'  =>  'yaozhen_new_insert',

        'password'  =>  'yaozhenpwd',

    ]);

    // table rows num Assertions

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

    // Asserting the State of Multiple Tables

    $queryTable  =  $this->getConnection()->createQueryTable('user',  "SELECT * FROM user");

    $arrayDateSet  =  new  My_DbUnit_ArrayDataSet(['user'  =>  [

            ['id'  =>  1,  'username'  =>  'yaozhen',  'password'  =>  'yaozhenpwd'],

            ['id'  =>  2,  'username'  =>  'yaozhen2',  'password'  =>  'yaozhenpwd2'],

            ['id'  =>  3,  'username'  =>  'yaozhen_new_insert',  'password'  =>  'yaozhenpwd'],

        ],

    ]);

    $expectedTable  =  $arrayDateSet->getTable("user");

    $this->assertTablesEqual($expectedTable,  $queryTable);

}

/**

 * test isnert mutlti row

 *

 * @depends testOneInsert

 */

public  function  testMultiInsert()

{

    $dbObj  =  new  Db('user');

    $dbObj->connect();

    $dbObj->insert('user',  [

        [

            'username'  =>  true,

            'password'  =>  'yaozhenpwd',

        ],

        [

            'username'  =>  'yaozhen_new_insert2',

            'password'  =>  null,

        ],

        [

            'username'  =>  'yaozhen_new_insert3',

            'password'  =>  123,

        ],

    ]);

    // table rows num Assertions

    $this->assertEquals(5,  $this->getConnection()->getRowCount('user'),  "Inserting failed");

}

/**

 * test exists func

 */

public  function  testExists()

{

    $dbObj  =  new  Db('user');

    $dbObj->connect();

    $result  =  $dbObj->exists('user',  [

        'username'  =>  'yaozhen',

        'password'  =>  'yaozhenpwd',

    ]);

    $this->assertTrue($result);

    $result  =  $dbObj->exists('user',  [

        'username'  =>  true,

        'password'  =>  null,

    ]);

    $this->assertFalse($result);

}

}

|

至此,你已經(jīng)學(xué)會(huì)了基本的 Mock 和數(shù)據(jù)庫(kù)測(cè)試栅组。當(dāng)然雀瓢,實(shí)際中比這個(gè)更復(fù)雜,但萬(wàn)變不離其宗玉掸,掌握了基本套路致燥,其它的看看文檔也很快就能搞定了。

最后排截,本文中的代碼因?yàn)閷?shí)際生產(chǎn)環(huán)境 PHP 版本(=5.4)原因嫌蚤,使用的是 PHPunit 4.8 版本,如果你使用的是 5.X 版本那可能有點(diǎn)不一樣断傲,還需你對(duì)照手冊(cè)來(lái)參考脱吱。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市认罩,隨后出現(xiàn)的幾起案子箱蝠,更是在濱河造成了極大的恐慌,老刑警劉巖垦垂,帶你破解...
    沈念sama閱讀 217,084評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宦搬,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡劫拗,警方通過(guò)查閱死者的電腦和手機(jī)间校,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)页慷,“玉大人憔足,你說(shuō)我怎么就攤上這事胁附。” “怎么了滓彰?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,450評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵控妻,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我揭绑,道長(zhǎng)弓候,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,322評(píng)論 1 293
  • 正文 為了忘掉前任他匪,我火速辦了婚禮菇存,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘诚纸。我一直安慰自己撰筷,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布畦徘。 她就那樣靜靜地躺著毕籽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪井辆。 梳的紋絲不亂的頭發(fā)上关筒,一...
    開(kāi)封第一講書(shū)人閱讀 51,274評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音杯缺,去河邊找鬼蒸播。 笑死,一個(gè)胖子當(dāng)著我的面吹牛萍肆,可吹牛的內(nèi)容都是我干的袍榆。 我是一名探鬼主播哪审,決...
    沈念sama閱讀 40,126評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼磷蜀,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了孩饼?” 一聲冷哼從身側(cè)響起亲铡,我...
    開(kāi)封第一講書(shū)人閱讀 38,980評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤才写,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后奖蔓,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體赞草,經(jīng)...
    沈念sama閱讀 45,414評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評(píng)論 3 334
  • 正文 我和宋清朗相戀三年吆鹤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了厨疙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,773評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡檀头,死狀恐怖轰异,靈堂內(nèi)的尸體忽然破棺而出岖沛,到底是詐尸還是另有隱情暑始,我是刑警寧澤搭独,帶...
    沈念sama閱讀 35,470評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站廊镜,受9級(jí)特大地震影響牙肝,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嗤朴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評(píng)論 3 327
  • 文/蒙蒙 一配椭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧雹姊,春花似錦股缸、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,713評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至歧杏,卻和暖如春镰惦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背犬绒。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,852評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工旺入, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凯力。 一個(gè)月前我還...
    沈念sama閱讀 47,865評(píng)論 2 370
  • 正文 我出身青樓茵瘾,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親咐鹤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拗秘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容