😎

Spring Bootにおける単体テストと結合テストの違い

2024/02/11に公開

はじめに

Spring Bootのテストと格闘していたら、テストコードにおいて単体テストと結合テストという概念があることを知り、なんとなく違いがわかったので、書き残しておきます。

単体テスト

Spring Bootにおける単体テスト(Unit Test)とは、他のオブジェクトやインフラストラクチャに依存しない形で実装するテストのことです。

他の要素に依存しないため、実行速度が速いという特徴があります。

例えば、以下のようなServiceの処理があったとします。


@Service
public class ItemService {

    private final ItemRepository itemRepository;

    public ItemService(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }

    public Optional<Item> getItemById(Long id) {
        return itemRepository.findById(id);
    }

    public Item saveItem(Item item) {
        return itemRepository.save(item);
    }
}

単体テストの場合、自身の処理のみをテストし、依存する外部の処理はMockなどを活用して全て代替処理として実行します。
以下、単体テストの実装例です。


@ExtendWith(MockitoExtension.class)
public class ItemServiceTest {

    @Mock
    private ItemRepository itemRepository;

    @InjectMocks
    private ItemService itemService;

    @Test
    public void getItemByIdTest() {
        Item item = new Item();
        item.setId(1L);
        item.setName("Item1");
        item.setPrice(100.0);

        when(itemRepository.findById(1L)).thenReturn(Optional.of(item));

        Optional<Item> found = itemService.getItemById(1L);

        assertTrue(found.isPresent());
        assertEquals("Item1", found.get().getName());
    }

    @Test
    public void saveItemTest() {
        Item item = new Item();
        item.setName("NewItem");
        item.setPrice(200.0);

        when(itemRepository.save(any(Item.class))).thenReturn(item);

        Item savedItem = itemService.saveItem(item);

        assertNotNull(savedItem);
        assertEquals("NewItem", savedItem.getName());
    }
}

結合テスト

Spring Bootにおける結合テスト(Integration Test)とは、複数のコンポーネントやシステムが統合された状態で、アプリケーションが正しく動作するかを検証するテストです。

これには、データベース、ファイルシステム、外部サービスとの連携が含む場合があります。

実際の依存関係を使用して実行されるため、基本的に単体テストより長い実行時間がかかりますが、アプリケーションの各部分が互いに正しく連携していることを検証できます。

単体テストのところで用意したItemServiceに対して、結合テストを実装すると以下のようになります。


@SpringBootTest
public class ItemServiceIntegrationTest {

    @Autowired
    private ItemService itemService;

    @Autowired
    private ItemRepository itemRepository;

    @Test
    public void testGetItemById() {
        // Given
        Item item = new Item();
        item.setName("Test Item");
        item.setPrice(100.0);
        item = itemRepository.save(item);

        // When
        Optional<Item> foundItem = itemService.getItemById(item.getId());

        // Then
        assertTrue(foundItem.isPresent());
        assertEquals("Test Item", foundItem.get().getName());
        assertEquals(100.0, foundItem.get().getPrice());
    }

    @Test
    public void testSaveItem() {
        // Given
        Item newItem = new Item();
        newItem.setName("New Item");
        newItem.setPrice(200.0);

        // When
        Item savedItem = itemService.saveItem(newItem);

        // Then
        assertNotNull(savedItem.getId());
        assertEquals("New Item", savedItem.getName());
        assertEquals(200.0, savedItem.getPrice());
    }
}

SpringBootの結合テストの場合、@SpringBootTestアノテーションを使用して、Spring Bootの全アプリケーションコンテキストをテスト時にロードします。

これにより、ItemServiceItemRepositoryが実際のアプリケーション環境と同様に動作することを保証します。

単体テストと結合テストの使い分け

実際にはプロジェクトの方針によりますが、基本的には、最初に実行時間が短く依存度が低い単体テストを中心に行い品質を確保し、その後に結合テストの比重を高めて全体の品質を保証するのが良いと考えます。

また、CI環境では、単体テストと結合テストの両方を自動的に実行することが望ましいです。

おわりに

単体テストと結合テスト、正直違いが感覚的に把握できてなかったのですが、実際のコードであれこれ試してみたことで、色々理解が進みました。満足です。

参考

Discussion