-
単純すぎるドメイン駆動設計Javaサンプルコード
- 単純すぎるドメイン駆動設計Javaサンプルコード (1) DDDらしいコード
- 単純すぎるドメイン駆動設計Javaサンプルコード (2) 仕様追加その1
- 単純すぎるドメイン駆動設計Javaサンプルコード (3) 仕様追加その2
- 単純すぎるドメイン駆動設計Javaサンプルコード (4) テスト <- ここ
いまさらですが、ここでテストコードも見てみましょう。
DDDの場合
ドメイン層
ドメイン層のテストは比較的単純に書くことができます。
AdditionElement
class AdditionElementTest {
@Test
@DisplayName("要素から値を取得できる")
void testPositiveValue() {
AdditionElement target = new AdditionElement(1);
assertEquals(1, target.getValue());
}
@Test
@DisplayName("要素として0を扱える")
void testZero() {
AdditionElement target = new AdditionElement(0);
assertEquals(0, target.getValue());
}
@Test
@DisplayName("要素として負の数は扱えない")
void testNegativeValue() {
assertThrows(IllegalArgumentException.class, () -> new AdditionElement(-1));
}
@Test
@DisplayName("要素どうしを足すと、足し算結果の値を持つ要素を返す")
void plus() {
AdditionElement element1 = new AdditionElement(1);
AdditionElement element2 = new AdditionElement(2);
AdditionElement expected = new AdditionElement(1 + 2);
assertEquals(expected, element1.plus(element2));
}
}
AdditionFormula
class AdditionFormulaTest {
AdditionFormula target;
@BeforeEach
void setUp() {
target = new AdditionFormula(
new AdditionElement(1),
new AdditionElement(2),
new AdditionElement(3)
);
}
@AfterEach
void tearDown() {
target = null;
}
@Test
@DisplayName("1つ目の要素の値を取得できる")
void getElement1Value() {
assertEquals(1, target.getElement1Value());
}
@Test
@DisplayName("2つ目の要素の値を取得できる")
void getElement2Value() {
assertEquals(2, target.getElement2Value());
}
@Test
@DisplayName("足し算結果の値を取得できる")
void getResultValue() {
assertEquals(3, target.getResultValue());
}
}
User
class UserTest {
User user;
@BeforeEach
void setUp() {
user = new User("hoge");
}
@AfterEach
void tearDown() {
user = null;
}
@Test
@DisplayName("ユーザは名前で識別できる。名前が同じなら同じユーザとみなす")
void testEquals1() {
User sameUser = new User("hoge");
assertEquals(sameUser, user);
}
@Test
@DisplayName("ユーザは名前で識別できる。名前が異なるなら別のユーザとみなす")
void testEquals2() {
User anotherUser = new User("fuga");
assertNotEquals(anotherUser, user);
}
@Test
@DisplayName("ユーザから名前を取得できる")
void getName() {
assertEquals("hoge", user.getName());
}
@Test
@DisplayName("ユーザは複数の足し算の履歴を保持する")
void testHistory() {
AdditionElement e11 = new AdditionElement(1);
AdditionElement e21 = new AdditionElement(2);
AdditionElement result1 = new AdditionElement(3);
user.addHistory(e11, e21, result1);
AdditionElement e12 = new AdditionElement(3);
AdditionElement e22 = new AdditionElement(4);
AdditionElement result2 = new AdditionElement(7);
user.addHistory(e12, e22, result2);
List<AdditionFormula> expected = new ArrayList<>(Arrays.asList(
new AdditionFormula(e11, e21, result1),
new AdditionFormula(e12, e22, result2)
));
assertEquals(expected, user.getHistory());
}
}
ユースケース層
ユースケース層では、UserRepositoryにモックを使用することにより、実際のファイル入出力を伴わずにテスト可能としています。
class AdditionServiceTest {
UserRepository mockUserRepository;
AdditionService service;
@BeforeEach
void setUp() {
mockUserRepository = mock(UserRepository.class);
service = new AdditionService(mockUserRepository);
}
@AfterEach
void tearDown() {
service = null;
mockUserRepository = null;
}
@Test
@DisplayName("足し算サービスを実行すると、足し算の結果を返却する")
void execute() {
try {
when(mockUserRepository.find(anyString())).thenReturn(new User("hoge"));
assertEquals(3, service.execute("hoge", 1, 2));
} catch (AdditionServiceException | UserRepositoryException e) {
fail();
}
}
@Test
@DisplayName("足し算サービスを実行すると、足し算の履歴を持つユーザを保存する")
void executeSuccess() {
try {
when(mockUserRepository.find(anyString())).thenReturn(new User("hoge"));
service.execute("hoge",1,2);
User expectedUser = new User("hoge");
expectedUser.addHistory(
new AdditionElement(1),
new AdditionElement(2),
new AdditionElement(3)
);
verify(mockUserRepository).find("hoge");
verify(mockUserRepository).save(expectedUser);
} catch (AdditionServiceException | UserRepositoryException e) {
fail();
}
}
@Test
@DisplayName("パラメータが不正だと、足し算サービスが失敗する")
void executeFailure1() throws UserRepositoryException {
when(mockUserRepository.find(anyString())).thenReturn(new User("hoge"));
assertThrows(AdditionServiceException.class, () -> {
service.execute("hoge",1, -2);
});
}
@Test
@DisplayName("ユーザの保存が失敗すると、足し算サービスが失敗する")
void executeFailure2() throws UserRepositoryException {
when(mockUserRepository.find(anyString())).thenReturn(new User("hoge"));
doThrow(new UserRepositoryException(new IOException()))
.when(mockUserRepository).save(any());
assertThrows(AdditionServiceException.class, () -> {
service.execute("hoge",1,2);
});
}
}
UI層・インフラストラクチャ層
UI層とインフラストラクチャ層についてはユニットテストで対応するよりも実際にアプリを動作させてテストした方が効果的かつ効率的だと思います。
詳細なロジックはドメイン層とユースケース層のユニットテストでほぼ網羅できると思われるため、テストケースは下記観点でそれぞれ1ケースずつぐらいでよいかと思います。
- 正常系
- アプリを起動して足し算を複数回実行し、標準出力された足し算の結果と、ファイル出力された内容が期待どおりであることを確認する。
- 終了コマンドを入力し、アプリが正常終了することを確認する。
- アプリを再度起動し、同じユーザ名で足し算実行すると、同じファイルに履歴が追記されることを確認する。
- 異常系
- 引数の数が過不足の場合に、アプリが異常終了することを確認する。
- 2つ目および3つ目の引数に数値以外の値を入力し、アプリが異常終了することを確認する。
※ファイル出力まわりの異常系は実際にIOExceptionを発生させるのは難しいと思われるので、FileWriter/FileReaderをモックに差し替えて動かすようなユニットテストを用意した方がよいかもしれませんが、ここでは割愛します。
手続き型設計の場合
正直どのようにテストするのがよいか困っています。DDDの場合よりも後回しにしたのはそのせいです。
1つのクラス、1つのメソッドにまとまってしまっているので、部分ごとにユニットテストをすることができません。
一部はprivateメソッドに分かれていますが、privateなのでクラスの外から見たら分かれていないのと同じです。部分ごとにテストできるように処理ごとに細かくprivateメソッドに切り出したとしても同様です。
※privateメソッドのテストについては議論があるようですが、ググったらすぐに以下の記事が見つかったので、参考までに貼っておきます。
となると、テストコードによるユニットテスト自動化は諦めて、実際にアプリを動作させてテストすることになりそうですが、その場合、前述のDDDの場合のUI層、インフラストラクチャ層のテスト観点に加えて、足し算のロジック部分まで網羅的にテストする必要があります。自動化も不可ではないと思われますが、ツールを用意するのに一手間かかりそうです。申し訳ありませんが、ちょっと考えただけで心が折れてしまったので、ここまでとさせてください。
実際の開発では、だからと言ってテストしないというのはありえないと思いますが、excelテストケース表を用意して手動テストすることになるのもありがちかと思います。手動テストが面倒なので改修もなるべくしたくなくなりますね。
このあたりは、DDD以前に、オブジェクト指向設計を行うことによってテストを容易にできるようにしましょうという話かと思います。