3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ひとりJUnitAdvent Calendar 2022

Day 7

JUnit + MockitoでDIしているサービスをMock化する

Last updated at Posted at 2022-12-18

はじめに

ひとりJUnitアドベントカレンダー7日目の記事です。
いろいろあって放置しているうちに12月18日になってました。まだ7日目なのに。

使用バージョン

pom.xml
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
$ mvn dependency:tree | grep mockito
[INFO] |  +- org.mockito:mockito-junit-jupiter:jar:3.1.0:test
[INFO] |  +- org.mockito:mockito-core:jar:3.1.0:test

springbootの古さは目を瞑ってください。

テスト対象クラスのロジックだけテストしたい

単体テストなのでテスト対象クラス単体だけテストしたいですね。

SpringbootにはDependency Injectionという仕組みがあります。
DIの説明は他の記事に譲りますが、テスト対象クラスに他のクラスが注入されていると
そのクラスをテストしようとした際、勝手にDIされて他クラスのロジックも通ってしまいます。
(もしくはDIに失敗してぬるぽになります)

注入されている側のロジックは無視、というか挙動を固定しておいて、
テスト対象クラスのロジックだけテストすることができたら、それはもううれしい。
ということで使えるのがMockitoです。

今回は以下のようなクラス構成で考えます。

HogeService.java
@Service
@RequiredArgsConstructor
public class HogeService {

  @NonNull
  private final MogeService mogeService;

  public void execute() {
    System.out.println(mogeService.returnStr());
    mogeService.returnNothing();
    System.out.println("hoge executed.");
  }
}
MogeService.java
@Service
public class MogeService {

  public String returnStr() {
    System.out.println("returnStr executed.");
    return "moge return";
  }

  public void returnNothing() {
    System.out.println("returnNothing executed.");
  }
}

HogeServiceはコンストラクタインジェクションでMogeServiceが注入されていて、
MogeServiceはStringを返すメソッドとvoidのメソッドをそれぞれ持っている形です。

HogeServiceをテストしようとした時、MogeServiceをMockにできれば嬉しいよね、
というのが今回の趣旨です。

手順1:HogeSerivceのテストクラスを作る

さて、まずは普通にテストクラスを作ります。

HogeServiceTest
class HogeServiceTest {

  private HogeService hogeService;

  @Test
  void テスト1() {
    hogeService.execute();
  }
}

ここはただの下準備なので何も言うことはありません。
テストクラスを作ってテスト対象クラスを呼び出してみただけです。
ちなみに変数hogeServiceに何も代入していないため、今のところぬるぽで即落ちます。

手順2:HogeServiceのインスタンスを作る

次はぬるぽを解消しましょう。
hogeServiceにインスタンスを詰めてあげるだけです。

HogeServiceTest
class HogeServiceTest {

  private HogeService hogeService;

  @BeforeEach
  private void setup() {
    hogeService = new HogeService(new MogeService());
  }

  @Test
  void テスト1() {
    hogeService.execute();
  }
}

HogeServiceはアノテーション@RequiredArgsConstructorを付与しているので、
MogeServiceのインスタンスを一つ受け取るコンストラクタをpublicで持っています。

よってnewしたMogeServiceを渡してあげることでインスタンス化できますが、
これではexecute()を呼んだ時にMogeServiceの処理まで動いてしまいますね。

手順3:MogeServiceをmockにする

そこで、@Mockです。

@ExtendWith(MockitoExtension.class)
class HogeServiceTest {

  private HogeService hogeService;

  @Mock
  private MogeService mockedMogeService;

  @BeforeEach
  private void setup() {
    hogeService = new HogeService(mockedMogeService);
  }

  @Test
  void テスト1() {
    hogeService.execute();
  }
}

重要なポイントは以下の3点です。

  • クラスに@ExtendWithMockitoExtention.classを指定する
  • モック化したいクラスに対して@Mockを付与する
  • モック化したクラスをテスト対象クラスにinjectionする

@Mockを付与されたmogeServiceはモックとなるため
hogeService.execute()を実行しても、実際のMogeServiceの処理は呼ばれません。

余談

コンストラクタインジェクションだからこうやって書いてたけど、

hogeService = new HogeService(mockedMogeService);

仮にフィールドインジェクションだったらどうなるの?

public class HogeService {

  @Autowired
  private MogeService mogeService;

そういう場合はアノテーションを使います。
というか、コンストラクタインジェクションでもこれで書けますし、
これを推奨している記事が多いイメージですが、なんとなく手癖が・・・

  @InjectMocks
  private HogeService hogeService;

手順4:Mockの挙動を設定する

実行しても、実際のMogeServiceの処理は呼ばれません。

じゃあ何が呼ばれるんだよという話になるので、
以下の処理を呼んでいるテストの実行結果を見てみます。

HogeService#execute
  public void execute() {
    System.out.println(mogeService.returnStr());
    mogeService.returnNothing();
    System.out.println("hoge executed.");
  }
null
hoge executed.

1行目の通り、mogeServicereturnStr()からの戻り値はnullでした。
returnNothing()も元の処理ではsysoutが行われますが、今回は何も出力されません。
すなわち、戻り値ありのメソッドはnullを返し、voidのメソッドは何もしなくなります。

ぬるぽは起きなくなりました、でもこのままでは意味がありません。
mogeService.returnStr()が空文字を返却するパターンも、
mogeService.returnNothing()が例外を投げるパターンも、
まだ何もテストしていないからです。こんなところで終わるわけにはいかない。

  @Test
  void テスト1() {
    when(mogeService.returnStr()).thenReturn("mogemoge");
    // もしくは
    doReturn("mogemoge").when(mogeService).returnStr();
    hogeService.execute();
  }

  @Test
  void テスト2() {
    doThrow(new NullPointerException()).when(mogeService).returnNothing();
    // 引数なしの場合はwhen()から始めることはできない
    hogeService.execute();
  }

ので、mockの挙動を自分で設定してみます。
使っているwhendoThrowMockitoが提供しているメソッドで、
今回はorg.mockito.Mockito.*をstaticインポートして使用しています。

覚えてしまえば直感的に使えるためわかりやすいですが、
戻り値の有無で書き方が変わることと、mock側メソッドの引数の扱いには注意が必要です。

まず戻り値の有無について。
戻り値があるメソッドはwhen()で囲んだ後、thenReturn()を生やす書き方と
doReturn()から始めてwhen()に繋げる書き方の2パターンが可能です。

戻り値のないvoidのメソッドについては、
先にdoThrow()を書いておいて、そこからwhen().method()と続くのみで、
when()から始めることはできません。

    // whenから始める(戻り値あり)
    when(mogeService.returnStr()).thenReturn("mogemoge");
    // whenから始める(戻り値なし):これはコンパイルエラーになる
    when(mogeService.returnNothing()).thenThrow(new NullPointerException());
    // doXXから始める(戻り値あり)
    doReturn("mogemoge").when(mogeService).returnStr();
    // doXXから始める(戻り値なし)
    doThrow(new NullPointerException()).when(mogeService).returnNothing();

条件→結果の順に読めるので個人的にはwhen()から始める記法が好みですが、
そちらを使うと戻り値の有無で記述が変わるため、まあ一長一短ではあります。

そしてもう一点、mock側メソッドへの引数の扱いですが、
上記例では記載ないものの、仮に以下のような引数を取るメソッドをmock化するとします。

MogeService
@Service
public class MogeService {

  public String returnArg(String arg1, String arg2) {
    return arg1 + arg2;
  }
}

その場合は「どういう引数を受け取った時の挙動なのか」を指定しなければなりません。
以下の例ではany()を指定していますが、
これは「どんな引数が来ても、returnArg()が呼び出されたら」という指定になります。

    when(mogeService.returnArg(any(), any())).thenReturn("mogemoge");

「特定の値がreturnArg()に渡されたら」という指定は以下です。

    when(mogeService.returnArg("hoge", "moge")).thenReturn("bar");
    when(mogeService.returnArg(eq("foo"), eq("bar"))).thenReturn("bar");

any()eq()はMatcherと呼ばれるものです。
上記のように、値をそのまま入れても、eq()で囲んでもどちらでも動きますが
Matcherを使いたい時はMatcherで統一する必要があるため、
混ぜて使うとMockitoに怒られて動きません。

    // これはダメ
    when(mogeService.returnArg("hoge", any())).thenReturn("bar");
    when(mogeService.returnArg("hoge", eq("moge"))).thenReturn("bar");

    // こう書く
    when(mogeService.returnArg(eq("hoge"), any())).thenReturn("bar");
    when(mogeService.returnArg(eq("hoge"), eq("moge"))).thenReturn("bar");

Matcherの有無で記述を変えるのも面倒なので、
常にeq()で囲むくらいの気持ちでいいかなと思っています。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?