1. 概要

Java 8では、ラムダやストリームなど、さまざまな新しいすばらしい機能が導入されました。 そして当然、Mockitoは2番目のメジャーバージョンでこれらの最近の革新を活用しました。

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

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

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

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

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

public interface JobService {
 
    Optional<JobPosition> 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のモックバージョンを作成し、次に、実装されていないメソッドの呼び出しから既知の値を返し、 assignJobPosition()が呼び出されたときに実際のメソッドを呼び出すようにMockitoに指示します。

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の内部の仕組みは、この構造の準備ができていませんでした。 このコードをMockitopreversion 2で実行すると、次のようなエラーが発生します。

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バージョンはここにあります)。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.7.5</version>
    <scope>test</scope>
</dependency>

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

3. オプションのおよびStreamのデフォルト値を返します

オプションのおよびStreamは、その他のJava8の新しい追加機能です。 2つのクラスの類似点の1つは、どちらも空のオブジェクトを表す特別なタイプの値を持っていることです。 この空のオブジェクトにより、これまでのところ遍在するNullPointerException。を簡単に回避できます。

3.1. オプションの例

前のセクションで説明した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<JobPosition> optional = jobService.findCurrentJobPosition(person);
        
        return !optional.isPresent();
    }
}

ここで、現在の職位がない場合に失業支援を受ける資格があることを確認するためのテストを作成するとします。

その場合、 findCurrentJobPosition()に空のオプションを返すように強制します。 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));
    }
}

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

オプションを処理するときにnull値を処理することはめったにないため、Mockitoはデフォルトで空のオプションを返すようになりました。 これは、Optional.empty()への呼び出しの戻り値とまったく同じ値です。

したがって、Mockitoバージョン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<JobPosition> listJobs(Person person);
}

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

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

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

Mockito 2より前では、このようなテストを作成するには、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());
    }
}

バージョン2にアップグレードすると、 when(…).thenReturn(…)呼び出しをドロップできます。これは、Mockitoがデフォルトでモックされたメソッドで空のストリームを返すためです。

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. Lambdaと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. ラムダとカスタムの組み合わせ回答

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

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

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

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

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

5. 結論

この記事では、Java8とMockito2の新機能を組み合わせて、よりクリーンでシンプルで短いコードを作成する方法について説明しました。 ここで見たJava8の機能のいくつかに精通していない場合は、次の記事を確認してください。

また、GitHubリポジトリで付随するコードを確認してください。