はじめに
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);
}
}
おわりに
どこまでの粒度でテストを実施するべきか、悩みます
メンテナンスがとてもしんどそうで。。。
この記事が他のエンジニアの助けになれば幸いです