【Java】【Mockit】モックメソッドの引数と呼び出し回数の検証

2024年1月13日

Mockitを使ってモック化したメソッドの呼び出し回数と呼び出し時に渡された引数の値を検証する方法についてまとめます。

準備

動作検証を行った環境は以下の通りです。

  • Java11
  • Spring Boot 2.1.11

テスト対象として以下のクラスを作成します。今回は商品の検索、保存を行うItemRepositoryクラスとItemRepositoryクラスを使用するItemServiceクラスを作成します。

ItemServiceクラスをテスト対象とし、依存先のItemRepositoryクラスをモック化します。

Itemクラス

package com.example.demo;

import lombok.Value;

@Value
public class Item {

    private int id;
    private String itemName;
    private int price;
}

ItemServiceクラス

リポジトリクラスを使用してItemの検索、保存を行うサービスクラスです。

package com.example.demo;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Transactional
@RequiredArgsConstructor
@Service
public class ItemService {

    private final ItemRepository itemRepository;

    // IDで検索
    public Item findById(int id) {

        return itemRepository.findById(id);
    }

    // 1つのItemを保存
    public void save(Item item) {

        var foundItem = findById(item.getId());
        if (foundItem == null) {
            itemRepository.save(item);
        }
    }

    // 複数のItemを保存
    public void save(List<Item> items) {

        items.forEach(this::save);
    }
}

ItemRepositoryクラス

リポジトリクラスです。未完成の状態を想定して各メソッドの処理は記述していません。

package com.example.demo;

import org.springframework.stereotype.Repository;

@Repository
public class ItemRepository {

    public Item findById(int id) {
        return null;
    }

    public void save(Item item) {

    }
}

テストコード

以下のようなテストコードを作成しました。このテストコードではItemRepositoryをモック化し、saveメソッドの実行回数と渡された引数を検証しています。

verifyメソッドを使用することでモック化したメソッドの呼び出し回数を検証することができます。実行回数はtimesメソッドを使って指定できます。

ArgumentCaptureクラスを使用することでモック化したメソッドに渡された引数の値を取得できます。取得した値をassertThatを使って検証しています。

package com.example.demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class ItemServiceTest {

    @InjectMocks
    private ItemService itemService;

    @Mock
    private ItemRepository itemRepository;

    // ItemRepositoryのsaveメソッドが実行されないことをテスト
    @Test
    public void save_not_called_test() {

        var item = new Item(1, "newItem", 100);

        // findByIdメソッドをモック化
        when(itemRepository.findById(anyInt())).thenReturn(new Item(1, "MockItem", 100));
        itemService.save(item);

        // ItemRepositoryクラスのsaveメソッドは実行されない
        verify(itemRepository, times(0)).save(any());
    }

    // ItemRepositoryのsaveメソッドが1回実行されることをテスト
    // saveメソッドの引数を検証
    @Test
    public void save_called_test() {

        var item = new Item(1, "newItem", 100);

        when(itemRepository.findById(anyInt())).thenReturn(null);
        doNothing().when(itemRepository).save(any());

        itemService.save(item);

        var argumentCapture = ArgumentCaptor.forClass(Item.class);

        // ItemRepositoryクラスのsaveメソッドは1回実行される
        verify(itemRepository, times(1)).save(argumentCapture.capture());

        // 引数を検証
        var capturedItem = argumentCapture.getValue();
        assertThat(capturedItem.getId()).isEqualTo(1);
        assertThat(capturedItem.getItemName()).isEqualTo("newItem");
        assertThat(capturedItem.getPrice()).isEqualTo(100);
    }

    // ItemRepositoryのsaveメソッドが2回実行されることをテスト
    // saveメソッドの引数を検証
    @Test
    public void save_items_test() {

        var item1 = new Item(1, "newItem1", 100);
        var item2 = new Item(2, "newItem2", 200);
        var items = List.of(item1, item2);

        doNothing().when(itemRepository).save(any());

        itemService.save(items);

        var argumentCapture = ArgumentCaptor.forClass(Item.class);

        // ItemRepositoryクラスのsaveメソッドは2回実行される
        verify(itemRepository, times(2)).save(argumentCapture.capture());

        // 引数を検証
        var capturedItem2 = argumentCapture.getAllValues();
        assertThat(capturedItem2).extracting("id", "itemName", "price")
                .containsExactlyInAnyOrder(
                        tuple(1, "newItem1", 100),
                        tuple(2, "newItem2", 200)
                );
    }
}

複数引数の検証

モック対象のメソッドが複数の引数を持つ場合にどのように検証するかを説明します。結論を言うと、引数ごとにArgumentCaptureを生成すればよいだけです。

先ほどの例のItemRepositoryに2つの引数を持つsave(Item item, ItemDetail itemDescription)メソッドを追加します。

package com.example.demo;

import org.springframework.stereotype.Repository;

@Repository
public class ItemRepository {

    public Item findById(int id) {
        return null;
    }

    public void save(Item item) {

    }

    // 2つの引数を持つメソッドを追加
    public void save(Item item, ItemDetail itemDescription) {

    }
}

ItemDetailクラスは以下のように定義しました。2つの引数を持つ例を作りたかっただけなので、クラスの中身はあまり意味はありません。

package com.example.demo;

import lombok.Value;

@Value
public class ItemDetail {

    private int itemId;
    private String description;
}
ItemServiceTestにテストケースを追加します。save(Item item, ItemDetail itemDescription)メソッドの引数であるitem, itemDetailに対応するArgumentCaptureのインスタンスをitemCapture, itemDetailCaptureとして生成しています。

それ以外はモック化したメソッドが1つの場合と同じで、ArgumentCaptureクラスのgetAllValuesメソッドを使って引数として渡された値を取得し、検証しています。
package com.example.demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class ItemServiceTest {

    @InjectMocks
    private ItemService itemService;

    @Mock
    private ItemRepository itemRepository;

    // 前述のテストケースは省略
    // 2つの引数を持つメソッドをモック化した場合
    @Test
    public void save_item_and_detail_test() {

        var item1 = new Item(1, "newItem1", 100);
        var itemDetail1 = new ItemDetail(1, "Good!");

        itemService.save(item1, itemDetail1);

        var itemCapture = ArgumentCaptor.forClass(Item.class);
        var itemDetailCapture = ArgumentCaptor.forClass(ItemDetail.class);

        verify(itemRepository, times(1)).save(itemCapture.capture(), itemDetailCapture.capture());


        var capturedItems = itemCapture.getAllValues();
        assertThat(capturedItems).extracting("id", "itemName", "price")
                .containsExactlyInAnyOrder(
                        tuple(1, "newItem1", 100)
                );

        var capturedItemDetails = itemDetailCapture.getAllValues();
        assertThat(capturedItemDetails).extracting("itemId", "description")
                .containsExactlyInAnyOrder(
                        tuple(1, "Good!")
                );
    }
}

この記事ではSpring Bootで作成したプロジェクト内でMockitを使ってモック化する方法と、モック化したメソッドに対する引数、モック化したメソッドの呼び出し方法を検証する方法を紹介しました。

また、ソフトウェアテストの手法やテストの進め方については【この1冊でよくわかる】ソフトウェアテストの教科書で分かりやすく解説されています。