【Java】given-when-then構文でのテストコードの書き方
Given-When-Then構文とは?
Given-When-Then構文は、テストの文脈を3つのフェーズに明確に分割して記述する書き方です。
テストコードをこの3つに整理して書くことで、「前提」「操作」「検証」の責任領域が分離され、誰が見ても一目で何を行っているかが理解できるようになります。
Given(前提条件)
テストを実行するための事前準備を行うフェーズです。
テスト対象のメソッドに渡す引数の準備や、モックでの振る舞いの定義を行います。
When(実行・操作)
テスト対象のメソッドを実際に呼び出すフェーズです。
基本的にテスト対象のメソッドを呼び出すだけなので1行になります。
※実行回数の検証や呼び出し順序など、複数行実行が必要なパターンもあります
Then(検証)
実行結果が期待通りであるかを確認するフェーズです。
戻り値の検証や例外の有無、モックのメソッドが正しい回数呼び出されていたかの検証等を行います。
テストコード例
実際のJavaコード(JUnit 5 + Mockito)を使って、Given-When-Then構文の書き方を見ていきましょう。
モックを使用しないシンプルなテスト
依存関係のない純粋なドメインロジックのテスト例です。
class UserNameTest {
@Test
void 文字列を渡すと生成できること() {
// given
String name = "test_name";
// when
var result = new UserName(name);
// then
assertEquals(name, result.value());
}
}
Mockitoを使ってモックを使用するテスト
外部のデータベースなどをモック化して行うサービス層のテスト例です。
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@InjectMocks
private UserService userService;
@Mock
private UserRepository userRepository;
@Test
void addUserが実行されること() {
// given
UserId userId = new UserId("test");
when(userRepository.existsByUserId(any())).thenReturn(Exists.NOT_EXISTS);
// when
userService.addUser(userId);
// then
verify(userRepository).addUser(any(), any());
}
@Test
void 試行回数3回目で追加できること() {
// given
UserId userId = new UserId(UUID.randomUUID().toString());
when(userRepository.existsByPlayerId(any()))
.thenReturn(Exists.EXISTS)
.thenReturn(Exists.EXISTS)
.thenReturn(Exists.NOT_EXISTS);
// when
userService.addUser(userId);
// then
verify(userRepository).addUser(any(), any());
}
@Test
void 試行回数3回で作成できない場合エラーになること() {
// given
UserId userId = new UserId(UUID.randomUUID().toString());
when(userRepository.existsByPlayerId(any()))
.thenReturn(Exists.EXISTS)
.thenReturn(Exists.EXISTS)
.thenReturn(Exists.EXISTS);
// when
Exception result = assertThrows(DomainException.class, () -> userService.addUser(userId));
// then
assertEquals("プレイヤーIDの生成に失敗", result.getMessage());
}
}
assertThrowsの書き方
whenの時点で例外が発生するため、whenとthenが混ざりやすいです。
目的によりますが、エラーメッセージまで検証したい場合は上記例のようにwhenとthenを分ける書き方、
// when
Exception result = assertThrows(DomainException.class, () -> userService.addUser(userId));
// then
assertEquals("プレイヤーIDの生成に失敗", result.getMessage());
例外が発生したかだけでよければwhenとthenを一緒に書くこともあります。
// when then
assertThrows(DomainException.class, () -> userService.addUser(userId));
検証的にはどちらでも問題ないと思うので、そこはプロジェクトで合わせてあげればよいと思います。
見やすいように整理する
テストコードは別途管理されている仕様書とは違い、実装コードとほぼずれのない最新の仕様書です。
後から見たときに、
どのような前提条件で(given)、何をしたときに(when)、どうなるべきか(then)
が見やすく整理されていることが一番大事です。
もちろん単体試験的な検証も重要ですが、問題発生時の調査や仕様の確認、非エンジニアでも確認できるように出来ていると、とても価値のある資産になりますね。
まとめ
テストコードは見やすいのが一番です。
自分独自の書き方より、定番な書き方を徹底的に重視したほうが、みんなに読みやすいコード及び仕様書になりますね。
以上、ここまで見ていただきありがとうございます。
皆さまの快適な開発ライフに、ほんの少しでもお役に立てれば幸いです。