5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

「ドメイン駆動設計入門」のLaravelによる実装例:Chapter 5-データにまつわる処理を分離する「リポジトリ」

Last updated at Posted at 2023-06-18

本プロジェクトでは、成瀬允宣氏著の「ドメイン駆動設計入門」のLaravel 10を用いた実装例を示します。

当該書籍では、ドメイン駆動設計(Domain Driven Deplopment, DDD)を用いたWEBアプリケーションの開発例が示されており、素晴らしい著作であることは言うまでもありません。
しかしながら実装はC#で示されているため
、例えばPHPフレームワークの代表例であるLaravelなどでDDDによる実装を行いたい場合には、具体的な実装例が分かりづらくなってしまいます。

そこで本プロジェクトでは、「ドメイン駆動設計入門」のLaravelによる実装例を示すことで、Laravel開発者のDDD実装の手助けとなることを目指します。あくまでLaravelの実装例を示すことを目的としていますので、ドメイン駆動設計とは何かについては書籍をお読みください。

これが皆様の開発の手助けとなれば幸いです。ご意見等ございましたら、コメント欄やIssueでお伝えいただければと思います。

目次

Githubにソースコードはアップ済みです。

Chapter 5: データにまつわる処理を分離する「リポジトリ」

Docker環境で開発

あらかじめ、srcフォルダをVisual Studio Codeで開き、Command PaletteのDev Containers: Reopen in ContainerでDocker環境に入っておきます。

リポジトリの作成はLaravelに必要か?

ドメイン駆動設計入門」ではリポジトリを作成しています。
リポジトリの責務は「データの永続化と再構築」で、データベースの操作などを行います。
これらの操作を抽象化したクラスをリポジトリとして用意することで、例えばMySQLからSQLiteへの変更などが生じた際のソフトウェア全体でのコード変更を最小限にとどめることができるようになり、ソフトウェアに柔軟性をもたらします。

ところで、Laravelでアプリケーションを構築する際にはリポジトリクラスを作成する必要があるのでしょうか?
このような疑問が生じる背景には、LaravelがEloquent ORMを採用しているためにデータベース操作が既に抽象化されているという事実があります。
Laravelでは、すでにEloquent ORMによってデータベース操作が抽象化されており、例えば開発中でMySQLからSQLiteへの変更など、データベースの変更があったとしてもコードの変更は不要です。
したがって、リポジトリの作成は不要でしょう。
同じような意見はこちらでも指摘されています。

テスト用のデータベースを作成する

本説では、テスト用のデータベースを用意し、テストを行なっていきます。

テストの実行方法

あらかじめ、web.phpの中身を元に戻しておきます(Exampleのテストで/へのroutingがテストされるため)。

routes/web.php
Route::get('/', function () {
    return view('welcome');
});

データベースファイルなどを変更している場合には、以下でマイグレーションを行なっておきます。

php artisan test --migrate-configuration

以下で、もともと用意されているtests/Feature/ExampleTest.phptests/Unit/ExampleTest.phpが実行されます。

php artisan test
実行結果
PASS  Tests\Unit\ExampleTest
✓ that true is true                                                                         0.02s  

PASS  Tests\Feature\ExampleTest
✓ the application returns a successful response                                             0.57s  

Tests:    2 passed (2 assertions)
Duration: 0.82s

SQLiteを使用したテスト環境のセットアップ

ドメイン駆動設計入門」で示されているように、インメモリデータベースであるSQLiteを使用してテストを行うための設定を行なっていきましょう。

まず、phpunit.xmlを開き、<env name="DB_DATABASE" value=":memory:"/><env name="DB_CONNECTION" value="sqlite"/>を追加します。
phpunit.xmlに作成された環境変数(<env name="foo" value="bar"/>等)は、テスト実行時に読み込まれます
ここで、DB_DATABASEの値を:memory:とすることによって、データベース用のファイルを作成せずにインメモリのデータベースでテストを動作させることができます。

phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
  <testsuites>
    <testsuite name="Unit">
      <directory suffix="Test.php">./tests/Unit</directory>
    </testsuite>
    <testsuite name="Feature">
      <directory suffix="Test.php">./tests/Feature</directory>
    </testsuite>
  </testsuites>
  <coverage/>
  <php>
    <env name="APP_ENV" value="testing"/>
    <env name="BCRYPT_ROUNDS" value="4"/>
    <env name="CACHE_DRIVER" value="array"/>
    <env name="DB_DATABASE" value=":memory:"/>
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="MAIL_MAILER" value="array"/>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="TELESCOPE_ENABLED" value="false"/>
  </php>
  <source>
    <include>
      <directory suffix=".php">./app</directory>
    </include>
  </source>
</phpunit>

UserName 値オブジェクトのテスト

Chapter 2で作成したUserName 値オブジェクトの動作確認を簡易なもので済ませてしまっていたので、書籍に記載はありませんが、ここでしっかりとテストを作成しておきましょう。

以下でテストを作成します。

php artisan make:test UserNameTest

ここでは、テストをfeature testとして作成しています。
プロジェクトでデフォルトで作成されているように、Laravelのテストフォルダはtests/Unittests/Featureの2つが用意されています。
make:test-uオプションをつけると、tests/Unitフォルダの方にテストが作成されますが、特に指定しない場合はtests/Featureフォルダの方に作成されます。
一般的に、ユニットテストでは1つのクラスのみをテスト対象とする場合に使用し、フィーチャーテストは複数のクラスが使用される複雑なテストを対象とする場合に使用します。
ここで、値オブジェクトは他のクラスやデータベースには依存しないと判断して、ユニットテストを作成すると問題が生じます。
Laravelのテストでは、Facade(Laravelの機能群)を使用する場合はfeature test、しない場合はunit testで使い分けます。
デフォルトではunit testでFacadeが使われない設定になっているため、Facadeを使用するか否かでテストディレクトリを分けた方がいいでしょう。
今回の値オブジェクトはFacadeを使用しているので、フィーチャーテストとして作成します。

作成されたtests/Feature/UserNameTest.phpを以下のように編集します。

tests/Feature/UserNameTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

use App\ValueObjects\UserName;

use Exception;

class UserNameTest extends TestCase
{
    public function test_empty_input(): void
    {
        $this->expectException(Exception::class);
        new UserName('');
    }

    public function shortNames(): array
    {
        return [
            ['a'],
            ['ab']
        ];
    }

    /**
     * @dataProvider shortNames
     */
    public function test_short_name(string $name): void
    {
        $this->expectException(Exception::class);
        new UserName($name);
    }

    public function longNames(): array
    {
        return [
            ['UE69KHLM3Q8BzwM6ZDAYa'], // 21 letters
            ['UE69KHLM3Q8BzwM6ZDAYad'], // 22 letters
        ];
    }

    /**
     * @dataProvider longNames
     */
    public function test_long_name(string $name): void
    {
        $this->expectException(Exception::class);
        new UserName($name);
    }

    public function validNames(): array
    {
        return [
            ['ReiRev'],
            ['abc'], // 3 letters
            ['eFaNtc7hZMRXn8Ud7y2g'], // 20 letters
            ['れいれぶ']
        ];
    }

    /**
     * @dataProvider validNames
     */
    public function test_valid_name(string $name): void
    {
        $username = new UserName($name);
        $this->assertEquals($name, $username->toString());
        $this->assertEquals($name, $username->value());
        $this->assertEquals(['username' => $name], $username->toArray());
    }
}

いくつかピックアップして内容を見ていきましょう。
テストを実行する関数は、publicな関数にし、名前はtest_*とするようにします。
基本的には$this->assertEqualsを使用して、ある値が所望の値かどうかをチェックします(test_valid_name関数など)。
もし、エラーが生じるかどうかを確かめたい場合には、エラーが生じる箇所の前に$this->expectException(Exception::class);を書いておけば良いです。
Exceptionのインポートは忘れずにしておきましょう。

また、複数の入力パターンに対してテストを実行したい場合には、PHP Unit Testのdata providerを使用することができます。
まず、複数の入力パターンをshortNamesのように関数の形で定義し、それを入力として受けるテストの関数(ここではtest_short_name)に、@dataProvider shortNamesのようなアノテーションをつけることで、複数の入力に対するテストを記述することができます。

php artisan testで、テストが全てパスすることを確認しましょう。

ユーザー作成処理のテスト

書籍リスト5.17のユーザー作成処理のテストを作成してみましょう。

以下のコマンドでUserTest.phpを作成します。

php artisan make:test UserTest

作成されたtests/Feature/UserTest.phpを以下のように編集します。
書籍に加えて、必要そうなテストを追加で作成しています。

tests/Feature/UserTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

use App\Models\User;
use App\ValueObjects\UserName;

use Exception;

class UserTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_create(): void
    {
        $username = new UserName('ReiRev');
        $user = User::create(['username' => $username]);

        $head = User::first();

        $this->assertEquals($user['username'], 'ReiRev');
        $this->assertEquals($head['username'], 'ReiRev');
    }

    public function test_duplicate_user_create(): void
    {
        $username = new UserName('ReiRev');
        $user = User::create(['username' => $username]);
        $this->expectException(Exception::class);
        $user = User::create(['username' => $username]);
    }
}


use RefreshDatabase;をクラス内に記述しておくことで、各テストごとに、すなわち各テストの関数の実行ごとにデータベースがリセットされます

以下でテストを実行し、問題なくテストが動作することを確認します。

php artisan make:test

参考文献

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?