テストを書いていると「あれ、どう実装すればいいんだっけ?」と手が止まってしまうことはありませんか?
これ記事では、そんなエンジニアのみなさまが「ここさえ見ればやりたいことを実現できる」を目指して情報をまとめてみました。
実業務でテストを書きながら「実装コスト」や「可読性」を追い求めた個人的ベストプラクティスなコードなのでぜひ使ってみてください!
前提
- SpringBoot利用
- ビルドツールはGradle
※Mavenでもコード部分は参考になればと…!
テスト対象サンプルコード
名前を受け取ってあいさつをするコードです。
@RequiredArgsConstructor
@Component
public class ParentComponent {
private final ChildComponent childComponent;
public String greeting(String name) {
return childComponent.greeting(name);
}
}
@Component
public class ChildComponent {
public String greeting(String name) {
return "Hello, " + name + ".";
}
}
@Component
class ParentComponent(private val childComponent: ChildComponent) {
fun greeting(name: String): String {
return childComponent.greeting(name)
}
suspend fun greetingSuspend(name: String): String {
return childComponent.greetingSuspend(name)
}
}
@Component
class ChildComponent {
fun greeting(name: String): String {
return "Hello, ${name}."
}
suspend fun greetingSuspend(name: String): String {
return "Hello, ${name}."
}
}
基本形
dependencies {
...
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
// @InjectMocksや@Mockを動作させるために必要
@ExtendWith(MockitoExtension.class)
class ParentComponentTest {
// テスト対象のクラス。「@InjectMocks」を付けると自動で依存クラスを注入してくれる
@InjectMocks
ParentComponent parentComponent;
// テスト対象クラスの依存クラス。「@Mock」を付けるとMockオブジェクトを生成してくれる
@Mock
ChildComponent childComponent;
@Test
void test() {
// 依存Mockの動作を定義
when(childComponent.greeting(any())).thenReturn("Hello");
var actual = parentComponent.greeting("hirabay");
var expected = "Hello";
// 戻り値を検証
assertThat(actual).isEqualTo(expected);
// 依存オブジェクトの呼び出され方を検証
verify(childComponent).greeting(any());
// 以下コードと同じ意味。timesで呼び出し回数を指定できます。
// verify(childComponent, times(1)).greeting(any());
}
}
dependencies {
...
testImplementation("io.mockk:mockk:1.13.3")
}
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import io.mockk.verify
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
// @InjectMockKsや@MockKを動作させるために必要
@ExtendWith(MockKExtension::class)
internal class ParentComponentTest {
// テスト対象のクラス。「@InjectMockKs」を付けると自動で依存クラスを注入してくれる
@InjectMockKs
lateinit var parentComponent: ParentComponent
// テスト対象クラスの依存クラス。「@MockK」を付けるとMockオブジェクトを生成してくれる
@MockK
lateinit var childComponent: ChildComponent
@Test
fun test() {
// 依存Mockの動作を定義
every {
childComponent.greeting(any())
} returns "Dummy"
val actual = parentComponent.greeting("hirabay")
val expected = "Dummy"
assertThat(actual).isEqualTo(expected)
// 呼出され方の検証
verify {
childComponent.greeting(any())
}
// 呼出され方の検証(exactlyで呼び出し回数まで検証できる)
verify(exactly = 1) {
childComponent.greeting(any())
}
}
}
@BeforeEach / @AfterEach
各テストケースごとに実行したい処理がある場合に使います。
@BeforeEach
void beforeEach() {
System.out.println("各テストケース実行前の処理");
}
@AfterEach
void afterEach() {
System.out.println("各テストケース実行後の処理");
}
@BeforeEach
fun beforeEach() {
// 各テストケース実行前の処理
}
@AfterEach
fun afterEach() {
// 各テストケース実行後の処理
}
@BeforeAll / @AfterAll
テストクラス全体の前後で実行したい処理がある場合に使います。
@BeforeAll
static void beforeAll() {
System.out.println("テスト実行前の処理");
}
@AfterAll
static void afterAll() {
System.out.println("テスト実行後の処理");
}
companion object {
@BeforeAll
@JvmStatic
fun beforeAll() {
println("テスト実行前の処理")
}
@AfterAll
@JvmStatic
fun afterAll() {
println("テスト実行後の処理")
}
}
@Nested
テストクラス内でテストケースをグルーピングしたい場合に使用します。
僕の場合は、親クラスをテスト対象のクラスごとに、
@Nestedなクラスをテスト対象のメソッドごとに定義することが多いです。
@Nested
class GreetingTest { // メソッド名+「Test」
@Test
void test() {
// テスト
}
}
@Nested
inner class GreetingTest { // メソッド名+「Test」
@Test
fun test() {
// テスト
}
}
@ParameterizedTest
テスト関数に引数を渡すことで、1メソッドで複数のテストケースを実行できてしまう優れものです!
とりあえず@ParameterizedTestでテストをまとめられないか検討してもいいレベルに便利です!
パラメータの指定方法に「@ValueSource」「@CsvSource」「@MethodSource」等があり、内容がFatになってしまうため別記事でまとめていますのでリンク先をご覧ください!
@MockBean / @MockkBean
SpringBootTestを実行するときは、オブジェクトをMock化するだけではなく、Bean登録もしなければならず@Mock / @MockKではうまく動きません。
代わりに@MockBeanや@MockkBeanを使用します。
使うアノテーションが変わるだけで使い方は同じです!
@SpringBootTest(classes = CheatsheetApplication.class)
public class SpringBootTestSample {
@MockBean
private ChildComponent childComponent;
@Test
void test() {
when(childComponent.greeting(any())).thenReturn("dummy");
// テスト
}
dependencies {
...
testImplementation("com.ninja-squad:springmockk:3.1.2")
}
@SpringBootTest
internal class SpringBootTestSample {
@MockkBean
lateinit var childComponent: ChildComponent
@Test
fun test() {
every {
childComponent.greeting(any())
} returns "dummy"
// テスト
}
}
コルーチン(※Kotlinのみ)
Kotlinのコルーチンを使用している場合、suspend関数を定義していると思います。
その場合はevery
, verify
の代わりにcoEvery
, coVerify
を使用します。
テストがスキップ扱いになるので最悪動かなくても気づけないです。
@Test
fun test() {
coEvery {
childComponent.greetingSuspend(any())
} returns "dummy"
val actual = runBlocking {
parentComponent.greetingSuspend("hirabay")
}
val expected = "dummy"
assertThat(actual).isEqualTo(expected)
coVerify {
childComponent.greetingSuspend(any())
}
}
staticメソッド
staticメソッドのMock化にはmockStatic
/ mockkStatic
を使用します。
dependencies {
// mockito-inlineを依存に追加する
testImplementation 'org.mockito:mockito-inline'
}
@Test
void test() {
var mockedValue = LocalDate.of(2022, 3, 1);
// Mock化の準備。close()の必要があるので try with resource文で
try (MockedStatic<LocalDate> mockedLocalDate = mockStatic(LocalDate.class)) {
// staticメソッドのMock化
mockedLocalDate.when(() -> LocalDate.now()).thenReturn(mockedValue);
// staticメソッドの呼び出し
var actual = LocalDate.now();
// 戻り値の検証
assertThat(actual.toString()).isEqualTo("2022-03-01");
// staticメソッドの呼び出され方の検証
mockedLocalDate.verify(() -> LocalDate.now());
}
}
@Test
fun staticMethod() {
val mockedValue = LocalDate.of(2020, 8, 11)
// staticメソッドをMock化
mockkStatic(LocalDate::class)
every {
LocalDate.now()
} returns mockedValue
val actual = LocalDate.now()
assertThat(actual).isEqualTo(mockedValue)
// Mock化を解除
unmockkStatic(LocalDate::class)
}
コンストラクタ
コンストラクタで生成されるオブジェクトをMock化するには
dependencies {
// mockito-inlineを依存に追加する
testImplementation 'org.mockito:mockito-inline'
}
@Test
void constructor() {
// Mock化の準備。close()の必要があるので try with resource文で
try (MockedConstruction<Random> mockedRandom = mockConstruction(Random.class)) {
// Mock化されたインスタンスが生成される
var random = new Random();
// Mock動作を指定
// 注意:コンストラクタの呼び出し後に実装すること!
mockedRandom.constructed().forEach(constructed -> {
when(constructed.nextInt()).thenReturn(1);
});
assertThat(random.nextInt()).isEqualTo(1);
assertThat(random.nextInt()).isEqualTo(1);
assertThat(random.nextInt()).isEqualTo(1);
// Mock呼び出され方を検証
mockedRandom.constructed().forEach(constructed -> {
verify(constructed, times(3)).nextInt();
});
}
}
@Test
fun constructor() {
mockkConstructor(ChildComponent::class)
// Mock動作を指定
// 注意:コンストラクタの呼び出し前に実装すること!
every {
anyConstructed<ChildComponent>().greeting(any())
} returns "dummy"
val childComponent = ChildComponent()
val actual = childComponent.greeting("hirabay")
assertThat(actual).isEqualTo("dummy")
verify {
anyConstructed<ChildComponent>().greeting(any())
}
}
あとがき
JUnit5のテストの書き方を紹介しました!
ここで紹介したような実装がスムーズにできるようになったら、次はテストの速さにも気を配ってみるといいかもしれません!
特にSpringBootアプリケーションのテストを書いているとテストが遅くなりがちなので、以下の記事もぜひご覧ください!