4
1

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.

PHPUnitでDDD実装パターンをテストしてみた

Posted at

はじめに

CakePHP4でDDDを意識して作成したAPIについて、テストコードを作成しました
こちらの記事の続きになります

https://qiita.com/rowpure/items/f2aab86414ea7dac9c97#

「ドメイン駆動設計 サンプルコード&FAQ | 松岡幸一郎」の本を大変参考にさせていただきました

https://booth.pm/ja/items/3363104

また、この記事で紹介するソースコードは、 GitHub にコミットしてあります

https://github.com/q23isline/reinventing_the_wheel

バージョン

  • PHP
    • 8.0.10
  • CakePHP
    • 4.3.0
  • PHPUnit
    • 9.5.10

テストコード

代表して、「ユーザーを登録する」ユースケースの処理の流れのテストコードを記載します

プレゼンテーション層

tests/TestCase/Controller/Api/V1/UsersControllerTest.php
declare(strict_types=1);

namespace App\Test\TestCase\Controller\Api\V1;

// (略)useの設定

class UsersControllerTest extends TestCase
{
    use IntegrationTestTrait;

    public function setUp(): void
    {
        parent::setUp();

        // CSRF コンポーネントのトークンミスマッチによるテスト失敗を防ぐ
        $this->enableCsrfToken();

        // 管理者でログイン
        $this->session([
            'Auth' => [
                'User' => [
                    'id' => 'dbdc8d4c-cf7d-4833-b755-4b69e97561f3',
                    'role' => 'admin',
                ],
            ],
        ]);

        // JSON形式でアクセス
        $this->configRequest(['headers' => ['Accept' => 'application/json']]);
    }

    public function test_ユーザー追加で追加したユーザーをレスポンスすること(): void
    {
        // Arrange
        $requestData = [
            'loginId' => 'test1018',
            'password' => 'password',
            'roleName' => 'viewer',
            'firstName' => '斉藤',
            'lastName' => '太郎',
            'firstNameKana' => 'サイトウ',
            'lastNameKana' => 'タロウ',
            'mailAddress' => 'saito6@example.com',
            'sex' => '1',
            'birthDay' => '1990-01-01',
            'cellPhoneNumber' => '09011111116',
        ];

        $id = '00676011-5447-4eb1-bde1-001880663af3';
        $mockUserAddUseCase = $this->createMock(UserAddUseCase::class);
        $mockUserAddUseCase->expects($this->once())
            ->method('handle')
            ->will($this->returnValue(new UserId($id)));

        $this->overridePrivatePropertyWithMock('userAddUseCase', $mockUserAddUseCase);

        $expected = ['userId' => $id];
        $expected = json_encode($expected, JSON_PRETTY_PRINT);

        // Act
        $this->post('/api/v1/users', $requestData);

        // Assert
        $this->assertResponseCode(200);
        $this->assertEquals($expected, (string)$this->_response->getBody());
    }

    public function test_ユーザー追加でバリデーションエラーをレスポンスすること(): void
    {
        // Arrange
        $requestData = [];
        $expected = [
            'error' => [
                'message' => 'Bad Request',
                'errors' => [
                    [
                        'field' => 'loginId',
                        'reason' => '必須項目が不足しています。',
                    ],
                    [
                        'field' => 'password',
                        'reason' => '必須項目が不足しています。',
                    ],
                    [
                        'field' => 'roleName',
                        'reason' => '必須項目が不足しています。',
                    ],
                    [
                        'field' => 'firstName',
                        'reason' => '必須項目が不足しています。',
                    ],
                    [
                        'field' => 'lastName',
                        'reason' => '必須項目が不足しています。',
                    ],
                    [
                        'field' => 'firstNameKana',
                        'reason' => '必須項目が不足しています。',
                    ],
                    [
                        'field' => 'lastNameKana',
                        'reason' => '必須項目が不足しています。',
                    ],
                    [
                        'field' => 'mailAddress',
                        'reason' => '必須項目が不足しています。',
                    ],
                    [
                        'field' => 'sex',
                        'reason' => '必須項目が不足しています。',
                    ],
                ],
            ],
        ];

        $expected = json_encode($expected, JSON_PRETTY_PRINT);

        // Act
        $this->post('/api/v1/users', $requestData);

        // Assert
        $this->assertResponseCode(400);
        $this->assertEquals($expected, (string)$this->_response->getBody());
    }

    private function overridePrivatePropertyWithMock(string $propertyName, MockObject $mockObject): void
    {
        EventManager::instance()->on(
            'Controller.initialize',
            function (EventInterface $event) use ($propertyName, $mockObject) {
                $controller = $event->getSubject();
                $property = (new ReflectionClass($controller))->getProperty($propertyName);
                $property->setAccessible(true);
                $property->setValue($controller, $mockObject);
            }
        );
    }
}
  • Controllerのプロパティにモックを持たせる方法は下の記事にあります

https://qiita.com/rowpure/items/3973b45a1a1c1654794c

ユースケース層

tests/TestCase/UseCase/Users/UserAddUseCaseTest.php
declare(strict_types=1);

namespace App\Test\TestCase\UseCase\Users;

// (略)useの設定

class UserAddUseCaseTest extends TestCase
{
    public function test_ユーザーが登録されること(): void
    {
        // Arrange
        $userRepository = new InMemoryUserRepository();
        $userService = new UserService($userRepository);
        $userAddUseCase = new UserAddUseCase($userRepository, $userService);

        $loginId = 'test';
        $inputData = new UserAddCommand(
            loginId: $loginId,
            password: 'p@ssw0rd',
            roleName: 'viewer',
            firstName: 'test1',
            lastName: 'test2',
            firstNameKana: 'テストイチ',
            lastNameKana: 'テストニ',
            mailAddress: 'test@example.com',
            sex: '1',
            birthDay: '1980-01-01',
            cellPhoneNumber: '09012345678',
        );

        // Act
        $userAddUseCase->handle($inputData);

        // Assert
        $createdLoginId = new LoginId($loginId);
        $createdUser = $userRepository->findByLoginId($createdLoginId);
        $this->assertNotNull($createdUser);
    }

    public function test_ユーザーが登録できないこと登録済ログインID(): void
    {
        // Arrange
        $userRepository = new InMemoryUserRepository();
        $userService = new UserService($userRepository);
        $userAddUseCase = new UserAddUseCase($userRepository, $userService);

        $loginId = 'test';
        $user = (new TestUserFactory())->create(loginId: $loginId);
        $userRepository->save($user);
        $inputData = new UserAddCommand(
            loginId: $loginId,
            password: 'p@ssw0rd',
            roleName: 'viewer',
            firstName: 'test1',
            lastName: 'test2',
            firstNameKana: 'テストイチ',
            lastNameKana: 'テストニ',
            mailAddress: 'test@example.com',
            sex: '1',
            birthDay: '1980-01-01',
            cellPhoneNumber: '09012345678',
        );

        // Assert
        $this->expectException(ValidateException::class);

        // Act
        $userAddUseCase->handle($inputData);
    }
}

テスト時に利用するインメモリリポジトリ

データベースへアクセスせず高速にテスト実行させることができます

src/Infrastructure/InMemory/Users/InMemoryUserRepository.php
declare(strict_types=1);

namespace App\Infrastructure\InMemory\Users;

// (略)useの設定

class InMemoryUserRepository implements IUserRepository
{
    public $store = [];

    public function assignId(): UserId
    {
        $uuid = (string)mt_rand(0, 99999999) . '-3882-42dd-9ab2-485e8e579a8e';

        return new UserId($uuid);
    }

    public function findByLoginId(LoginId $loginId): ?User
    {
        foreach ($this->store as $elem) {
            if ($elem->getLoginId()->getValue() === $loginId->getValue()) {
                return $this->clone($elem);
            }
        }

        return null;
    }

    public function save(User $user): UserId
    {
        $this->store[$user->getId()->getValue()] = $this->clone($user);

        return $user->getId();
    }

    private function clone(User $user): User
    {
        $birthDay = null;
        if (!empty($user->getBirthDay())) {
            $birthDay = $user->getBirthDay();
        }

        $cellPhoneNumber = null;
        if (!empty($user->getCellPhoneNumber())) {
            $cellPhoneNumber = $user->getCellPhoneNumber();
        }

        return User::create(
            $user->getId(),
            $user->getLoginId(),
            $user->getPassword(),
            $user->getRoleName(),
            $user->getFirstName(),
            $user->getLastName(),
            $user->getFirstNameKana(),
            $user->getLastNameKana(),
            $user->getMailAddress(),
            $user->getSex(),
            $birthDay,
            $cellPhoneNumber,
        );
    }
}

テスト用インスタンス生成オブジェクト

デフォルト値を設定することで、テスト時に、より強調したいパラメータを表すことができます

tests/TestCase/UseCase/Users/TestUserFactory.php
declare(strict_types=1);

namespace App\Test\TestCase\UseCase\Users;

// (略)useの設定

class TestUserFactory
{
    public function create(
        string $userId = '01509588-3882-42dd-9ab2-485e8e579a8e',
        string $loginId = 'test',
        string $password = 'p@ssw0rd',
        string $roleName = 'viewer',
        string $firstName = 'test1',
        string $lastName = 'test2',
        string $firstNameKana = 'テストイチ',
        string $lastNameKana = 'テストニ',
        string $mailAddress = 'test@example.com',
        string $sex = '1',
        ?string $birthDay = null,
        ?string $cellPhoneNumber = null,
    ): User {
        if (!is_null($birthDay)) {
            $birthDay = new BirthDay($birthDay);
        }

        if (!is_null($cellPhoneNumber)) {
            $cellPhoneNumber = new CellPhoneNumber($cellPhoneNumber);
        }

        return User::create(
            new UserId($userId),
            new LoginId($loginId),
            new Password($password),
            new RoleName($roleName),
            new FirstName($firstName),
            new LastName($lastName),
            new FirstNameKana($firstNameKana),
            new LastNameKana($lastNameKana),
            new MailAddress($mailAddress),
            new Sex($sex),
            $birthDay,
            $cellPhoneNumber,
        );
    }
}

ドメインサービス層

tests/TestCase/Domain/Services/UserServiceTest.php
declare(strict_types=1);

namespace App\Test\TestCase\Domain\Services;

// (略)useの設定

class UserServiceTest extends TestCase
{
    public function test_ユーザーインスタンスが自分自身であること(): void
    {
        // Arrange
        $user = User::create(
            new UserId('01509588-3882-42dd-9ab2-485e8e579a8e'),
            new LoginId('test'),
            new Password('p@ssw0rd'),
            new RoleName('viewer'),
            new FirstName('test1'),
            new LastName('test2'),
            new FirstNameKana('テストイチ'),
            new LastNameKana('テストニ'),
            new MailAddress('test@example.com'),
            new Sex('1'),
            new BirthDay('1980-01-01'),
            new CellPhoneNumber('09012345678'),
        );
        $userOther = $user;

        // Act
        $isMyself = $user->isMyself($userOther);

        // Assert
        $this->assertTrue($isMyself);
    }

    public function test_ユーザーインスタンスが自分自身でないこと(): void
    {
        // Arrange
        $user = User::create(
            new UserId('01509588-3882-42dd-9ab2-485e8e579a8e'),
            new LoginId('test'),
            new Password('p@ssw0rd'),
            new RoleName('viewer'),
            new FirstName('test1'),
            new LastName('test2'),
            new FirstNameKana('テストイチ'),
            new LastNameKana('テストニ'),
            new MailAddress('test@example.com'),
            new Sex('1'),
            new BirthDay('1980-01-01'),
            new CellPhoneNumber('09012345678'),
        );
        $userOther = User::create(
            new UserId('99999999-3882-42dd-9ab2-485e8e579a8e'),
            new LoginId('test1'),
            new Password('p@ssw0rd'),
            new RoleName('viewer'),
            new FirstName('test1'),
            new LastName('test2'),
            new FirstNameKana('テストイチ'),
            new LastNameKana('テストニ'),
            new MailAddress('test1@example.com'),
            new Sex('1'),
            new BirthDay('1980-01-01'),
            new CellPhoneNumber('09012345679'),
        );

        // Act
        $isMyself = $user->isMyself($userOther);

        // Assert
        $this->assertFalse($isMyself);
    }
}

ドメイン層

tests/TestCase/Domain/Models/User/UserTest.php
declare(strict_types=1);

namespace App\Test\TestCase\Domain\Models\User;

// (略)useの設定

class UserTest extends TestCase
{
    public function test_ユーザーインスタンスが作成されること(): void
    {
        // Arrange
        $id = new UserId('01509588-3882-42dd-9ab2-485e8e579a8e');
        $loginId = new LoginId('test');
        $password = new Password('p@ssw0rd');
        $roleName = new RoleName('viewer');
        $firstName = new FirstName('test1');
        $lastName = new LastName('test2');
        $firstNameKana = new FirstNameKana('テストイチ');
        $lastNameKana = new LastNameKana('テストニ');
        $mailAddress = new MailAddress('test@example.com');
        $sex = new Sex('1');
        $birthDay = new BirthDay('1980-01-01');
        $cellPhoneNumber = new CellPhoneNumber('09012345678');

        // Act
        $user = User::create(
            $id,
            $loginId,
            $password,
            $roleName,
            $firstName,
            $lastName,
            $firstNameKana,
            $lastNameKana,
            $mailAddress,
            $sex,
            $birthDay,
            $cellPhoneNumber,
        );

        // Assert
        $this->assertEquals($id, $user->getId());
        $this->assertEquals($loginId, $user->getLoginId());
        $this->assertEquals($password, $user->getPassword());
        $this->assertEquals($roleName, $user->getRoleName());
        $this->assertEquals($firstName, $user->getFirstName());
        $this->assertEquals($lastName, $user->getLastName());
        $this->assertEquals($firstNameKana, $user->getFirstNameKana());
        $this->assertEquals($lastNameKana, $user->getLastNameKana());
        $this->assertEquals($mailAddress, $user->getMailAddress());
        $this->assertEquals($sex, $user->getSex());
        $this->assertEquals($birthDay, $user->getBirthDay());
        $this->assertEquals($cellPhoneNumber, $user->getCellPhoneNumber());
    }
}

インフラ層

tests/TestCase/Infrastructure/CakePHP/Users/CakePHPUserRepositoryTest.php
declare(strict_types=1);

namespace App\Test\TestCase\Infrastructure\CakePHP\Users;

// (略)useの設定

class CakePHPUserRepositoryTest extends TestCase
{
    use IntegrationTestTrait;

    protected $fixtures = ['app.Users'];

    public function test_UUIDを取得すること(): void
    {
        // UUID の正規表現パターン
        $expectUserIdPattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/';

        // Act
        $userId = (new CakePHPUserRepository())->assignId();

        // Assert
        $this->assertMatchesRegularExpression($expectUserIdPattern, $userId->getValue());
    }

    public function test_ログインIDによってユーザー情報を取得すること(): void
    {
        // Arrange
        $loginId = 'admin';

        $expect = User::create(
            new UserId('41559b8b-e831-4972-8afa-21ee8b952d85'),
            new LoginId($loginId),
            new Password('password'),
            new RoleName('admin'),
            new FirstName('admin'),
            new LastName('管理者'),
            new FirstNameKana('アドミン'),
            new LastNameKana('カンリシャ'),
            new MailAddress('admin@example.com'),
            new Sex('1'),
            new BirthDay('2021-10-14'),
            new CellPhoneNumber('09012345678'),
        );

        // Act
        $actual = (new CakePHPUserRepository())->findByLoginId(new LoginId($loginId));

        // Assert
        $this->assertEquals($expect, $actual);
    }

    public function test_ログインIDに一致するユーザー情報が存在しない場合空を返すこと(): void
    {
        // Arrange
        $loginId = 'sample';

        // Act
        $actual = (new CakePHPUserRepository())->findByLoginId(new LoginId($loginId));

        // Assert
        $this->assertNull($actual);
    }

    public function test_ユーザー情報を保存すること(): void
    {
        // Arrange
        $userId = '01509588-3882-42dd-9ab2-485e8e579a8e';
        $user = User::reconstruct(
            new UserId($userId),
            new LoginId('test'),
            new Password('p@ssw0rd1'),
            new RoleName('editor'),
            new FirstName('test2'),
            new LastName('test3'),
            new FirstNameKana('テストニ'),
            new LastNameKana('テストサン'),
            new MailAddress('test1@example.com'),
            new Sex('2'),
            new BirthDay('1980-01-02'),
            new CellPhoneNumber('09012345679'),
        );

        // Act
        $actual = (new CakePHPUserRepository())->save($user);

        // Assert
        $this->assertEquals(new UserId($userId), $actual);
    }
}

おわりに

どこまでの粒度でテストを実施するべきか、悩みます
メンテナンスがとてもしんどそうで。。。
この記事が他のエンジニアの助けになれば幸いです

4
1
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?