Edited at

テストコードを書きやすくするためのシステム分割とモック化

More than 1 year has passed since last update.


この文書について


  • テストコード書こうぜ という流れはあるが、システムによって テストコードが書きやすいものと書きにくいものがある

  • テストが書きにくい事例として1つの関数やクラスの役務が多かったり、外界と結合している


    • 大抵の場合、サーバ間通信



  • サブシステム分割することでモック化する余地ができ、テストが書きやすくなる

  • もっと詳しく知りたい場合、 「layered architecture」 や 「dependency injection」で検索


サンプルケース


  • エンタープライズアーキテクチャのAPI


    • REST API 2本を叩き、マッシュアップして200応答を返す


      • http://domain/path/to/api/v1/hoge

      • http://domain/path/to/api/v1/fuga



    • 上記2本のAPIのうち、すべてが正常応答でなければ、このAPIは500応答を返す




悪いケース


ソースコード


define('HOGE_API_BASE_URL', 'http://hoge.api.domain/path/to/api/v1/');
define('FUGA_API_BASE_URL', 'http://fuga.api.domain/path/to/api/v1/');

function mashup ($hogeApiUrl, $fugaApiUrl) {
$hogeResult = @file_get_contents($hogeApiUrl);
$fugaResult = @file_get_contents($fugaApiUrl);
if ( !$hogeResult || !$fugaResult ) {
throw new Exception('Failed to fetch.');
}
$result = [];
foreach ($hogeResult as $aHogeObject) {
// マッシュアップ処理 は 省略
}
return $result;
}

function main() {
header("Content-Type: application/json; charset=utf-8");
try {
$hogeApiUrl = HOGE_API_BASE_URL . 'hoge';
$fugaApiUrl = FUGA_API_BASE_URL . 'fuga';
$mashupResult = mashup($hogeApiUrl, $fugaApiUrl);
http_response_code(200);
echo json_encode($mashupResult);
} catch (Exception $e) {
http_response_code(500);
echo json_encode((object)null);
}
}

main();


何が悪いのか?


  • mashupの関数が複数の役割を持っている


    • 各APIと通信すること

    • 通信結果がすべて正常化チェックすること

    • 通信結果をマッシュアップすること



  • このままではテスト時にhttpサーバが必須


    • localhostでAPIを立てる or 開発環境サーバを使う


      • 開発環境サーバを使う場合、データをいちいち変えるのが非効率






改善案


コード

メインロジック

define('HOGE_API_BASE_URL', 'http://hoge.api.domain/path/to/api/v1/');

define('FUGA_API_BASE_URL', 'http://fuga.api.domain/path/to/api/v1/');

function main() {
header("Content-Type: application/json; charset=utf-8");
try {
$fetcherHoge = new Fetcher(HOGE_API_BASE_URL);
$fetcherFuga = new Fetcher(FUGA_API_BASE_URL);
$masup = new Mashup($fetcherHoge, $fetcherFuga);
$result = $masup->execute();
http_response_code(200);
echo json_encode($result);
} catch (Exception $e) {
http_response_code(500);
echo json_encode((object)null);
}
}
main();

通信相当のクラス

class Fetcher {

protected $baseUrl;
function __construct ($baseUrl) {
$this->baseUrl = $baseUrl;
}
protected function createUrl($resource, array $queries) {
$httpBuildQueryResult = http_build_query($queries);
$queryString = '';
if (empty($httpBuildQueryResult)) {
$queryString = '?' . $httpBuildQueryResult;
}
return $this->baseUrl . $resource . $queryString;
}
public function fetch($resource, array $queries = []) {
$url = $this->createUrl($resource, $queries);
return @file_get_contents($url);
}
}

マッシュアップ担当のクラス

class Mashup {

protected $hogeFetcher;
protected $fugaFetcher;
function __construct ($hogeFetcher, $fugaFetcher) {
$this->hogeFetcher = $hogeFetcher;
$this->fugaFetcher = $fugaFetcher;
}

public function execute() {
$hogeResult = $this->hogeFetcher->fetch('hoge');
$fugaResult = $this->fugaFetcher->fetch('fuga');
if ( !$hogeResult || !$fugaResult) {
throw new Exception('Failed to fetch.');
}
$result = [];
foreach ($hogeResult as $aHogeObject) {
// マッシュアップ処理 は 省略
}
return $result;
}
}


  • 役割を分けた


    • 通信処理をFetcherクラスが担当する

    • マッシュアップ処理をMashupクラスが担当する



  • 分けたことのメリット


    • Fecherを適宜Mockに差し替えることで
      実働するサーバがなくてもテストが書ける




テストの例

use PHPUnit\Framework\TestCase;

class MashupTest extends TestCase
{
public function createFetcherStub()
{
return $this->getMockBuilder(Fetcher::class)
->disableOriginalConstructor()
->disableOriginalClone()
->disableArgumentCloning()
->getMock();
}

/**
* @expectedException Exception
*/
public function testExecuteWhenFugaReturnsFalse()
{
$stubHoge = $this->createFetcherStub();
$stubHoge->method('fetch')->willReturn([]);
$stubFuga = $this->createFetcherStub();
$stubFuga->method('fetch')->willReturn(false);

$mashup = new Mashup($stubHoge, $stubFuga);
$mashup->execute();
}
}


最後に


  • 結合テストで全部頑張らず、分割してモック化

  • 一般的なフレームワークの流儀に従えば、自然とクラスの役務が分割される


    • DBアクセス等は直接PDOを叩かずに、Repository経由になっている



  • 深い話は「layered architecture」 や 「dependency injection」で検索