Computer

@MockBeanと@Mock、Mockito.mock()の違い

2020年3月7日

概要

@Mock、@MockBean、Mockito.mock()の違いを調べたので備忘録を兼ねてまとめておきます。

Spring Bootのアプリケーションなどをテストする時に便利なモックオブジェクトですが、他の人が書いたコードを見ていると、@Mockや@MockBean、Mockito.mock()メソッドを使ってモックをするコードをよく見かけます。

@MockとMockito.mock()はMockitoなのですが、@MockBeanはSpring Frameworkのアノテーションとなっています。Spring Bootは自動的にMockitoを使えるようにしてくれるので便利ですが、違いをわからないとつまづくこともあります。

Mockito.mock()

Mockito.mock() は手動でモックオブジェクトを生成する方法です。

@Test
public void getUserCount_noUser_returnZero() {
    UserRepository mockRepository = Mockito.mock(UserRepository.class);
    Mockito.when(mockRepository.countAll()).thenReturn(0L);
 
    long userCount = mockRepository.countAll();
 
    Assert.assertEquals(0L, userCount);
    Mockito.verify(localMockRepository).countAll();
}

Mockitoのメソッドをじかに呼び出してモックを作るこの方法は、普通のテストクラスで使えます。

@Mock

@Mock アノテーションを使うと、Mockito.mock()メソッドを使わずにモックオブジェクトを作れます。

@RunWith(MockitoJUnitRunner.class)
public class UserRepositoryTest {
     
    @Mock
    UserRepository mockRepository;
     
    @Test
    public void getUserCount_noUser_returnZero() {
        Mockito.when(mockRepository.countAll()).thenReturn(0L);
 
        long userCount = mockRepository.countAll();
 
        Assert.assertEquals(0L, userCount);
        Mockito.verify(mockRepository).countAll();
    }
}

上の例のように、クラス変数の上に@Mock をつけることで、mockRepositoryにはモックオブジェクトがセットされます。

このアノテーションを処理させるには、@RunWith MockitoJUnitRunner.classを指定するか、MockitoAnnotations.initMocks() を手動で呼び出す必要があります。

@MockBean

@MockBean アノテーションはMockitoではなくSpring Bootが提供するアノテーションです。@MockBeanを使うと、Spring Bootは通常のMockitoのモックを作成し、それをアプリケーションコンテキスト内に登録します。

そのため、@MockBeanで生成されたモックオブジェクトは、@Autowiredなど、SpringのDIのメカニズムを通じて注入されます。

@RunWith(SpringRunner.class)
public class UserRepositoryTest {
     
    @MockBean
    UserRepository mockRepository;

    @Autowired
    UserService userService;
     
    @Test
    public void getUserCount_noUser_returnZero() {
        Mockito.when(mockRepository.countAll()).thenReturn(0L);
 
        long userCount = userService.count();
 
        Assert.assertEquals(0L, userCount);
        Mockito.verify(mockRepository).countAll();
    }
}

上の例はUserServiceUserRepositoryに依存しているケースですが、userRepositoryには@MockBeanアノテーションがついているため、モックオブジェクトが生成され、同時にアプリケーションコンテキストに追加されます。

userServiceにはモックされたUserRepositoryがインジェクトされた状態で返されます。

上の@Mockアノテーションと似ていますが、Springのアプリケーションコンテキストにモックオブジェクトを登録するかどうかが変わってきます。また、このアノテーションを処理させるには、1行目で行なっているように、@RunWith SpringRunner.classを指定する必要があります。

MockitoかSpring Bootかの違い

以上をフレームワーク別に分けると

@MockMockito.mock()はMockito

@MockBeanは Spring Boot

ということになるので、どちらのツールを使うべきかわかりやすくなります。

一般的にSpring Bootを使ったテストは非常に遅いので、ユニットテストには適しません

Spring Bootを使ったテストは、Springのアプリケーションを完全にロードしてからテストを実行します。そのため、アプリケーションが大きくなれななるほど動作が遅くなり、ちょっとユニットテストをするだけでも起動に10秒以上かかってしまいます。

理想的にはユニットテストは一瞬で終わるように書くべきだといわれますし、実際に僕もそう思います。

テストが起動するまでに毎回10秒とかかかってしまうと、時間をムダにしている感じがありますし、それが原因でテストをするのが億劫になってしまうと本末転倒です。

そのためには、自分がテストしているロジック以外は全てモックしてしまうMockitoを使ったテストが理にかなっているといえます。

ユニットテスト= すべてMockitoで対応

結合テストだけ Spring Boot Test

と分けるとスッキリするかもしれません。

@MockBeanを多用するとアプリケーションコンテキストが何度も再生成されるので遅くなる

@MockMockito.mock()@MockBeanの違いをまとめましたが、これを調べたきっかけは、@MockBeanを使ってテストを書いていた時におかしな問題が発生したためです。

その問題とは、

  1. @MockBeanを使って外部サービスをモックしたテストクラスを作成した
  2. IDEからこのテストクラスだけを実行すると正常に動作する
  3. 他のテストクラスとまとめて実行すると、@MockBeanを使っているクラスに差し掛かったところでFailed to create application contextというエラーが発生する
  4. ログを見ると、テスト開始前にすでに生成されたはずのデータベーステーブルを作成しようとしてHibernateがエラーを出していた

というものでした。

使用していたのはSpring Bootの1.5.15.RELEASEと1.5.19.RELEASEですが、これはSpringのバグではないのでどのバージョンでも同じ問題が起こるはずです。Spring Frameworkは「本当に同じ人間が作っているのか」と思うくらいに安定感と品質がズバ抜けて高いので、何かおかしいときはまずは自分の問題を疑うのが無難です…

動作を見ていると、Spring Boot Testは@MockBeanを付与したテストクラスを見つけると、そこでアプリケーションコンテキストを再生成するようです。

僕のアプリケーションは少し特殊な構成になっていて、スキーマの生成をJavaコードで行なっていました。そのため、以下のようなことが起こっていました。

  1. テスト実行前にSpring Bootがアプリケーションコンテキストを作成。同時にスキーマも作成される
  2. テストが普通に実行される
  3. @MockBeanが付与されたクラスを実行する前に、Spring Bootがアプリケーションコンテキストを再作成。同時にスキーマも再作成。
  4.  しかしDB(H2)は再起動されず、すでにテーブルも存在するので[Table already exists]エラーが発生する。

回避策としては、

  1. スキーマ生成のコードで例外を無視させる
  2. アプリケーションコンテキストが終了する前にスキーマを削除する
  3. @MockBeanを使わない

という選択肢がありました。

僕はとりあえず3番目で対処しました。方法としては@Mockアノテーションを使い、モックが必要なところで適宜モックオブジェクトをテスト対象のクラスにセットするという方法です。

ただ他の人が知らずに@MockBeanを使ったら問題が再発するので、一番すっきりするのは2番目かもしれません。

その場合、スキーマを生成したBeanに@PreDestroyをつけたメソッドを作成して、そこでスキーマの削除を行えばエラーは発生しなくなりました。便利な機能ですが、あちこちのクラスで@MockBeanを使うと、その度にアプリケーションコンテキストの再生成が行われるので、テストの実行がかなりもっさりします。

テストが存在しないよりははるかにマシですが、Spring Bootを使ったテストはただでさえオーバーヘッドが大きいので使いどころを限定しないとストレスがたまりますね。

-Computer

Copyright© dawaan , 2020 All Rights Reserved.