在上一篇我們是 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);
}
}
這時候我們來執行一次測試看會得到怎樣的結果
沒想到竟然亮紅燈了,可見上一個測試是多不穩固,所以再次檢查程式碼發現,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);
}
}
再次執行測試就可以得到綠燈了