💯

【Spring Boot】MockBeanを使った簡単ユニットテスト

2023/12/02に公開1

初めに

実務未経験者がテストコードを書いてくれと言われると、初めはどう書いたらいいかわかりませんよね?少なくとも私はそうでした。私は完全未経験ではなかったのですが、初めてJavaの案件に入り、これまで全く書いてこなかったテストコードを書く場面に当たりました。この記事がJava初心者やJavaやらないけどテストコードが初めてって人にもなんとなくユニットテストの雰囲気を掴んでもらえるように記事を書いていきます。

ユニットテストとは?

ユニットテスト(単体テスト)が何かはネットにいろんな記事が出回っています。(一応1つ参考に
ただ初学者だとその説明を読んでもよくわからんってなる人も大勢いると思います。
要はプログラムの中の小さい範囲を切り出して、その限られた範囲のコードが正しく動作するかを確認するテストってイメージでいいでしょう!わからない人はこの後のコードを見ればなんとなくイメージもついてくると思うので深く考えなくて大丈夫です!

今回の環境

IDEはInteliJ IDEAを使用しています。

  • Spring Boot:3.1.2
  • JUnit5
  • JDK:Amazon corretto17

今回のテスト対象となるコード

今回テストの対象となるコードはService層のコードになります。あまり深く考えなくていいのですが、今回でいうService層とは下図の黄色の部分です。要はリクエストを処理するコントローラーやDBを操作するDAO(Data Access Object)のコードではないよってことだけわかればOKです!

テスト対象のコードは以下になります。

AccountServie.java
package com.example.bulletin.board.service;

import com.example.bulletin.board.dao.AccountDao;
import com.example.bulletin.board.entity.gen.Account;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class AccountService {
    private AccountDao accountDao;

    public AccountService(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    // アカウント名からPKであるアカウントIDを取得
    public int getAccountIdByAccountName(String accountName) {
        // ユーザー名を基にDBからアカウント情報を取得
        Optional<Account> account = accountDao.findByUserName(accountName);
        // アカウントが存在すればアカウントIDを設定し、存在しなければ-1とする
        return account.map(Account::getId).orElse(-1);
    }
}

このクラスにはメソッドは1つしかありません。getAccountIdByAccountNameメソッドが担う役割はデータ操作を担うDaoのメソッドを呼び出して、アカウント名に一致するデータが返ってきたらアカウントIDを返すが、存在しなければ-1を返すというシンプルなものです。

テストコード

以下が私が作成したテストコードの全文になります。少しずつ解説をしてきます。

AccountServiceTest.java
package com.example.bulletin.board.service;

import com.example.bulletin.board.dao.AccountDao;
import com.example.bulletin.board.entity.gen.Account;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.any;

@SpringBootTest
class AccountServiceTest {
    @Autowired
    private AccountService service;

    @MockBean
    private AccountDao accountDao;

    @Test
    void アカウント名から正しいアカウントIDを取得できること() {
        // Arrange
        var account = new Account();
        account.setId(100);
        Optional<Account> optionalAccount = Optional.of(account);
        Mockito.when(accountDao.findByUserName(any()))
                .thenReturn(optionalAccount);
        // Act
        int resultId = service.getAccountIdByAccountName("テストユーザー");

        // Assert
        assertEquals(100, resultId);
    }

    @Test
    void 存在しないアカウント名の場合マイナス1を返すこと() {
        // Arrange
        Optional<Account> optionalAccount = Optional.empty();
        Mockito.when(accountDao.findByUserName(any()))
                .thenReturn(optionalAccount);
        // Act
        int resultId = service.getAccountIdByAccountName("テストユーザー");

        // Assert
        assertEquals(-1, resultId);
    }

}

まずテストケースを考えよう

まずテスト対象のメソッド(AccountServie.javaのgetAccountIdByAccountNameメソッド)を見て、引数によってどのように戻り値が変わるかを考えましょう。今回であれば引数の値(アカウント名)によって、取得したIDが戻り値となる場合とアカウントが見つからず-1が戻り値となる場合が考えられ、その2つのケースを正しく処理できるかをテストしています。より適切にテストケースを考えるためにテスト技法などを学ぶのも良いと思いますが、話がややこしくなってしまうので今回は割愛します(参考Qiita)。

AAAパターン

AAAパターンとはテストコードを読みやすくするためのパターンの1つで、AAAはArrange(準備)、Act(実行)、Assert(確認)の略になります。今回のテストコードでもArrange、Act、Assertをコメントで記載し、区切っています。
簡単に解説すると

  1. Arrangeのブロックでは必要なデータを作ったり、モックを設定したりとテストのための準備をします。
  2. Actのブロックではテストしたいメソッドを実行します。
  3. Assertのブロックでは、Actの結果として正しいテスト結果が得られているのかを確認します。

このようなステップを意識したコードを書くと、テストコードを読むときに非常にわかりやすいですよね。今回のコードはシンプルすぎますが、テストコードが複雑になった場合はより効果を発揮するかと思います!

各アノテーション

各アノテーションについて超ざっくり解説します。イメージだけ掴んだ後、気になったら各自で調べていただくのが良いかと思います!

  • @SpringBootTest
    SpringBootのテストをするときはclassの上に付けておこうくらいに最初は考えましょう。
  • @Autowired
    テスト対象であるAccountServiceをDIするためのアノテーションです。DIの説明などは割愛しますが、AccountServiceをこのテストコードで使えるようにしますといったイメージです。Actのブロックでは実際にAccountServiceのメソッドを呼び出して正しい挙動が得られるかを確認するため必要になります。
  • @MockBean
    このアノテーションを用いることでAccountDaoをモックにすることができます。本来のAccountServiceではDB操作を実行するという役割でAccountDaoが使われますが、このアノテーションを用いることで、モックにすり替えることができます。モックにしないとServiceのテストコードがDaoに依存するようになり、Daoのコードで誤った結果が出力されると、Serviceのコードが何も悪くなくてもServiceのテストが失敗してしまいます。
  • @Test
    JUnitで実行するテストのメソッドにつけると覚えておけばOKです。

2つのテストメソッドがありますが、やっていることはほとんど変わらないので、主にこちらのテストメソッドだけ見ていきます。

    @Test
    void アカウント名から正しいアカウントIDを取得できること() {
        // Arrange
        var account = new Account();
        account.setId(100);
        Optional<Account> optionalAccount = Optional.of(account);
        Mockito.when(accountDao.findByUserName(any()))
                .thenReturn(optionalAccount);
        // Act
        int resultId = service.getAccountIdByAccountName("テストユーザー");

        // Assert
        assertEquals(100, resultId);
    }

テストメソッド名

まずメソッド名に日本語を使用しています。こうするとテスト実行時に下図のように、どのようなテストが実行され、どのテストが失敗したかがわかりやすいですよね。

本当は今回のメソッド名にあるような「正しいアカウント」ってどんなアカウント?ってなるからもう少しわかりやすい名前をつけた方がいいんだろうけど、わかりやすい名前って考え出すとなかなか難しいポイントでもあります。
ちなみに実務では「正常系_アカウント名から正しいアカウントIDを取得できること」のように正常系・異常系を頭につけるようなメソッド名もわかりやすいと教えてもらいました。

Arrangeブロック

大事なのは以下のコードです。

Mockito.when(accountDao.findByUserName(any()))
	.thenReturn(optionalAccount);

このMokitoを使えばモックとなっているaccountDaoのメソッドを実行した時に、どのような値を返すかを決めることができます。この書き方でaccountDaoのfindByUserNameメソッドを実行したら引数がなんであろうと(anyの効果)、optionalAccountを返しますという意味になります。そのoptionalAccountのデータをMokitoの手前で作成しています。

var account = new Account();
account.setId(100);
Optional<Account> optionalAccount = Optional.of(account);

→この記述により、IDとして100がセットされたアカウントがDaoのfindByUserNameメソッドの戻り値となることがわかります。

Actブロック

int resultId = service.getAccountIdByAccountName("テストユーザー");

たったの1行ですが何が起こっているかをよりわかりやすくするために図を用意しました。ServiceTestのメソッド内でServiceのメソッドを呼び出し、その内部にあるDaoメソッドがMock化され決まった値を返すようになっているということですね。

Assertブロック

assertEquals(100, resultId);

先ほどActで返ってきた値(resultId)と予測値100が一致していれば、Serviceは正しい挙動になっていることがわかります。実測値と予測値が一致しているかどうか、正しい挙動をしているかどうかを確かめるためにassert〜というメソッドがよく用いられます。assertEqualsは2つの値が一致しているかを確認するだけの非常にわかりやすいメソッドです。実測値と予測値が異なる場合はassertEqualsでエラーを吐きテストは失敗します。
例えば、上のコードを以下のように100→20に書き換えると下図のようにテストは失敗します。

assertEquals(20, resultId);

もう1つのテストメソッド

「存在しないアカウント名の場合マイナス1を返すこと() 」というもう1つのテストメソッドでは以下のようにオプショナルが空になるようにして、そのオブジェクトをMockでDaoのメソッドから返すようにしています。

Optional<Account> optionalAccount = Optional.empty();

これによってDaoのメソッドからアカウント情報が取得できない場合はserviceメソッドの戻り値が-1になるという挙動が正しく行われるかをテストすることができます。
それ以外はこれまで解説した内容と同様です。

まとめ

説明を省いた部分も多くありますが、なんとなくJUnitを用いたユニットテストのイメージは掴めたでしょうか?
私もJavaやテストコードの経験はまだまだ浅いので、何か気づいた点があれば教えていただけると助かります!

Discussion

yasunayasuna

AAAパターン初めて知りました!このパターンで解説していただいたおかげでテストコードの意図がとても理解しやすかったです💡