【Java&Kotlin】実務で使えるJUnit5テストコード

テストを書いていると「あれ、どう実装すればいいんだっけ?」と手が止まってしまうことはありませんか?

これ記事では、そんなエンジニアのみなさまが「ここさえ見ればやりたいことを実現できる」を目指して情報をまとめてみました。

実業務でテストを書きながら「実装コスト」や「可読性」を追い求めた個人的ベストプラクティスなコードなのでぜひ使ってみてください!

前提

  • 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

各テストケースごとに実行したい処理がある場合に使います。

メモ
共通的なMockの定義の実装に便利です!
    @BeforeEach
    void beforeEach() {
        System.out.println("各テストケース実行前の処理");
    }

    @AfterEach
    void afterEach() {
        System.out.println("各テストケース実行後の処理");
    }
    @BeforeEach
    fun beforeEach() {
        // 各テストケース実行前の処理
    }

    @AfterEach
    fun afterEach() {
        // 各テストケース実行後の処理
    }

@BeforeAll / @AfterAll

テストクラス全体の前後で実行したい処理がある場合に使います。

メモ
Javaでいうstaticメソッドで関数を定義する必要があります。
    @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クラスに引き継がれます!
    @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を使用します。

注意
suspend関数のテスト実装時に、テストメソッドをsuspend関数で定義しないこと!!!
テストがスキップ扱いになるので最悪動かなくても気づけないです。
@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アプリケーションのテストを書いているとテストが遅くなりがちなので、以下の記事もぜひご覧ください!

【35分→4分!】SpringBootアプリケーションのテスト高速化

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です