1
/
5

いかにLaravelで外部依存しているサービスを作ったか

アルバイトエンジニアの妹尾です。今回は弊社で開発しているWorkinGoodでいかに外部依存しているシステムを開発したか、簡単なサンプルのコードも交えて話をします。

WorkinGoodは派遣スタッフの方が自分で勤怠を入力したり、給与明細を確認することができるサービスです。

技術的には以下のもので構成されています。

  • Laravel
  • AWS(色々)
  • TypeScript
  • React + flux

見た感じモダンな雰囲気がありますが、このシステムには一つ解決しなければならない問題があります。

レガシーシステムとの連携

それは弊社で開発している別のシステムをバックエンドにした構成にしなくてはなりませんでした。

このMGというのがレガシーシステムなのですが、もともとAPIを用意していたわけではなく、WorkinGoodの開発と同時に新たにAPIをつくる必要があります。しかもこのレガシーシステムはWindows上でしか動かないので以前の記事で紹介したDockerで環境を用意することもできません。

DockerでLaravel開発環境を整える by Takuma Seno | MatchinGoodエンジニアアルバイトブログ
このブログ初投稿となります、MatchinGoodでエンジニアアルバイトをしている妹尾です。 弊社では人材紹介会社様と人材派遣会社様向けのシステムの開発をしており、今年初めごろに派遣会社スタッフ様向けの新サービスである WorkinGood を始めました。 主にPHPを使用して開発していますが、新しいサービスに関してはAWSやDocker、Reactなどトレンドとなっているものに追随して技術を選択しています。 そこで今回は弊社の開発環境構築にDockerを採用した話をしたいと思います。 弊社ではプロダクシ
https://www.wantedly.com/companies/matchingood/post_articles/36964

このような状況では以下のような問題が発生します。

  • 開発時に依存しているシステムに繋げないといけない
  • CIでのユニットテストの時に依存しているシステムのバージョンがあわない

開発時に依存しているシステムに繋げる時は弊社が所有しているサーバーに開発用のバックエンドを立ち上げて開発を行うことで解決しました。しかし、CI時にバックエンドにつなげようとするとバージョンが合わなかったり、繰り返し使うことによってデータがユニットテストに合わなくなってきてしまいます。

どう解決したか

まず、バックエンドに依存する部分をサービス層として独立させました。全てのバックエンドへのリクエストはこのサービス層を通します。


こうすることでDIを利用してサービス層をモックすることができます。実際にこのアイディアの簡単なコードの例を見て見ましょう。

class AssignmentService
{
protected $requestBuilder;

public function __construct($requestBuilder)
{
$this->requestBuilder = $requestBuilder;
}

public function get($since, $until)
{
$url = "http://backend.com/assignments?since={$since}&until={$until}";

// リクエストを投げる
$response = $this->requestBuilder
->setUrl($url)
->sendRequest();
$result = $response->getResult();

// あとはリクエスト結果をモデルに変換する
.
.
.
     return $assignments
}
}

これはAssignmentというリソースをバックエンドからとってくるサービス層の架空の例です。コード中に出てくる$requestBuilderというオブジェクトが実際にHTTPリクエストを行なっています。この$requestBuilderをモックオブジェクトに差し替えることで実際の通信自体をモックすることができます。

ユニットテスト

このままだとコードを書いても実装が正しいのかどうかわかりません。そこでユニットテストを並行して書きます。

class AssignmentServiceTest extends TestCase
{
    protected $mock;
    protected $result = // 想定されるリクエストの返り値

    public function setUp()
    {
        parent::setUp();
        $mock = $this->getMockBuilder('RequestBuilder')
            ->setMethods([
                'setUrl',
                'sendRequest'
            ])
            ->getMock();
        $mock->method('setUrl')
            ->will($this->returnSelf());
        $this->mock = $mock;
    }

    public function testGet()
    {
        $this->mock->method('sendRequest')
            ->willReturn($this->result);
        $service = new AssignmentService($this->mock);
        $assignments = $service->get('2016-09-01', '2016-09-30');
        // あとはこの$assignmentsが想定通りかどうかチェックする
    }
}

こうすることで依存しているサービスの開発状況によらずに開発を進めることができます。

フロントエンドAPIの開発

WorkinGoodではUIをReactで構築しているのでLaravel側にクライアントサイド向けのAPIを用意する必要があります。これをここではバックエンド側のAPIと区別するためにフロントエンドAPIと呼びます。サービス層を実際に使っている部分でもあります。

サービス層を呼んでいるのでこのAPIを開発するときも結果を確認するためにユニットテストを並行して書くことで開発を進めます。

class AssignmentsController extends Controller
{
    function get(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'since' => 'required|date_format:"Y-m-d"',
            'until' => 'required|date_format:"Y-m-d"',
        ]);
        if ($validator->fails()) {
            return response()->json(['status' => 'failed', 'errors' => $validator->errors()], 422);
        }
        $since = $request->since;
        $until = $request->until;
        $service = new AssignmentService(new RequestBuilder);
        $assignments = $service->get($since, $until);
        // あとはレスポンスの形式にして返す。
    }
}

フロントエンドAPIを簡単にこう書いた場合、コントローラのユニットテストはこのように書くことができます。なお、簡単にするためにログイン処理などは省きます。

class AssignmentsControllerTest extends TestCase
{
    protected $mock;
    protected $result;

    public function setUp()
    {
        parent::setUp();
        $this->mock = Mockery::mock('overload:App\Service\AssignmentService');
    }


    public function testGet()
    {
        $this->result = // サービス層が返す期待するモデル
        $this->mock
            ->shouldReceive('get')
            ->andReturn($this->result);
        $this->visit('/assignments?since=2016-09-01&until=2016-09-30')
            ->seeJson([
                // 期待するAPIの返り値
            ]);
    }
}

このMockeryというのはPHPライブラリでユニットテスト時にモックすることを可能にしてくれます。overloadというものを使うと呼び出す関数が内部的に呼び出しているクラス自体をモックできるという優れものです。

この実装とユニットテストを並行して書き進めることでバックエンドの進捗によらずスムーズに開発することができています。

現状の問題点

このサービスでは積極的にMockeryを使っていますが、ドキュメントを見ると

Prefixing the valid name of a class (which is NOT currently loaded) with
“overload:” will generate an alias mock (as with “alias:”) except that 
created new instances of that class will import any expectations set on 
the origin mock ($mock). 

とあります。"which is NOT currently loaded"という部分が実は厄介で、他の部分ですでにモック対象のクラスが読み込まれているとモックすることができません。これはこのユニットテストのファイル一つだけをテストする場合は問題ないのですが、CIなどで全てのユニットテストを行う時に引っかかってしまいます。同じプロセスですでに読み込まれている場合はモックできないので

/**
 * @runInSeparateProcess
 * @preserveGlobalState disabled
 */
public function testGet()

のようにアノテーションをつける必要があります。こうするとユニットテストの実行時間が結構伸びてしまうので現在改善案を考えています。

まとめ

Laravelに寄せた話でしたが、アイディア自体はどのような環境でも適応できる話だったと思います。外部依存するサービスがあると同じPC上に環境を構築できなかったりするのでこのようなスタイルで開発することがとても有効な場合があると思います。

これらは全てアルバイトエンジニアが設計して実装してきました。これはまだまだ一例にすぎず、やる気があればもっと高度な技術的なチャレンジをやりきることもできます。プログラミング初心者の方も実際のコードを通じて得られるものがたくさんあると思います。

ぜひ一度お話を聞きにきてください!

マッチングッド株式会社's job postings
6 Likes
6 Likes

Weekly ranking

Show other rankings
Like Takuma Seno's Story
Let Takuma Seno's company know you're interested in their content