【Spring Boot】Spring Boot Testの基本

Java

こんにちは、かつコーチです。

DIやJPAを使ってCRUDまで作れるようになると、次にぶつかる壁が「テストの書き方が分からない」という悩みです。
筆者もJava基礎編で単体テストの書き方は学んだものの、Spring Bootのアプリケーションをどうテストすればいいのか、最初はかなり迷いました。

コントローラやリポジトリはDIコンテナ(Beanを管理する仕組み)に依存しているため、普通の@Testだけでは動かないメソッドが多いのです。
この記事では、spring-boot-starter-testが提供するテストの基本と、@SpringBootTestの使い方を解説します。

Spring Boot Testとは?

spring-boot-starter-testに含まれるもの

Spring InitializrでプロジェクトAを作ると、標準でspring-boot-starter-testが依存関係に含まれています。
このスターターには、以下のライブラリがまとめて入っています。

ライブラリ役割
JUnit 5テストの実行フレームワーク
Mockitoモックオブジェクトの作成
AssertJ読みやすいアサーション(assertThat)を書くためのライブラリ
Spring TestSpring Bootアプリケーション専用のテストサポート

つまり、追加の設定をしなくてもbuild.gradleまたはpom.xmlに1行あるだけで、テストに必要な道具は一通り揃います。

なぜ@Testだけでは足りないのか

JUnit 5の@Testだけでテストクラスを書くと、フィールドに@AutowiredでBeanを注入しようとしてもDIコンテナが起動していないため、NullPointerExceptionになります。

public class ArticleServiceTest {

    @Autowired
    private ArticleService articleService; // ここがnullのまま

    @Test
    void findAll_returnsArticles() {
        // articleServiceがnullなのでNullPointerExceptionが発生する
        articleService.findAll();
    }
}

Spring Bootのテストでは、DIコンテナを起動する専用のアノテーション(@SpringBootTestなど)をクラスに付けることで、初めて@Autowiredが機能するようになります。
この「テストの種類ごとに、どこまでDIコンテナを起動するか」を使い分けるのが、Spring Bootテストの基本方針です。

基本の書き方・実装手順

@SpringBootTestでアプリケーション全体を起動する

もっともシンプルなのが@SpringBootTestです。
アプリケーション全体のDIコンテナを起動し、実際の設定に近い状態でテストできます。

package com.example.blog;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class BlogApplicationTests {

    @Test
    void contextLoads() {
        // アプリケーションコンテキストが正常に起動することを確認するだけの最小テスト
        assertThat(true).isTrue();
    }
}

contextLoads()という中身のないテストに見えますが、これは「設定ミスでBeanの生成に失敗していないか」を確認する重要な役割を持っています。
筆者は実案件で、application.propertiesのDB接続情報を1文字間違えたまま気づかず、contextLoadsが落ちて初めてミスに気づいたことがあります。

サービス層をDIしてテストする

@SpringBootTestを付けたクラスでは、他のBeanと同じように@Autowiredでサービスを注入できます。

package com.example.blog.service;

import com.example.blog.entity.Article;
import com.example.blog.repository.ArticleRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Transactional // テスト後にDBの変更を自動でロールバックする
class ArticleServiceTest {

    @Autowired
    private ArticleService articleService;

    @Autowired
    private ArticleRepository articleRepository;

    @Test
    void save_persistsArticle() {
        Article article = new Article();
        article.setTitle("テスト記事");
        article.setBody("本文です");

        Article saved = articleService.save(article);

        assertThat(saved.getId()).isNotNull();
        assertThat(articleRepository.findById(saved.getId())).isPresent();
    }
}

@Transactionalをテストクラスやテストメソッドに付けると、各テストの終了後に自動でロールバックされます。
これにより、テストで作成したデータが他のテストに影響を与える心配がなくなります。

webEnvironmentでHTTPサーバーの起動有無を切り替える

@SpringBootTestwebEnvironment属性で挙動を変えられます。
実際にHTTPサーバーを起動してエンドポイントを叩きたい場合は、以下のように指定します。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ArticleControllerIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void getArticles_returns200() {
        var response = restTemplate.getForEntity("/api/articles", String.class);
        assertThat(response.getStatusCode().value()).isEqualTo(200);
    }
}

RANDOM_PORTを指定すると、実際に空いているポートでサーバーが起動し、TestRestTemplateを使って本物のHTTPリクエストを送れます。
逆に、DBアクセスだけ確認したい場合はサーバー起動が不要なので、webEnvironmentを省略した方がテストが高速になります。

よくあるつまずきポイント・エラー対処

❌ Before:すべてのテストで@SpringBootTestを使ってしまう

筆者が最初にテストを書き始めた頃、「とりあえず全部@SpringBootTestを付ければ動く」と考えて、コントローラのテストにもリポジトリのテストにも同じアノテーションを使っていました。

// コントローラのテストなのに、アプリ全体を起動してしまっている
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ArticleControllerTest {
    // ...
}

テストの数が20〜30個を超えたあたりから、テストスイート全体の実行に1分以上かかるようになり、開発のたびにテストを待つのがストレスになりました。
@SpringBootTestはDIコンテナ全体(DB接続やセキュリティ設定を含む)を毎回起動するため、テストが増えるほど実行時間が線形に伸びていきます。

✅ After:テスト対象に応じてスライステストを使い分ける

Spring Bootには、コントローラだけ・リポジトリだけを対象に、必要な部分のBeanだけを起動する「スライステスト」用のアノテーションが用意されています。

アノテーション起動する範囲
@SpringBootTestアプリケーション全体(統合テスト向け)
@WebMvcTestコントローラ層のみ
@DataJpaTestJPA関連のみ(Repository)
@JsonTestJSONのシリアライズ/デシリアライズのみ

コントローラのテストは@WebMvcTest、リポジトリのテストは@DataJpaTestに切り替えたところ、テストスイート全体の実行時間が3分の1程度に短縮できました。
それぞれの具体的な使い方は、次回以降の記事で詳しく解説します。

応用・一歩先の使い方

@ActiveProfilesでテスト専用の設定を読み込む

テスト実行時だけ別の設定(テスト用DBなど)を読み込みたい場合は、@ActiveProfilesでプロファイルを切り替えられます。

@SpringBootTest
@ActiveProfiles("test")
class ArticleServiceTest {
    // application-test.propertiesの設定が読み込まれる
}

src/test/resources/application-test.propertiesを用意しておけば、本番用の設定を汚さずにテスト専用のDB接続情報などを定義できます。
プロファイルの切り替えについては、デプロイ・インフラ連携カテゴリの記事でさらに詳しく扱います。

@MockBeanでDIコンテナ内のBeanを差し替える

外部APIを呼び出すサービスなど、テストで本物を動かしたくないBeanは@MockBeanで差し替えられます。

@SpringBootTest
class NotificationServiceTest {

    @MockBean
    private MailClient mailClient; // 本物の代わりにモックがDIされる

    @Autowired
    private NotificationService notificationService;

    @Test
    void notify_callsMailClientOnce() {
        notificationService.notify("test@example.com", "件名");
        verify(mailClient, times(1)).send(any());
    }
}

@MockBeanはDIコンテナに登録されているBeanをMockitoのモックに置き換えてくれるアノテーションです。
外部サービスに依存するテストでも、ネットワークに接続せず安定してテストを実行できるようになります。

まとめ

この記事のポイント

  • spring-boot-starter-testにはJUnit 5・Mockito・AssertJ・Spring Testがまとめて含まれている
  • @SpringBootTestはDIコンテナ全体を起動し、実際の設定に近い状態でテストできる
  • webEnvironmentでHTTPサーバーを起動するかどうかを切り替えられる
  • @Transactionalを付けるとテスト後にDBの変更が自動でロールバックされる
  • テストが増えてきたら、対象範囲を絞った「スライステスト」への切り替えを検討する

次に読むべき記事

コントローラのテストをもっと軽量に書きたい方は、次の「@WebMvcTestでコントローラのテストを書く」で詳しく解説しています。

タグ: Spring Boot, 中級者向け, テスト

タイトルとURLをコピーしました