Skip to content
Published on

Java テスト完全ガイド:JUnit5・Mockito・WireMock実践マスター

Authors

はじめに

Javaのテストエコシステムは非常に充実しています。JUnit5による単体テスト、Mockitoによるモック、WireMockによるHTTPスタブ、TestContainersによる統合テストを組み合わせることで、信頼性の高いテストスイートを構築できます。本ガイドでは各ツールの実践的な使い方をゼロから解説します。


1. JUnit5 アノテーション

基本アノテーション

JUnit5(Jupiter)では豊富なアノテーションが用意されています。

import org.junit.jupiter.api.*;
import static org.assertj.core.api.Assertions.*;

class CalculatorTest {

    private Calculator calculator;

    @BeforeAll
    static void initAll() {
        System.out.println("全テスト開始前に一度だけ実行");
    }

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @AfterEach
    void tearDown() {
        System.out.println("各テスト後に実行");
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("全テスト終了後に一度だけ実行");
    }

    @Test
    @DisplayName("2つの正の数を足す")
    void addTwoPositiveNumbers() {
        int result = calculator.add(2, 3);
        assertThat(result).isEqualTo(5);
    }

    @Test
    @Disabled("実装待ち")
    void skippedTest() {
        // このテストはスキップされる
    }
}

@ParameterizedTest

同じロジックを複数の入力でテストする際に使います。

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

class ParameterizedExamples {

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 5, 8})
    void positiveNumbers(int number) {
        assertThat(number).isPositive();
    }

    @ParameterizedTest
    @CsvSource({
        "2, 3, 5",
        "10, 20, 30",
        "-1, 1, 0"
    })
    void additionTest(int a, int b, int expected) {
        Calculator calc = new Calculator();
        assertThat(calc.add(a, b)).isEqualTo(expected);
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/test-data.csv", numLinesToSkip = 1)
    void csvFileTest(int a, int b, int expected) {
        assertThat(new Calculator().add(a, b)).isEqualTo(expected);
    }

    @ParameterizedTest
    @EnumSource(DayOfWeek.class)
    void enumTest(DayOfWeek day) {
        assertThat(day).isNotNull();
    }

    @ParameterizedTest
    @MethodSource("provideStrings")
    void methodSourceTest(String input, int expectedLength) {
        assertThat(input).hasSize(expectedLength);
    }

    static Stream<Arguments> provideStrings() {
        return Stream.of(
            Arguments.of("hello", 5),
            Arguments.of("world", 5),
            Arguments.of("java", 4)
        );
    }
}

@ExtendWith と @Nested

@ExtendWith(MockitoExtension.class)
class ServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Nested
    @DisplayName("ユーザー検索")
    class FindUser {

        @Test
        @DisplayName("存在するIDで検索する")
        void findByExistingId() {
            when(userRepository.findById(1L))
                .thenReturn(Optional.of(new User(1L, "Alice")));

            User user = userService.findById(1L);
            assertThat(user.getName()).isEqualTo("Alice");
        }

        @Test
        @DisplayName("存在しないIDで例外が発生する")
        void findByNonExistingId() {
            when(userRepository.findById(99L))
                .thenReturn(Optional.empty());

            assertThatThrownBy(() -> userService.findById(99L))
                .isInstanceOf(UserNotFoundException.class)
                .hasMessageContaining("99");
        }
    }

    @Nested
    @DisplayName("ユーザー作成")
    class CreateUser {

        @Test
        @DisplayName("有効なデータでユーザーを作成する")
        void createWithValidData() {
            User newUser = new User(null, "Bob");
            User savedUser = new User(2L, "Bob");
            when(userRepository.save(newUser)).thenReturn(savedUser);

            User result = userService.create(newUser);
            assertThat(result.getId()).isEqualTo(2L);
        }
    }
}
クイズ1: @BeforeEach と @BeforeAll の違いは?

答え: @BeforeEach は各テストメソッドの前に毎回実行され、@BeforeAll はテストクラス全体で一度だけ(最初に)実行されます。

解説: @BeforeAll のメソッドは static である必要があります(インスタンスではなくクラスに紐づくため)。@BeforeEach は各テストごとにフレッシュな状態を保証したい場合(例: オブジェクトの再生成)に使います。@BeforeAll は重いリソース(DB接続、サーバー起動)の初期化に適しています。


2. Mockito: Mock・Spy・Captor

@Mock と @InjectMocks

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentGateway paymentGateway;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private OrderService orderService;

    @Test
    void processOrder_success() {
        Order order = new Order(1L, 100.0, "user@example.com");
        when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
        when(paymentGateway.charge(order.getAmount())).thenReturn(true);
        when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0));

        Order result = orderService.processOrder(1L);

        assertThat(result.getStatus()).isEqualTo(OrderStatus.COMPLETED);
        verify(emailService).sendConfirmation(eq("user@example.com"), any());
    }

    @Test
    void processOrder_paymentFails_throwsException() {
        Order order = new Order(1L, 100.0, "user@example.com");
        when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
        when(paymentGateway.charge(anyDouble())).thenReturn(false);

        assertThatThrownBy(() -> orderService.processOrder(1L))
            .isInstanceOf(PaymentFailedException.class);

        verify(emailService, never()).sendConfirmation(anyString(), any());
    }
}

@Spy

@Spy は実際のオブジェクトをラップし、特定のメソッドだけをスタブできます。

@ExtendWith(MockitoExtension.class)
class ListSpyTest {

    @Spy
    private List<String> spyList = new ArrayList<>();

    @Test
    void spyUsesRealMethodsByDefault() {
        spyList.add("hello");
        spyList.add("world");

        // 実際のメソッドが呼ばれる
        assertThat(spyList).hasSize(2);

        // 特定のメソッドだけスタブ
        doReturn(100).when(spyList).size();
        assertThat(spyList.size()).isEqualTo(100);

        // 実際のデータはそのまま
        assertThat(spyList.get(0)).isEqualTo("hello");
    }
}

@Captor と ArgumentCaptor

@ExtendWith(MockitoExtension.class)
class EmailServiceTest {

    @Mock
    private EmailClient emailClient;

    @Captor
    private ArgumentCaptor<EmailMessage> emailCaptor;

    @InjectMocks
    private NotificationService notificationService;

    @Test
    void sendWelcomeEmail_capturesCorrectMessage() {
        notificationService.sendWelcome("Alice", "alice@example.com");

        verify(emailClient).send(emailCaptor.capture());

        EmailMessage captured = emailCaptor.getValue();
        assertThat(captured.getTo()).isEqualTo("alice@example.com");
        assertThat(captured.getSubject()).contains("ようこそ");
        assertThat(captured.getBody()).contains("Alice");
    }

    @Test
    void sendBulkEmails_capturesAllMessages() {
        List<String> recipients = List.of("a@x.com", "b@x.com", "c@x.com");
        notificationService.sendBulk(recipients, "お知らせ");

        verify(emailClient, times(3)).send(emailCaptor.capture());

        List<EmailMessage> allMessages = emailCaptor.getAllValues();
        assertThat(allMessages).hasSize(3);
        assertThat(allMessages).extracting(EmailMessage::getTo)
            .containsExactlyInAnyOrderElementsOf(recipients);
    }
}

when/thenReturn のバリエーション

// 複数回呼ばれた場合に異なる値を返す
when(repository.findNext())
    .thenReturn("first")
    .thenReturn("second")
    .thenThrow(new NoSuchElementException());

// 引数に基づいて動的にレスポンスを返す
when(userRepository.findById(anyLong()))
    .thenAnswer(invocation -> {
        Long id = invocation.getArgument(0);
        return Optional.of(new User(id, "User" + id));
    });

// void メソッドのスタブ
doNothing().when(emailService).send(any());
doThrow(new RuntimeException("SMTP error")).when(emailService).send(any());
doAnswer(invocation -> {
    System.out.println("メール送信: " + invocation.getArgument(0));
    return null;
}).when(emailService).send(any());
クイズ2: @Mock と @Spy の使い分けは?

答え: @Mock は完全なモックオブジェクトを生成し、すべてのメソッドはデフォルトで何もしない(null やデフォルト値を返す)。@Spy は実際のオブジェクトをラップし、スタブしていないメソッドは実際の実装が呼ばれます。

解説: @Mock はすべての依存関係をコントロールしたい場合に使います。@Spy は既存クラスの一部のメソッドだけをオーバーライドしてテストしたい場合(例: 一部の外部呼び出しだけをモックしたい)に有効です。@SpythenReturn ではなく doReturn を使う必要がある点に注意が必要です(実際のメソッドが先に呼ばれるのを防ぐため)。


3. WireMock: HTTP スタブサーバー

基本セットアップ

<!-- pom.xml -->
<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8</artifactId>
    <version>2.35.0</version>
    <scope>test</scope>
</dependency>

JUnit5 での使用

import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import com.github.tomakehurst.wiremock.client.WireMock;

@WireMockTest(httpPort = 8089)
class ExternalApiClientTest {

    private ExternalApiClient client;

    @BeforeEach
    void setUp() {
        client = new ExternalApiClient("http://localhost:8089");
    }

    @Test
    void getUser_returnsUserFromApi() {
        stubFor(get(urlEqualTo("/api/users/1"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {
                        "id": 1,
                        "name": "Alice",
                        "email": "alice@example.com"
                    }
                    """)));

        User user = client.getUser(1L);

        assertThat(user.getName()).isEqualTo("Alice");
        assertThat(user.getEmail()).isEqualTo("alice@example.com");

        verify(getRequestedFor(urlEqualTo("/api/users/1"))
            .withHeader("Accept", equalTo("application/json")));
    }

    @Test
    void getUser_serverError_throwsException() {
        stubFor(get(urlPathMatching("/api/users/.*"))
            .willReturn(aResponse()
                .withStatus(500)
                .withBody("Internal Server Error")));

        assertThatThrownBy(() -> client.getUser(1L))
            .isInstanceOf(ApiException.class)
            .hasMessageContaining("500");
    }
}

リクエストマッチング

// クエリパラメータマッチング
stubFor(get(urlPathEqualTo("/api/search"))
    .withQueryParam("q", containing("java"))
    .withQueryParam("page", equalTo("1"))
    .willReturn(okJson("{\"results\": []}")));

// リクエストボディマッチング
stubFor(post(urlEqualTo("/api/orders"))
    .withRequestBody(matchingJsonPath("$.amount", greaterThan(0)))
    .withRequestBody(matchingJsonPath("$.currency", equalTo("JPY")))
    .willReturn(aResponse()
        .withStatus(201)
        .withHeader("Location", "/api/orders/123")));

// ヘッダーマッチング
stubFor(get(urlEqualTo("/api/secure"))
    .withHeader("Authorization", matching("Bearer .+"))
    .willReturn(okJson("{\"data\": \"secret\"}")));

// Basic 認証
stubFor(get(urlEqualTo("/api/basic"))
    .withBasicAuth("user", "password")
    .willReturn(ok()));

レスポンステンプレートと遅延

// 遅延レスポンス
stubFor(get(urlEqualTo("/api/slow"))
    .willReturn(aResponse()
        .withStatus(200)
        .withFixedDelay(2000)  // 2秒遅延
        .withBody("Slow response")));

// ランダム遅延
stubFor(get(urlEqualTo("/api/random-delay"))
    .willReturn(aResponse()
        .withStatus(200)
        .withUniformRandomDelay(500, 2000)));

// シナリオを使ったステートフルスタブ
stubFor(get(urlEqualTo("/api/status"))
    .inScenario("Order Processing")
    .whenScenarioStateIs(Scenario.STARTED)
    .willReturn(okJson("{\"status\": \"PENDING\"}"))
    .willSetStateTo("Processing"));

stubFor(get(urlEqualTo("/api/status"))
    .inScenario("Order Processing")
    .whenScenarioStateIs("Processing")
    .willReturn(okJson("{\"status\": \"COMPLETED\"}"))
    .willSetStateTo(Scenario.STARTED));
クイズ3: WireMock の stubFor と verify の役割は?

答え: stubFor は HTTP リクエストに対するレスポンスを事前に定義します。verify はテスト実行後に特定のリクエストが期待通りに送られたかを検証します。

解説: stubFor はテストの「Arrange(準備)」フェーズで使い、モックサーバーの振る舞いを設定します。verify は「Assert(検証)」フェーズで使い、実際にどのようなリクエストが送信されたかを確認します。例えば verify(postRequestedFor(urlEqualTo("/api/orders")).withRequestBody(containing("amount"))) のように使います。


4. TestContainers: コンテナを使った統合テスト

PostgreSQL テスト

@Testcontainers
@SpringBootTest
class UserRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void saveAndFindUser() {
        User user = new User(null, "Alice", "alice@example.com");
        User saved = userRepository.save(user);

        assertThat(saved.getId()).isNotNull();

        Optional<User> found = userRepository.findById(saved.getId());
        assertThat(found).isPresent();
        assertThat(found.get().getEmail()).isEqualTo("alice@example.com");
    }
}

Redis と Kafka のテスト

@Testcontainers
class CacheAndMessagingTest {

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
        .withExposedPorts(6379);

    @Container
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.4.0"));

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }

    @Autowired
    private CacheService cacheService;

    @Test
    void cacheStoresAndRetrievesValue() {
        cacheService.put("key1", "value1");
        String retrieved = cacheService.get("key1");
        assertThat(retrieved).isEqualTo("value1");
    }

    @Test
    void kafkaProducesAndConsumesMessage() throws Exception {
        kafkaProducer.send("test-topic", "Hello Kafka");
        await().atMost(10, SECONDS).untilAsserted(() -> {
            assertThat(testConsumer.getMessages()).contains("Hello Kafka");
        });
    }
}

5. Spring Boot テスト

@SpringBootTest

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

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void healthEndpointReturnsOk() {
        ResponseEntity<String> response = restTemplate.getForEntity("/actuator/health", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

@WebMvcTest と MockMvc

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void getUser_returnsUserJson() throws Exception {
        when(userService.findById(1L))
            .thenReturn(new User(1L, "Alice", "alice@example.com"));

        mockMvc.perform(get("/api/users/1")
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.name").value("Alice"))
            .andExpect(jsonPath("$.email").value("alice@example.com"));
    }

    @Test
    void createUser_validInput_returns201() throws Exception {
        User input = new User(null, "Bob", "bob@example.com");
        User saved = new User(2L, "Bob", "bob@example.com");
        when(userService.create(any(User.class))).thenReturn(saved);

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(input)))
            .andExpect(status().isCreated())
            .andExpect(header().string("Location", containsString("/api/users/2")));
    }

    @Test
    void createUser_invalidEmail_returns400() throws Exception {
        String invalidJson = "{\"name\": \"Bob\", \"email\": \"not-an-email\"}";

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(invalidJson))
            .andExpect(status().isBadRequest());
    }
}

@DataJpaTest

@DataJpaTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void findByCategory_returnsMatchingProducts() {
        entityManager.persist(new Product(null, "Java入門", "BOOK", 3000));
        entityManager.persist(new Product(null, "Spring解説", "BOOK", 4000));
        entityManager.persist(new Product(null, "キーボード", "ELECTRONICS", 15000));
        entityManager.flush();

        List<Product> books = productRepository.findByCategory("BOOK");

        assertThat(books).hasSize(2);
        assertThat(books).extracting(Product::getName)
            .containsExactlyInAnyOrder("Java入門", "Spring解説");
    }
}
クイズ4: @WebMvcTest と @SpringBootTest の違いは?

答え: @WebMvcTest は Web レイヤー(コントローラー)のみをロードし、サービスやリポジトリはモックします。@SpringBootTest はアプリケーション全体のコンテキストをロードします。

解説: @WebMvcTest はコントローラーのルーティング・バリデーション・シリアライゼーションをテストするのに最適で、起動が速いです。@SpringBootTest は全コンポーネントの統合テストに使いますが、起動に時間がかかります。コントローラーのロジックだけをテストしたい場合は @WebMvcTest、エンドツーエンドのフローをテストしたい場合は @SpringBootTest を選びます。


6. TDD ワークフロー: Red → Green → Refactor

TDD の基本サイクル

// ステップ1: Red - 失敗するテストを書く
class ShoppingCartTest {

    @Test
    void emptyCart_totalIsZero() {
        ShoppingCart cart = new ShoppingCart();
        assertThat(cart.getTotal()).isEqualTo(0.0);
    }

    @Test
    void addItem_increasesTotal() {
        ShoppingCart cart = new ShoppingCart();
        cart.addItem(new Item("Book", 1500.0));
        assertThat(cart.getTotal()).isEqualTo(1500.0);
    }

    @Test
    void addMultipleItems_sumIsCorrect() {
        ShoppingCart cart = new ShoppingCart();
        cart.addItem(new Item("Book", 1500.0));
        cart.addItem(new Item("Pen", 200.0));
        assertThat(cart.getTotal()).isEqualTo(1700.0);
    }

    @Test
    void applyDiscount_reducesTotal() {
        ShoppingCart cart = new ShoppingCart();
        cart.addItem(new Item("Book", 1500.0));
        cart.applyDiscount(10);  // 10%割引
        assertThat(cart.getTotal()).isEqualTo(1350.0);
    }
}

// ステップ2: Green - テストを通過する最小限の実装
public class ShoppingCart {
    private List<Item> items = new ArrayList<>();
    private double discountPercent = 0;

    public void addItem(Item item) {
        items.add(item);
    }

    public void applyDiscount(double percent) {
        this.discountPercent = percent;
    }

    public double getTotal() {
        double subtotal = items.stream()
            .mapToDouble(Item::getPrice)
            .sum();
        return subtotal * (1 - discountPercent / 100);
    }
}

// ステップ3: Refactor - コードを改善する
public class ShoppingCart {
    private final List<Item> items = new ArrayList<>();
    private double discountPercent = 0;

    public void addItem(Item item) {
        Objects.requireNonNull(item, "Item must not be null");
        items.add(item);
    }

    public void applyDiscount(double percent) {
        if (percent < 0 || percent > 100) {
            throw new IllegalArgumentException("Discount must be between 0 and 100");
        }
        this.discountPercent = percent;
    }

    public double getTotal() {
        double subtotal = calculateSubtotal();
        return applyDiscountTo(subtotal);
    }

    private double calculateSubtotal() {
        return items.stream().mapToDouble(Item::getPrice).sum();
    }

    private double applyDiscountTo(double amount) {
        return amount * (1 - discountPercent / 100);
    }
}

7. コードカバレッジ: JaCoCo と Sonar

JaCoCo 設定

<!-- pom.xml -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.10</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
        <execution>
            <id>check</id>
            <goals>
                <goal>check</goal>
            </goals>
            <configuration>
                <rules>
                    <rule>
                        <element>BUNDLE</element>
                        <limits>
                            <limit>
                                <counter>LINE</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>
                            </limit>
                            <limit>
                                <counter>BRANCH</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.70</minimum>
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

Sonar 統合

# sonar-project.properties
sonar.projectKey=my-java-project
sonar.projectName=My Java Project
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.coveragePlugin=jacoco
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
sonar.exclusions=**/generated/**,**/dto/**
# GitHub Actions での実行例
- name: Build and analyze
  env:
    SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
  run: mvn verify sonar:sonar -Dsonar.host.url=https://sonarcloud.io
クイズ5: ラインカバレッジとブランチカバレッジの違いは?

答え: ラインカバレッジはテストで実行されたコード行の割合を示します。ブランチカバレッジは if/else・switch などの分岐をどれだけカバーしているかを示します。

解説: 例えば if (x > 0) { doA(); } else { doB(); } というコードがあります。x = 1 のみでテストした場合、ラインカバレッジは 100% でも、ブランチカバレッジは 50% です(else ブランチが未テスト)。一般的にブランチカバレッジの方が品質の高い指標とされます。JaCoCo ではラインカバレッジ 80%・ブランチカバレッジ 70% 以上を目安にすることが多いです。


8. テストのベストプラクティス

AAA パターン (Arrange-Act-Assert)

@Test
void calculateOrderTotal_withDiscountCode_appliesDiscount() {
    // Arrange
    Order order = new Order();
    order.addItem(new Item("Book", 2000));
    order.addItem(new Item("Pen", 500));
    DiscountCode code = new DiscountCode("SAVE10", 10);

    // Act
    double total = orderService.calculateTotal(order, code);

    // Assert
    assertThat(total).isEqualTo(2250.0);  // 2500 * 0.9
}

テストの命名規則

// methodName_stateUnderTest_expectedBehavior パターン
@Test
void getUserById_existingId_returnsUser() { }

@Test
void getUserById_nonExistingId_throwsNotFoundException() { }

@Test
void processPayment_insufficientFunds_returnsFailure() { }

// given_when_then パターン
@Test
@DisplayName("有効なクレジットカードで支払いが成功する")
void givenValidCard_whenProcessPayment_thenSucceeds() { }
クイズ6: テストピラミッドとは何か?

答え: テストピラミッドとは、下から「単体テスト(多)→ 統合テスト(中)→ E2E テスト(少)」という形で構成される理想的なテスト戦略の概念です。

解説: 単体テストは高速・安定・安価なのでたくさん書きます。統合テストは外部サービスや DB を含むため適度な数にします。E2E テスト(ブラウザテスト等)は実行に時間がかかりフラッキーになりやすいので最小限にします。この逆三角形(E2E が多い状態)を「アイスクリームコーン」と呼び、アンチパターンとされています。


まとめ

ツール役割主なアノテーション
JUnit5テストフレームワーク@Test, @ParameterizedTest, @Nested
Mockitoモック/スタブ@Mock, @Spy, @InjectMocks, @Captor
WireMockHTTP スタブstubFor, verify
TestContainers統合テスト@Container, @Testcontainers
Spring Boot TestWeb レイヤーテスト@WebMvcTest, @DataJpaTest
JaCoCoカバレッジ計測Maven Plugin

適切なテスト戦略を構築することで、リグレッションを防ぎ、リファクタリングへの自信を持つことができます。TDD を実践し、Red-Green-Refactor のサイクルを意識してコードを書きましょう。