2015/09/23

PHP Mockery

去上了鐵哥的 PHP 測試的課程,回來一股腦的想要把每個看的到的可怕東西都 refactor,但鐵哥有交代,refactor 之前最好還是先下測試,正所謂先套保命索,再去走鋼索,好詩好詩,於是這幾天我除了重複看鐵哥的 readme 想熟記心法以外,完全醉心於 Mockery 了,之前也寫過一陣子測試,但有些情況很難模擬,部分原因是自己 code 拆的不夠細不好測,不然就是某些情況必須有外部的結果才能測試,例如說 mail 啦、資料庫等等,鐵哥介紹 Mockery 這個好用的工具以後解決了我的問題,而且我發現寫 code 為了想要好 mock 來拆 code 變成一種 refactoring 的依據,筆記一下這幾天的心得,用最簡單的方式解說,中間跳過一些繁複的過程。

DI

Game.php
class Game
{
    public function result(DB $db)
    {
        return $db->data();
    }
}
DB.php
class DB
{
    public function data()
    {
        return false;
    }
}

上面執行 var_dump((new Game)->result(new DB)); 的結果會是 false,我們來撰寫測試。

GameTest.php
use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $db = new DB;
        $game = new Game();

        $this->assertTrue($game->result($db));
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

F

Time: 519 ms, Memory: 9.75Mb

There was 1 failure:

1) GameTest::testResult
Failed asserting that false is true.

D:\www\phpunit\tests\GameTest.php:12

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

因為 DBdata() 回傳的是 false,讓我們來 mock 他。

use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $db = m::mock('DB');
        $db->shouldReceive('data')
            ->once()
            ->andReturn(true);
        $game = new Game();

        $this->assertTrue($game->result($db));
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

.

Time: 485 ms, Memory: 10.25Mb

OK (1 test, 1 assertion)

Mock 內部呼叫 method

Game.php
class Game
{
    public function result()
    {
        return $this->data();
    }

    public function data()
    {
        return false;
    }
}

執行 var_dump((new Game)->result()); 的結果為 false

GameTest.php
use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $game = new Game();

        $this->assertTrue($game->result());
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

F

Time: 415 ms, Memory: 9.75Mb

There was 1 failure:

1) GameTest::testResult
Failed asserting that false is true.

D:\www\phpunit\tests\GameTest.php:11

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

讓我們使用 partial 來 mock 他。

use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $game = m::mock('Game[data]');
        $game->shouldReceive('data')
            ->once()
            ->andReturn(true);

        $this->assertTrue($game->result());
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

.

Time: 442 ms, Memory: 10.25Mb

OK (1 test, 1 assertion)

Mock new class

Game.php
class Game
{
    public function result()
    {
        $db = new DB;

        return $db->data();
    }
}

執行 var_dump((new Game)->result()); 結果為 false

GameTest.php
use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $game = new Game;

        $this->assertTrue($game->result());
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

F

Time: 403 ms, Memory: 9.75Mb

There was 1 failure:

1) GameTest::testResult
Failed asserting that false is true.

D:\www\phpunit\tests\GameTest.php:11

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

讓我們用 overload 來 mock 他。

use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $game = new Game;
        $mock = m::mock('overload:DB');
        $mock->shouldReceive('data')
            ->once()
            ->andReturn(true);

        $this->assertTrue($game->result());
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

.

Time: 466 ms, Memory: 10.25Mb

OK (1 test, 1 assertion)

overload 目前發現對 static method 也有效用,但他會無法偵測到其他事件,例如 once()twice(),所以 static function 建議抽出來執行。

class Game
{
    public function result()
    {
        $data = $this->data();

        return $data;
    }

    public function data()
    {
        return DB::data();
    }
}

這樣就可以使用剛介紹 partial 的方法驗證了。

沒有留言: