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 的方法驗證了。

2015/09/16

jQuery Map And Get

我們常常會用到 jQuery 去 fetch 一段 DOM 以後拿到值來做事情,舉例來說今天有三個 input。

HTML
<input type="text" value="a">
<input type="text" value="b">
<input type="text" value="c">

我們想要 fetch DOM 把所有的 value 拿出來並且組成 a, b, c 這樣的字串該怎麼做,方法當然很多,一般來說會利用 $.each 來組。

$.each
var result = [];

$(':input').each(function() {
    result.push(this.value);   
});

result = result.join(',');

console.log(result); // a,b,c

如果理解更多的 jQuery API 的話,可以用一些特殊技巧用更少的行數達成某些結果。

$.map
var result = $(':input').map(function() {
    return this.value;   
}).get().join(',');

console.log(result); // a,b,c

mapget 是 jQuery 常拿來處理陣列的技巧,如果善加利用可以省去很多迴圈或是宣告的事情。