【Java】Mockitoでモック化したメソッドを複数回呼ぶ方法

Mockitoでモック化したメソッドを複数回呼ぶ方法です。

実行環境は以下になります。

JDK:17
Junit:4.13.2
Mockito:4.0.0

引数が基本型の場合

以下のような基本型の引数のケースです。

StringやLocalDateなどの参照型でも基本型に扱いが近しいものはこのケースに含まれます。

public String execute1(int no) {
    return null; // do something
}

doReturnを複数定義し、引数に想定の値をセットすればOKです。

doReturn("1st").when(logic).execute1(1);
doReturn("2nd").when(logic).execute1(2);

引数が参照型の場合(フィールドが基本型のみ)

以下のような参照型の引数のケースです。

なおかつ、参照型クラスのフィールドが基本型のみのケースです。

public String execute2(Dto dto) {
    return null; // do something
}

doReturnを複数定義し、引数に想定の値をrefEq()でセットすればOKです。

Dto dto1 = new Dto();
dto1.setNo(1);
Dto dto2 = new Dto();
dto2.setNo(2);
doReturn("1st").when(logic).execute2(refEq(dto1));
doReturn("2nd").when(logic).execute2(refEq(dto2));

引数が参照型の場合(フィールドに参照型が含まれる)

以下のような参照型の引数のケースです。

なおかつ、参照型クラスのフィールドに別の参照型クラスが含まれるケースです。

public String execute3(NestDto nestDto) {
    return null; // do something
}

ArgumentMatcherは一つのメソッドに対して複数定義することができません。

そこで、doReturn()のメソッドチェーンとArgumentCaptorを使って対応します。

doReturn()はメソッドチェーンで繋げることができるので、1回目に返す値、2回目に返す値を指定することができます。

メソッドの引数にany()を使えば、どんな引数が来てもdoReturn()が適用されるので、これで複数回呼びことが可能になります。

any()を使うと引数の検証ができないので、ArgumentCaptorを使います。

ArgumentCaptorは実際にメソッドに渡された引数をキャプチャしてくれるので、ArgumentCaptorから引数の値を取得して検証します。

doReturn("1st").doReturn("2nd").when(logic).execute3(any(NestDto.class));
ArgumentCaptor<NestDto> captor = ArgumentCaptor.forClass(NestDto.class);

List<String> actual = target.execute3(logic);

verify(logic, times(2)).execute3(captor.capture());
assertThat(captor.getAllValues().get(0).getDto().getNo(), is(1));
assertThat(captor.getAllValues().get(1).getDto().getNo(), is(2));

ソースコード

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Dto {
    private int no;
}
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class NestDto {
    private Dto dto;
}
public class Logic {

    // 引数が基本型の場合
    public String execute1(int no) {
        return null; // do something
    }

    // 引数が参照型の場合(フィールドが基本型のみ)
    public String execute2(Dto dto) {
        return null; // do something
    }

    // 引数が参照型の場合(フィールドに参照型が含まれる)
    public String execute3(NestDto nestDto) {
        return null; // do something
    }

}
import java.util.Arrays;
import java.util.List;

public class Sample {

    // 引数が基本型の場合
    public List<String> execute1(Logic logic) {
        String str1 = logic.execute1(1);
        String str2 = logic.execute1(2);
        return Arrays.asList(str1, str2);
    }

    // 引数が参照型の場合(フィールドが基本型のみ)
    public List<String> execute2(Logic logic) {
        Dto dto1 = new Dto();
        dto1.setNo(1);
        String str1 = logic.execute2(dto1);

        Dto dto2 = new Dto();
        dto2.setNo(2);
        String str2 = logic.execute2(dto2);

        return Arrays.asList(str1, str2);
    }

    // 引数が参照型の場合(フィールドに参照型が含まれる)
    public List<String> execute3(Logic logic) {
        NestDto dto1 = new NestDto();
        dto1.setDto(new Dto());
        dto1.getDto().setNo(1);
        String str1 = logic.execute3(dto1);

        NestDto dto2 = new NestDto();
        dto2.setDto(new Dto());
        dto2.getDto().setNo(2);
        String str2 = logic.execute3(dto2);

        return Arrays.asList(str1, str2);
    }

}
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;

import java.util.List;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.*;

public class SampleTest {

    // 引数が基本型の場合
    @Test
    public void test1() {
        // セットアップ
        Sample target = new Sample();
        Logic logic = mock(Logic.class);

        doReturn("1st").when(logic).execute1(1);
        doReturn("2nd").when(logic).execute1(2);

        // テスト実行
        List<String> actual = target.execute1(logic);

        // 結果検証
        verify(logic, times(1)).execute1(1);
        verify(logic, times(1)).execute1(2);
        assertThat(actual.get(0), is("1st"));
        assertThat(actual.get(1), is("2nd"));
    }

    // 引数が参照型の場合(フィールドが基本型のみ)
    @Test
    public void test2() {
        // セットアップ
        Sample target = new Sample();
        Logic logic = mock(Logic.class);

        Dto dto1 = new Dto();
        dto1.setNo(1);
        Dto dto2 = new Dto();
        dto2.setNo(2);
        doReturn("1st").when(logic).execute2(refEq(dto1));
        doReturn("2nd").when(logic).execute2(refEq(dto2));

        // テスト実行
        List<String> actual = target.execute2(logic);

        // 結果検証
        verify(logic, times(1)).execute2(refEq(dto1));
        verify(logic, times(1)).execute2(refEq(dto2));
        assertThat(actual.get(0), is("1st"));
        assertThat(actual.get(1), is("2nd"));
    }

    // 引数が参照型の場合(フィールドに参照型が含まれる)
    @Test
    public void test3() {
        // セットアップ
        Sample target = new Sample();
        Logic logic = mock(Logic.class);

        doReturn("1st").doReturn("2nd").when(logic).execute3(any(NestDto.class));
        ArgumentCaptor<NestDto> captor = ArgumentCaptor.forClass(NestDto.class);

        // テスト実行
        List<String> actual = target.execute3(logic);


        // 結果検証
        verify(logic, times(2)).execute3(captor.capture());
        assertThat(captor.getAllValues().get(0).getDto().getNo(), is(1));
        assertThat(captor.getAllValues().get(1).getDto().getNo(), is(2));
        assertThat(actual.get(0), is("1st"));
        assertThat(actual.get(1), is("2nd"));
    }

    // 引数が参照型の場合(フィールドに参照型が含まれる)
    // ArgumentMatcherを使った失敗例
    @Test
    public void test4() {
        // セットアップ
        Sample target = new Sample();
        Logic logic = mock(Logic.class);

        ArgumentMatcher<NestDto> matcher1 = argument -> {
            assertThat(argument.getDto().getNo(), is(1));
            return true;
        };
        ArgumentMatcher<NestDto> matcher2 = argument -> {
            assertThat(argument.getDto().getNo(), is(2));
            return true;
        };
        doReturn("1st").when(logic).execute3(argThat(matcher1));
        doReturn("2nd").when(logic).execute3(argThat(matcher2)); // matcher1が上書きされる

        // テスト実行
        List<String> actual = target.execute3(logic);

        // 結果検証
        verify(logic, times(1)).execute3(argThat(matcher1)); // NG
        verify(logic, times(1)).execute3(argThat(matcher2));
        assertThat(actual.get(0), is("1st"));
        assertThat(actual.get(1), is("2nd"));
    }

}

関連記事

Mockitoの引数の検証方法については以下を参考にして下さい。

stmtk358.hatenablog.com

参考文献

javadoc.io

www.javadoc.io