iT邦幫忙

2023 iThome 鐵人賽

DAY 26
0
Software Development

開心撰寫 PHPUnit系列 第 26

Day 26. 兩個物件互動測試 - 變更 Mock 對象讓測試更穩固

  • 分享至 

  • xImage
  •  

在上一篇我們是 mock 的對象是 Home 以及 Board,但會發現一件事情,當我們的 Home 或 Board 這兩隻程式的回傳值一但變更是完全不會反應該在測試上面的,這樣的測試雖然能通過,但我們是完成無法信任它的。

為了解決這個問題,我們可以考慮 Mock 的對像改為 ClientInterface,讓 Home 及 Board 多做一點事。雖然和 HomeTest 及 BoardTest 的測試有點重覆了,但讓任何修改能反應在測試上這點就非常值得的,所以我們就來思考要如何 mock ClientInterface 吧

首先是 Home 的 ClientInterface,我們從 Home 的程式可以得知

private function parseRows($html)
{
    preg_match_all('/<a\sclass="board"[^>]*>.+?<\/a>/s', $html, $matches);

    return $matches[0];
}

我們的正規表示式是從 來進行分析的,所以我們只需這回傳一部份的 HTML

<a class="board" href="/bbs/Gossiping/index.html">
    <div class="board-name">Gossiping</div>
    <div class="board-nuser"><span class="hl f4">8803</span></div>
    <div class="board-class">綜合</div>
    <div class="board-title">◎[八卦] 亞運李智凱、許皓鋐奪金!</div>
</a>

接著是 Board 的 ClientInterface,一樣可以從 Board 的程式得知

private function parseRows($html)
{
    preg_match_all('/class="r-ent">.+<div class="mark">(.+)<\/div>/sU', $html, $matches);

    return $matches[0];
}

我們的正規表示式是從 來進行分析,所以我們只需這回傳一部份的 HTML

<div class="r-ent">
    <div class="nrec"><span class="hl f2">4</span></div>
    <div class="title">

        <a href="/bbs/Gossiping/M.1696537444.A.1A5.html">[問卦] 司機夫人真的有去卡地亞血拚$1.1M嗎?</a>

    </div>
    <div class="meta">
        <div class="author">uwmtsa</div>
        <div class="article-menu">

            <div class="trigger">⋯</div>
            <div class="dropdown">
                <div class="item"><a href="/bbs/Gossiping/search?q=thread%3A%5B%E5%95%8F%E5%8D%A6%5D+%E5%8F%B8%E6%A9%9F%E5%A4%AB%E4%BA%BA%E7%9C%9F%E7%9A%84%E6%9C%89%E5%8E%BB%E5%8D%A1%E5%9C%B0%E4%BA%9E%E8%A1%80%E6%8B%9A%241.1M%E5%97%8E%3F">搜尋同標題文章</a></div>

                <div class="item"><a href="/bbs/Gossiping/search?q=author%3Auwmtsa">搜尋看板內 uwmtsa 的文章</a></div>

            </div>

        </div>
        <div class="date">10/06</div>
        <div class="mark"></div>
    </div>
</div>

但 Board 還多分頁的功能,所以我們還得再多存分頁的 HTML

<div class="btn-group btn-group-paging">
    <a class="btn wide" href="/bbs/Gossiping/index1.html">最舊</a>
    <!-- 因為不需要再執行分頁所以直接修改為 disabled -->
    <a class="btn wide disabled">‹ 上頁</a>
    <a class="btn wide disabled">下頁 ›</a>
    <a class="btn wide" href="/bbs/Gossiping/index.html">最新</a>
</div>

這些資料都準備好之後,我們可以將 PttCrawlerTest 修改為

<?php

namespace Recca0120\Ithome30\Tests;

use GuzzleHttp\Psr7\Response;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psr\Http\Client\ClientInterface;
use Recca0120\Ithome30\PttCrawler;
use Recca0120\Ithome30\Crawlers\Home;
use Recca0120\Ithome30\Crawlers\Board;

class PttCrawlerTest extends TestCase
{
    public function test_fetch_board_page()
    {
        $crawler = new PttCrawler($this->givenHome(), $this->givenBoard());
        $records = $crawler->all();

        self::assertEquals([
            'board_name' => 'Gossiping',
            'board_class' => '綜合',
            'nrec' => '4',
            'type' => '問卦',
            'title' => '司機夫人真的有去卡地亞血拚$1.1M嗎?',
            'author' => 'uwmtsa',
            'date' => '10/06',
            'url' => 'https://www.ptt.cc/bbs/Gossiping/M.1696537444.A.1A5.html',
        ], $records[0]);
    }

    private function givenBoard()
    {
        /** @var Mockery\Mock|ClientInterface $client */
        $client = Mockery::mock(ClientInterface::class);
        $client->allows('sendRequest')->andReturn(new Response(200, [], '
<div class="btn-group btn-group-paging">
    <a class="btn wide" href="/bbs/Gossiping/index1.html">最舊</a>
    <a class="btn wide disabled">‹ 上頁</a>
    <a class="btn wide disabled">下頁 ›</a>
    <a class="btn wide" href="/bbs/Gossiping/index.html">最新</a>
</div>
<div class="r-ent">
    <div class="nrec"><span class="hl f2">4</span></div>
    <div class="title">

        <a href="/bbs/Gossiping/M.1696537444.A.1A5.html">[問卦] 司機夫人真的有去卡地亞血拚$1.1M嗎?</a>

    </div>
    <div class="meta">
        <div class="author">uwmtsa</div>
        <div class="article-menu">

            <div class="trigger">⋯</div>
            <div class="dropdown">
                <div class="item"><a href="/bbs/Gossiping/search?q=thread%3A%5B%E5%95%8F%E5%8D%A6%5D+%E5%8F%B8%E6%A9%9F%E5%A4%AB%E4%BA%BA%E7%9C%9F%E7%9A%84%E6%9C%89%E5%8E%BB%E5%8D%A1%E5%9C%B0%E4%BA%9E%E8%A1%80%E6%8B%9A%241.1M%E5%97%8E%3F">搜尋同標題文章</a></div>

                <div class="item"><a href="/bbs/Gossiping/search?q=author%3Auwmtsa">搜尋看板內 uwmtsa 的文章</a></div>

            </div>

        </div>
        <div class="date">10/06</div>
        <div class="mark"></div>
    </div>
</div>
        '));

        return new Board($client);
    }

    private function givenHome()
    {
        /** @var Mockery\Mock|ClientInterface $client */
        $client = Mockery::mock(ClientInterface::class);
        $client->allows('sendRequest')->andReturn(new Response(200, [], '
<a class="board" href="/bbs/Gossiping/index.html">
    <div class="board-name">Gossiping</div>
    <div class="board-nuser"><span class="hl f4">8803</span></div>
    <div class="board-class">綜合</div>
    <div class="board-title">◎[八卦] 亞運李智凱、許皓鋐奪金!</div>
</a>
        '));

        return new Home($client);
    }
}

這時候我們來執行一次測試看會得到怎樣的結果

https://ithelp.ithome.com.tw/upload/images/20231011/20065818Ug4Ee6GDLW.png

沒想到竟然亮紅燈了,可見上一個測試是多不穩固,所以再次檢查程式碼發現,Board::fetch 應該要回傳 Generator 才對,所以立刻調整程式碼

<?php
// src/PttCrawler.php

namespace Recca0120\Ithome30;

use Generator;
use Recca0120\Ithome30\Crawlers\Home;
use Recca0120\Ithome30\Crawlers\Board;

class PttCrawler
{
    public function __construct(private Home $home, private Board $board)
    {
    }

    public function all(): Generator
    {
        foreach ($this->home->all() as $board) {
            foreach ($this->board->fetch($board) as $paginator) {
                // 應該回傳 Generator
                yield $paginator;
            }
        }
    }
}
<?php

namespace Recca0120\Ithome30\Tests;

use GuzzleHttp\Psr7\Response;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psr\Http\Client\ClientInterface;
use Recca0120\Ithome30\PttCrawler;
use Recca0120\Ithome30\Crawlers\Home;
use Recca0120\Ithome30\Crawlers\Board;

class PttCrawlerTest extends TestCase
{
    public function test_fetch_board_page()
    {
        $crawler = new PttCrawler($this->givenHome(), $this->givenBoard());
        // generator 操作
        $generator = $crawler->all();
        $paginator = $generator->current();

        self::assertEquals([
            'board_name' => 'Gossiping',
            'board_class' => '綜合',
            'nrec' => '4',
            'type' => '問卦',
            'title' => '司機夫人真的有去卡地亞血拚$1.1M嗎?',
            'author' => 'uwmtsa',
            'date' => '10/06',
            'url' => 'https://www.ptt.cc/bbs/Gossiping/M.1696537444.A.1A5.html',
        ], $paginator[0]);
    }

    private function givenBoard()
    {
        /** @var Mockery\Mock|ClientInterface $client */
        $client = Mockery::mock(ClientInterface::class);
        $client->allows('sendRequest')->andReturn(new Response(200, [], '
<div class="btn-group btn-group-paging">
    <a class="btn wide" href="/bbs/Gossiping/index1.html">最舊</a>
    <a class="btn wide disabled">‹ 上頁</a>
    <a class="btn wide disabled">下頁 ›</a>
    <a class="btn wide" href="/bbs/Gossiping/index.html">最新</a>
</div>
<div class="r-ent">
    <div class="nrec"><span class="hl f2">4</span></div>
    <div class="title">

        <a href="/bbs/Gossiping/M.1696537444.A.1A5.html">[問卦] 司機夫人真的有去卡地亞血拚$1.1M嗎?</a>

    </div>
    <div class="meta">
        <div class="author">uwmtsa</div>
        <div class="article-menu">

            <div class="trigger">⋯</div>
            <div class="dropdown">
                <div class="item"><a href="/bbs/Gossiping/search?q=thread%3A%5B%E5%95%8F%E5%8D%A6%5D+%E5%8F%B8%E6%A9%9F%E5%A4%AB%E4%BA%BA%E7%9C%9F%E7%9A%84%E6%9C%89%E5%8E%BB%E5%8D%A1%E5%9C%B0%E4%BA%9E%E8%A1%80%E6%8B%9A%241.1M%E5%97%8E%3F">搜尋同標題文章</a></div>

                <div class="item"><a href="/bbs/Gossiping/search?q=author%3Auwmtsa">搜尋看板內 uwmtsa 的文章</a></div>

            </div>

        </div>
        <div class="date">10/06</div>
        <div class="mark"></div>
    </div>
</div>
        '));

        return new Board($client);
    }

    private function givenHome()
    {
        /** @var Mockery\Mock|ClientInterface $client */
        $client = Mockery::mock(ClientInterface::class);
        $client->allows('sendRequest')->andReturn(new Response(200, [], '
<a class="board" href="/bbs/Gossiping/index.html">
    <div class="board-name">Gossiping</div>
    <div class="board-nuser"><span class="hl f4">8803</span></div>
    <div class="board-class">綜合</div>
    <div class="board-title">◎[八卦] 亞運李智凱、許皓鋐奪金!</div>
</a>
        '));

        return new Home($client);
    }
}

再次執行測試就可以得到綠燈了


上一篇
Day 25. 兩個物件互動測試 - Mock
下一篇
Day 27. 兩個物件互動測試 - PHPVCR
系列文
開心撰寫 PHPUnit30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言