MockitoのJava 8機能

Mockito Java 8の機能

1. 概要

Java 8は、ラムダやストリームなどの一連の新しい素晴らしい機能を導入しました。 そして当然、Mockitoはこれらの最近の革新を2nd major versionで活用しました。

この記事では、この強力な組み合わせが提供するすべてのものを検討します。

2. デフォルトの方法でインターフェースをモックする

Java 8以降、インターフェイスにメソッド実装を記述できるようになりました。 これは素晴らしい新機能かもしれませんが、その言語への導入は、その概念以来Javaの一部であった強力な概念に違反していました。

Mockitoバージョン1はこの変更に対応していませんでした。 基本的に、インターフェースから実際のメソッドを呼び出すように要求することができなかったためです。

2つのメソッド宣言を持つインターフェースがあると想像してください。1つは私たちが慣れ親しんでいる昔ながらのメソッドシグネチャで、もう1つはまったく新しいdefaultメソッドです。

public interface JobService {

    Optional findCurrentJobPosition(Person person);

    default boolean assignJobPosition(Person person, JobPosition jobPosition) {
        if(!findCurrentJobPosition(person).isPresent()) {
            person.setCurrentJobPosition(jobPosition);

            return true;
        } else {
            return false;
        }
    }
}

assignJobPosition()defaultメソッドには、実装されていないfindCurrentJobPosition()メソッドへの呼び出しがあることに注意してください。

ここで、findCurrentJobPosition()の実際の実装を記述せずに、assignJobPosition()の実装をテストするとします。 JobService,のモックバージョンを作成し、Mockitoに、実装されていないメソッドの呼び出しから既知の値を返し、assignJobPosition()が呼び出されたときに実際のメソッドを呼び出すように指示するだけです。

public class JobServiceUnitTest {

    @Mock
    private JobService jobService;

    @Test
    public void givenDefaultMethod_whenCallRealMethod_thenNoExceptionIsRaised() {
        Person person = new Person();

        when(jobService.findCurrentJobPosition(person))
              .thenReturn(Optional.of(new JobPosition()));

        doCallRealMethod().when(jobService)
          .assignJobPosition(
            Mockito.any(Person.class),
            Mockito.any(JobPosition.class)
        );

        assertFalse(jobService.assignJobPosition(person, new JobPosition()));
    }
}

これは完全に合理的であり、インターフェイスの代わりに抽象クラスを使用していれば、うまく機能します。

ただし、Mockito 1の内部構造は、この構造に対応する準備ができていませんでした。 このコードをバージョン2より前のMockitoで実行すると、うまく説明された次のエラーが表示されます。

org.mockito.exceptions.base.MockitoException:
Cannot call a real method on java interface. The interface does not have any implementation!
Calling real methods is only possible when mocking concrete classes.

Mockitoはその仕事をしていて、この操作はJava 8以前には考えられなかったため、インターフェースで実際のメソッドを呼び出すことはできないと言っています。

幸いなことに、使用しているMockitoのバージョンを変更するだけで、このエラーをなくすことができます。 たとえば、Mavenを使用すると、バージョン2.7.5を使用できます(最新のMockitoバージョンはhereにあります)。


    org.mockito
    mockito-core
    2.7.5
    test

コードを変更する必要はありません。 次回テストを実行すると、エラーは発生しなくなります。

3. OptionalおよびStreamのデフォルト値を返す

OptionalおよびStreamは、他のJava8の新しい追加です。 2つのクラスの1つの類似点は、両方が空のオブジェクトを表す特別なタイプの値を持つことです。 この空のオブジェクトにより、これまでのところ遍在するNullPointerException.を簡単に回避できます。

3.1. Optionalの例

前のセクションで説明したJobServiceを注入し、JobService#findCurrentJobPosition()を呼び出すメソッドを持つサービスについて考えてみます。

public class UnemploymentServiceImpl implements UnemploymentService {

    private JobService jobService;

    public UnemploymentServiceImpl(JobService jobService) {
        this.jobService = jobService;
    }

    @Override
    public boolean personIsEntitledToUnemploymentSupport(Person person) {
        Optional optional = jobService.findCurrentJobPosition(person);

        return !optional.isPresent();
    }
}

ここで、ある人が現在の職に就いていないときに失業支援を受ける資格があることを確認するテストを作成するとします。

その場合、findCurrentJobPosition()に空のOptionalを返すように強制します。 Before Mockito 2、そのメソッドの呼び出しをモックする必要がありました。

public class UnemploymentServiceImplUnitTest {

    @Mock
    private JobService jobService;

    @InjectMocks
    private UnemploymentServiceImpl unemploymentService;

    @Test
    public void givenReturnIsOfTypeOptional_whenMocked_thenValueIsEmpty() {
        Person person = new Person();

        when(jobService.findCurrentJobPosition(any(Person.class)))
          .thenReturn(Optional.empty());

        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
    }
}

モックオブジェクトへのメソッド呼び出しに対するMockitoのデフォルトの戻り値はnullであるため、13行目のこのwhen(…).thenReturn(…)命令が必要です。 バージョン2はその動作を変更しました。

Optional,Mockito now returns an empty Optional by defaultを処理するときにnull値を処理することはめったにないためです。 これは、Optional.empty()への呼び出しの戻り値とまったく同じ値です。

したがって、when using Mockito version 2の場合、13行目を削除しても、テストは引き続き成功します。

public class UnemploymentServiceImplUnitTest {

    @Test
    public void givenReturnIsOptional_whenDefaultValueIsReturned_thenValueIsEmpty() {
        Person person = new Person();

        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
    }
}

3.2. Streamの例

Streamを返すメソッドをモックした場合も同じ動作が発生します。

JobServiceインターフェースに新しいメソッドを追加して、人がこれまでに働いたことのあるすべての職位を表すストリームを返しましょう。

public interface JobService {
    Stream listJobs(Person person);
}

このメソッドは、特定の検索文字列に一致するジョブで作業したことがあるかどうかを照会する別の新しいメソッドで使用されます。

public class UnemploymentServiceImpl implements UnemploymentService {

    @Override
    public Optional searchJob(Person person, String searchString) {
        return jobService.listJobs(person)
          .filter((j) -> j.getTitle().contains(searchString))
          .findFirst();
    }
}

したがって、listJobs()の記述について心配することなく、searchJob(),の実装を適切にテストし、その人がまだ仕事をしていないときにシナリオをテストするとします。 その場合、listJobs()が空のStreamを返すようにします。

そのようなテストを書くためのBefore Mockito 2, we would need to mock the call to listJobs()

public class UnemploymentServiceImplUnitTest {

    @Test
    public void givenReturnIsOfTypeStream_whenMocked_thenValueIsEmpty() {
        Person person = new Person();
        when(jobService.listJobs(any(Person.class))).thenReturn(Stream.empty());

        assertFalse(unemploymentService.searchJob(person, "").isPresent());
    }
}

If we upgrade to version 2の場合、Mockito will return an empty Stream on mocked methods by default:が次のようになるため、when(…).thenReturn(…)呼び出しをドロップできます。

public class UnemploymentServiceImplUnitTest {

    @Test
    public void givenReturnIsStream_whenDefaultValueIsReturned_thenValueIsEmpty() {
        Person person = new Person();

        assertFalse(unemploymentService.searchJob(person, "").isPresent());
    }
}

4. ラムダ式の活用

Java 8のラムダ式を使用すると、ステートメントをはるかにコンパクトで読みやすくすることができます。 Mockitoを使用する場合、ラムダ式によってもたらされる単純さの2つの非常に優れた例は、ArgumentMatchersとカスタムAnswersです。

4.1. ラムダとArgumentMatcherの組み合わせ

Java 8より前は、ArgumentMatcherを実装するクラスを作成し、matches()メソッドでカスタムルールを作成する必要がありました。

Java 8では、内部クラスを単純なラムダ式に置き換えることができます。

public class ArgumentMatcherWithLambdaUnitTest {

    @Test
    public void whenPersonWithJob_thenIsNotEntitled() {
        Person peter = new Person("Peter");
        Person linda = new Person("Linda");

        JobPosition teacher = new JobPosition("Teacher");

        when(jobService.findCurrentJobPosition(
          ArgumentMatchers.argThat(p -> p.getName().equals("Peter"))))
          .thenReturn(Optional.of(teacher));

        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(linda));
        assertFalse(unemploymentService.personIsEntitledToUnemploymentSupport(peter));
    }
}

4.2. ラムダとカスタムAnswerの組み合わせ

ラムダ式をMockitoのAnswerと組み合わせると、同じ効果が得られます。

たとえば、Personの名前が「Peter」の場合に単一のJobPositionを含むStreamを返すように、listJobs()メソッドへの呼び出しをシミュレートする場合」、それ以外の場合は空のStream、それ以外の場合は、Answerインターフェイスを実装するクラス(匿名または内部)を作成する必要があります。

繰り返しますが、ラムダ式を使用すると、すべての模擬動作をインラインで記述できます。

public class CustomAnswerWithLambdaUnitTest {

    @Before
    public void init() {
        MockitoAnnotations.initMocks(this);

        when(jobService.listJobs(any(Person.class))).then((i) ->
          Stream.of(new JobPosition("Teacher"))
          .filter(p -> ((Person) i.getArgument(0)).getName().equals("Peter")));
    }
}

上記の実装では、PersonAnswer内部クラスは必要ないことに注意してください。

5. 結論

この記事では、新しいJava 8とMockito 2の機能を一緒に活用して、よりクリーンでシンプルで短いコードを作成する方法を説明しました。 ここで見たJava 8の機能のいくつかに慣れていない場合は、次の記事をご覧ください。

また、GitHub repositoryの付随するコードを確認してください。