かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( 番外編 )( Spring Boot + JMockit でテスト対象クラスの @Autowired で DI しているフィールドの一部をモックにする )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その29 )( 貸出状況取得タスクの作成3 ) でテスト対象クラス InquiringStatusOfBookQueueListener 内のフィールド calilApiService だけをモックにしたいと思い、JMockit で対応する方法を調査したので、その内容を記載します。

前回1週間以上更新できなかったのはこれを調査していたためでした。JMockit を使う場合に Spring Boot でどうすればよいのか全然分からない時があるんですよね。。。

参照したサイト・書籍

  1. JMockit - The JMockit Testing Toolkit Tutorial
    http://jmockit.org/tutorial.html

目次

  1. フィールドをモックにしたいので JMockit の @Tested + @Injectable アノテーションを試してみる
  2. @Injectable アノテーションを付加して DI するインスタンスを @Autowired の時と同じにしたいので @Injectable @Autowired アノテーションを付加してみる
  3. mail001Helper.createMessage が正常に動作していない理由とは?
  4. getMockInstance() と Deencapsulation.setField() で必要なフィールドだけモックに入れ替えてみる
  5. モックに入れ替えたフィールドをテストメソッド終了後に元に戻すには?
  6. テストの時にモックにしそうな部分は private メソッドにしておけばよいのでは?
  7. まとめ

説明

フィールドをモックにしたいので JMockit の @Tested + @Injectable アノテーションを試してみる

最初に状況を確認できるようにするためにテスト対象クラス InquiringStatusOfBookQueueListener の receiveMessage メソッド内に System.out.println でログを出力する処理を入れておきます。"★★★ " が記述されているところが追加したところです。

    @RabbitListener(queues = {Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK})
    public void receiveMessage(Message message) throws MessagingException {
        // 受信したメッセージを InquiringStatusOfBookQueueMessage クラスのインスタンスに変換する
        InquiringStatusOfBookQueueMessage convertedMessage
                = inquiringStatusOfBookQueueService.convertMessageToObject(message);
        System.out.println("★★★ " + convertedMessage.getLendingAppId());
        
        // 選択中の図書館を取得する
        LibraryForsearch libraryForsearch = libraryForsearchDao.selectSelectedLibrary();
        System.out.println("★★★ " + libraryForsearch.getSystemid() + ", " + libraryForsearch.getFormal());

        // 更新対象の lending_app テーブルのデータを取得する
        LendingApp lendingApp = lendingAppDao.selectById(convertedMessage.getLendingAppId(), SelectOptions.get().forUpdate());
        if (lendingApp == null) {
            logger.error("lending_app テーブルに対象のデータがありませんでした ( lending_app_id = {} )。", convertedMessage.getLendingAppId());
            return;
        }
        System.out.println("★★★ " + lendingApp.getLendingAppId());
        
        // lending_book テーブルから調査対象の ISBN 一覧を取得する
        List<LendingBook> lendingBookList
                = lendingBookDao.selectByLendingAppId(convertedMessage.getLendingAppId(), SelectOptions.get().forUpdate());
        if (lendingBookList == null) {
            logger.error("lending_book テーブルに対象のデータがありませんでした ( lending_app_id = {} )。", convertedMessage.getLendingAppId());
            return;
        }
        List<String> isbnList = lendingBookList.stream()
                .map(LendingBook::getIsbn)
                .collect(Collectors.toList());
        isbnList.stream()
                .forEach(s -> System.out.println("★★★ " + s));
        
        // カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する
        List<Book> bookList = calilApiService.check(libraryForsearch.getSystemid(), isbnList);
        bookList.stream()
                .forEach(s -> System.out.println("★★★ " + s.getIsbn()));

        // lending_book テーブルに取得した貸出状況を反映し、lending_app テーブルの status を 2(未申請) に更新する
        copyLendingStateFromBookListToEntityList(bookList, lendingBookList);
        updateLendingData(lendingBookList, lendingApp);

        // データを登録したユーザへメールを送信する
        UserInfo userInfo = userInfoDao.selectById(lendingApp.getLendingUserId());
        System.out.println("★★★ " + userInfo.getMailAddress());
        MimeMessage mimeMessage = mail001Helper.createMessage(userInfo.getMailAddress(), convertedMessage.getLendingAppId());
        System.out.println("★★★ " + mimeMessage.getRecipients(javax.mail.Message.RecipientType.TO));
        emailService.sendMail(mimeMessage);
    }

テストクラスを以下のように実装します。

  • private InquiringStatusOfBookQueueListener listener; に付加するアノテーションを @Autowired ではなく @Tested にします。
  • フィールド calilApiService にモッククラスを DI したいので @Injectable private CalilApiService calilApiService; を定義します。
  • calilApiService.check メソッドをモックにしたいので、テストメソッド testReceiveMessage 内に new NonStrictExpectations() {{ ... }}; でモックメソッドを記述します。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class InquiringStatusOfBookQueueListenerTest {

    ..........

    @Tested private InquiringStatusOfBookQueueListener listener;
    @Injectable private CalilApiService calilApiService;

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        /**
         * モック定義部
         */
        new NonStrictExpectations() {{
            calilApiService.check(anyString, (List<String>) any);
            result = new Delegate() {
                List<Book> aDelegateMethod() {
                    List<Book> bookList = new ArrayList<>();
                    bookList.add(new Book("978-4-7741-6366-6", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出可")))));
                    bookList.add(new Book("978-4-7741-5377-3", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書あり")))));
                    bookList.add(new Book("978-4-7973-8014-9", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出中")))));
                    bookList.add(new Book("978-4-7973-4778-4", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "準備中")))));
                    bookList.add(new Book("978-4-87311-704-1", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書なし")))));
                    return bookList;
                }
            };
        }};

        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);

        /**
         * 検証
         */
        ..........
    }

}

テストメソッド testReceiveMessage を実行すると java.lang.IllegalStateException: Missing @Injectable for field InquiringStatusOfBookQueueListener#inquiringStatusOfBookQueueService, of type ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueService のエラーメッセージが出力されました。どうも @Autowired を付加して定義しているフィールド全てについて @Injectable を付加したクラスを定義する必要があるようです。

f:id:ksby:20151107204910p:plain

テストクラスを以下のように変更します。

  • テスト対象クラス InquiringStatusOfBookQueueListener 内に @Autowired を付加して定義している全てのフィールドに対応する @Injectable private ... の定義を追加します。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class InquiringStatusOfBookQueueListenerTest {

    ..........

    @Tested private InquiringStatusOfBookQueueListener listener;
    @Injectable private InquiringStatusOfBookQueueService inquiringStatusOfBookQueueService;
    @Injectable private CalilApiService calilApiService;
    @Injectable private EmailService emailService;
    @Injectable private Mail001Helper mail001Helper;
    @Injectable private LibraryForsearchDao libraryForsearchDao;
    @Injectable private LendingAppDao lendingAppDao;
    @Injectable private LendingBookDao lendingBookDao;
    @Injectable private UserInfoDao userInfoDao;

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        /**
         * モック定義部
         */
        new NonStrictExpectations() {{
            calilApiService.check(anyString, (List<String>) any);
            result = new Delegate() {
                List<Book> aDelegateMethod() {
                    List<Book> bookList = new ArrayList<>();
                    bookList.add(new Book("978-4-7741-6366-6", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出可")))));
                    bookList.add(new Book("978-4-7741-5377-3", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書あり")))));
                    bookList.add(new Book("978-4-7973-8014-9", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出中")))));
                    bookList.add(new Book("978-4-7973-4778-4", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "準備中")))));
                    bookList.add(new Book("978-4-87311-704-1", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書なし")))));
                    return bookList;
                }
            };
        }};

        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);

        /**
         * 検証
         */
        ..........
    }

}

テストメソッド testReceiveMessage を実行すると今度は検証でエラーが出るところまで進みますが、一番最初の convertedMessage.getLendingAppId() の出力が 1 ではなく 0 になっていました。フィールド inquiringStatusOfBookQueueService が モッククラスなのが原因のようです。

f:id:ksby:20151107210904p:plain

@Injectable private CalilApiService calilApiService; 以外は実際に @Autowired を付加して DI された時と同じインスタンスを使用したいのですが、どうすればよいのかがなかなか分かりませんでした。。。

@Injectable アノテーションを付加して DI するインスタンスを @Autowired の時と同じにしたいので @Injectable @Autowired アノテーションを付加してみる

JMockit の Tutorial を見たり Google で検索してもさっぱり分からなかったので、テストクラスを以下のように変更してみました。

  • 単純に @Autowired アノテーションを付加してみます。@Injectable private CalilApiService calilApiService; 以外は @Injectable@Injectable @Autowired へ変更します。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class InquiringStatusOfBookQueueListenerTest {

    ..........

    @Tested private InquiringStatusOfBookQueueListener listener;
    @Injectable @Autowired private InquiringStatusOfBookQueueService inquiringStatusOfBookQueueService;
    @Injectable private CalilApiService calilApiService;
    @Injectable @Autowired private EmailService emailService;
    @Injectable @Autowired private Mail001Helper mail001Helper;
    @Injectable @Autowired private LibraryForsearchDao libraryForsearchDao;
    @Injectable @Autowired private LendingAppDao lendingAppDao;
    @Injectable @Autowired private LendingBookDao lendingBookDao;
    @Injectable @Autowired private UserInfoDao userInfoDao;

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        /**
         * モック定義部
         */
        new NonStrictExpectations() {{
            calilApiService.check(anyString, (List<String>) any);
            result = new Delegate() {
                List<Book> aDelegateMethod() {
                    List<Book> bookList = new ArrayList<>();
                    bookList.add(new Book("978-4-7741-6366-6", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出可")))));
                    bookList.add(new Book("978-4-7741-5377-3", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書あり")))));
                    bookList.add(new Book("978-4-7973-8014-9", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出中")))));
                    bookList.add(new Book("978-4-7973-4778-4", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "準備中")))));
                    bookList.add(new Book("978-4-87311-704-1", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書なし")))));
                    return bookList;
                }
            };
        }};

        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);

        /**
         * 検証
         */
        ..........
    }

}

テストメソッド testReceiveMessage を実行すると今度も検証でエラーが出るところまで進みますが、一番最初の convertedMessage.getLendingAppId() の出力は 1 になりました。`@Injectable @Autowired を付加することで @Autowired を付加した時と同じインスタンスが DI されるようです。

ただし今度は一番最後の mimeMessage.getRecipients(javax.mail.Message.RecipientType.TO) だけが何も出力されていませんでした。mail001Helper.createMessage が正常に動作していないようです。なぜこれだけ動作していないのでしょうか。。。

f:id:ksby:20151107213025p:plain

mail001Helper.createMessage が正常に動作していない理由とは?

実際に mail001Helper.createMessage が呼びだされているのかを確認します。mail001Helper.createMessage メソッドに System.out.println でログを出力するよう変更します。"★★★ " が記述されているところが追加したところです。

@Component
public class Mail001Helper {

    ..........

    public MimeMessage createMessage(String toAddr, Long lendingAppId) throws MessagingException {
        System.out.println("★★★ Mail001Helper.createMessage");
        MimeMessage mimeMessage = this.mailSender.createMimeMessage();
        MimeMessageHelper message = new MimeMessageHelper(mimeMessage, false, "UTF-8");
        message.setFrom(FROM_ADDR);
        message.setTo(toAddr);
        message.setSubject(SUBJECT);
        message.setText(generateTextUsingVelocity(lendingAppId), false);
        return message.getMimeMessage();
    }

テストメソッド testReceiveMessage を実行しても上で入れたログは出力されませんでした。mail001Helper はおそらくモックになっているようです。

f:id:ksby:20151107231536p:plain

InquiringStatusOfBookQueueService.convertMessageToObject にも同じようにログを入れてみたのですが、そちらは出力されました。何が違うのでしょうか。。。

この時はいろいろ試行錯誤して、テスト対象クラス InquiringStatusOfBookQueueListener の中でフィールドを定義する時にクラスではなくインターフェースにすれば動くようになるということが分かりました。以下のように変更するとテストメソッド testReceiveMessage が正常に終了するようになります。

src/main/java/ksbysample/webapp/lending/helper/mail の下に MsgHelper インターフェースを新規作成します。

package ksbysample.webapp.lending.helper.mail;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

public interface MailHelper {

    public MimeMessage createMessage(String toAddr, Long lendingAppId) throws MessagingException;

}

src/main/java/ksbysample/webapp/lending/helper/mail の下の Mail001Helper.java を MailHelper インターフェースを実装するように変更します。

  • public class Mail001Helper の後に implements MailHelper を追加します。
  • createMessage メソッドに @Override を付加します。
@Component
public class Mail001Helper implements MailHelper {

    ..........

    @Override
    public MimeMessage createMessage(String toAddr, Long lendingAppId) throws MessagingException {
        ..........
    }

    ..........

}

src/main/java/ksbysample/webapp/lending/listener/rabbitmq の下の InquiringStatusOfBookQueueListener.javaprivate Mail001Helper mail001Helper;private MailHelper mail001Helper; へ変更します。

@Component
public class InquiringStatusOfBookQueueListener {

    ..........

    @Autowired
    private MailHelper mail001Helper;

テストクラスを以下のように変更します。

  • @Injectable @Autowired private Mail001Helper mail001Helper;@Injectable @Autowired private MailHelper mail001Helper; へ変更します。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class InquiringStatusOfBookQueueListenerTest {

    ..........

    @Tested private InquiringStatusOfBookQueueListener listener;
    @Injectable @Autowired private InquiringStatusOfBookQueueService inquiringStatusOfBookQueueService;
    @Injectable private CalilApiService calilApiService;
    @Injectable @Autowired private EmailService emailService;
    @Injectable @Autowired private MailHelper mail001Helper;
    @Injectable @Autowired private LibraryForsearchDao libraryForsearchDao;
    @Injectable @Autowired private LendingAppDao lendingAppDao;
    @Injectable @Autowired private LendingBookDao lendingBookDao;
    @Injectable @Autowired private UserInfoDao userInfoDao;

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        /**
         * モック定義部
         */
        new NonStrictExpectations() {{
            calilApiService.check(anyString, (List<String>) any);
            result = new Delegate() {
                List<Book> aDelegateMethod() {
                    List<Book> bookList = new ArrayList<>();
                    bookList.add(new Book("978-4-7741-6366-6", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出可")))));
                    bookList.add(new Book("978-4-7741-5377-3", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書あり")))));
                    bookList.add(new Book("978-4-7973-8014-9", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出中")))));
                    bookList.add(new Book("978-4-7973-4778-4", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "準備中")))));
                    bookList.add(new Book("978-4-87311-704-1", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書なし")))));
                    return bookList;
                }
            };
        }};

        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);

        /**
         * 検証
         */
        ..........
    }

}

テストメソッド testReceiveMessage を実行すると検証まで正常に終了するようになります。

f:id:ksby:20151108041930p:plain

なぜ実クラスでは動作せず、インターフェースにすると動作するようになるのかの理由までは分かりませんでした。

テストが完了するようになるのですが、いちいちインターフェースを定義したくはなかったのでこの方法は採用せず他の方法を探すことにします。上のインターフェースの実装、及び mail001Helper.createMessage に入れた System.out.println の処理を元に戻します。

getMockInstance() と Deencapsulation.setField() で必要なフィールドだけモックに入れ替えてみる

この時点でちょっとお手上げになったので、The JMockit Testing Toolkit Tutorial を読みなおして見ました。使えそうと思ったのは以下の2点です。

この2つを利用して、モックにしたいクラスのオブジェクトを生成 → Deencapsulation.setField でセット、してみます。

テストクラスを以下のように変更します。

  • テスト対象クラス InquiringStatusOfBookQueueListener の定義は @Autowired にします。
  • テストメソッド内で .getMockInstance()、Deencapsulation.setField で calilApiService のモックを生成してセットします。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class InquiringStatusOfBookQueueListenerTest {

    ..........

    @Autowired
    private InquiringStatusOfBookQueueListener listener;

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        /**
         * モック定義部
         */
        CalilApiService calilApiService = new MockUp<CalilApiService>() {
            @Mock
            List<Book> check(String systemid, List<String> isbnList) {
                List<Book> bookList = new ArrayList<>();
                bookList.add(new Book("978-4-7741-6366-6", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出可")))));
                bookList.add(new Book("978-4-7741-5377-3", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書あり")))));
                bookList.add(new Book("978-4-7973-8014-9", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出中")))));
                bookList.add(new Book("978-4-7973-4778-4", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "準備中")))));
                bookList.add(new Book("978-4-87311-704-1", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書なし")))));
                return bookList;
            }
        }.getMockInstance();
        setField(listener, "calilApiService", calilApiService);

        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);

        /**
         * 検証
         */
        ..........
    }

}

テストメソッド testReceiveMessage を実行すると検証まで正常に終了しました。

f:id:ksby:20151108095716p:plain

ただしこの方法だと Spring の DI コンテナに生成されているオブジェクトを変更してしまい、他のテストメソッドではモックに入れ替わったままになるのでは? と思ったので、確認してみることにします。

テストクラスを以下のように変更します。

  • モックに入れ替えたテストを実行してから、入れ替えずに実施する想定のテストを実行したいので、テストメソッド名順にテストが実行されるようクラスに @FixMethodOrder(MethodSorters.NAME_ASCENDING) を付加します。
  • テストメソッド xtestReceiveMessage を追加します。testReceiveMessage の先頭に "x" を追加して testReceiveMessage テストメソッドの後に実行されるようにし、内部には testReceiveMessage のテスト本体のみ記述します。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class InquiringStatusOfBookQueueListenerTest {

    ..........

    @Autowired
    private InquiringStatusOfBookQueueListener listener;

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        /**
         * モック定義部
         */
        CalilApiService calilApiService = new MockUp<CalilApiService>() {
            @Mock
            List<Book> check(String systemid, List<String> isbnList) {
                List<Book> bookList = new ArrayList<>();
                bookList.add(new Book("978-4-7741-6366-6", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出可")))));
                bookList.add(new Book("978-4-7741-5377-3", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書あり")))));
                bookList.add(new Book("978-4-7973-8014-9", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出中")))));
                bookList.add(new Book("978-4-7973-4778-4", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "準備中")))));
                bookList.add(new Book("978-4-87311-704-1", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書なし")))));
                return bookList;
            }
        }.getMockInstance();
        setField(listener, "calilApiService", calilApiService);

        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);

        /**
         * 検証
         */
        ..........
    }

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void xtestReceiveMessage() throws Exception {
        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);
    }

}

まずは xtestReceiveMessage のみ実行し成功することを確認します。

f:id:ksby:20151108125014p:plain

次にテストクラス内のテストメソッドを全て実行してみます。

f:id:ksby:20151108125452p:plain

testReceiveMessage → xtestReceiveMessage の順で実行されており、今度は xtestReceiveMessage はエラーになりました。やはり testReceiveMessage テストメソッド終了後に入れ替えたモックが自動で元に戻るわけではないようです。

testReceiveMessage テストメソッド終了後にモックに入れ替えたフィールドのオブジェクトを元に戻す方法を検討します。

モックに入れ替えたフィールドをテストメソッド終了後に元に戻すには?

Deencapsulation クラスで setField メソッドが提供されているならば getField メソッドも提供されているのでは?と思い、調べてみると存在しました。元のオブジェクトを取得する方法が見つかりましたので、テストクラスを以下のように変更します。

  • テストメソッド内でモックに入れ替える前に getField でオリジナルのオブジェクトを取得します。
  • モックの入替~テスト本体~検証までを try-finally で囲み、finally 内で取得しておいたオリジナルのオブジェクトに戻すようにします。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class InquiringStatusOfBookQueueListenerTest {

    ..........

    @Autowired
    private InquiringStatusOfBookQueueListener listener;

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        CalilApiService calilApiServiceOrg = getField(listener, "calilApiService");
        try {
            /**
             * モック定義部
             */
            CalilApiService calilApiService = new MockUp<CalilApiService>() {
                @Mock
                List<Book> check(String systemid, List<String> isbnList) {
                    List<Book> bookList = new ArrayList<>();
                    bookList.add(new Book("978-4-7741-6366-6", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出可")))));
                    bookList.add(new Book("978-4-7741-5377-3", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書あり")))));
                    bookList.add(new Book("978-4-7973-8014-9", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出中")))));
                    bookList.add(new Book("978-4-7973-4778-4", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "準備中")))));
                    bookList.add(new Book("978-4-87311-704-1", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書なし")))));
                    return bookList;
                }
            }.getMockInstance();
            setField(listener, "calilApiService", calilApiService);

            /**
             * テスト本体
             */
            InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
            queueMessage.setLendingAppId(1L);
            Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
            listener.receiveMessage(message);

            /**
             * 検証
             */
            ..........
        } finally {
            setField(listener, "calilApiService", calilApiServiceOrg);
        }
    }

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void xtestReceiveMessage() throws Exception {
        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);
    }

}

テストクラス内のテストメソッドを全て実行してみると、全て成功しました。

f:id:ksby:20151108150028p:plain

テストの時にモックにしそうな部分は private メソッドにしておけばよいのでは?

テストが成功して他のテストにも影響がない方法を見つけられましたが、そもそもテストでモックにしそうな部分はフィールドのオブジェクトをそのまま使用せずに private メソッドにしておけばモック化も楽なのではないかと思いました。ちょっと試してみます。

src/main/java/ksbysample/webapp/lending/listener/rabbitmq の下の InquiringStatusOfBookQueueListener.java を以下のように変更します。

  • private List<Book> checkStatusOfBook(String systemid, List<String> isbnList) メソッドを追加します。
  • receiveMessage 内の calilApiService.check を直接呼び出していた部分を calilApiService.check(...)checkStatusOfBook(...) へ変更します。
@Component
public class InquiringStatusOfBookQueueListener {

    ..........
    
    @RabbitListener(queues = {Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK})
    public void receiveMessage(Message message) throws MessagingException {
        ..........
        
        // カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する
        List<Book> bookList = checkStatusOfBook(libraryForsearch.getSystemid(), isbnList);
        bookList.stream()
                .forEach(s -> System.out.println("★★★ " + s.getIsbn()));

        ..........
    }

    private List<Book> checkStatusOfBook(String systemid, List<String> isbnList) {
        return calilApiService.check(systemid, isbnList);
    }
    
    ..........
    
}

テストクラスを以下のように変更します。この方法なら private メソッドをモックメソッドに置き換えてもテストメソッド終了後に元に戻す必要はありません。

  • try-finally は削除し、フィールドのオリジナルのオブジェクトを取得、復帰していた処理も削除します。
  • testReceiveMessage テストメソッドでは、new MockUp<InquiringStatusOfBookQueueListener>() { ... }; の中で checkStatusOfBook メソッドのモックメソッドを定義します。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class InquiringStatusOfBookQueueListenerTest {

    ..........

    @Autowired
    private InquiringStatusOfBookQueueListener listener;

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        /**
         * モック定義部
         */
        new MockUp<InquiringStatusOfBookQueueListener>() {
            @Mock
            List<Book> checkStatusOfBook(String systemid, List<String> isbnList) {
                List<Book> bookList = new ArrayList<>();
                bookList.add(new Book("978-4-7741-6366-6", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出可")))));
                bookList.add(new Book("978-4-7741-5377-3", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書あり")))));
                bookList.add(new Book("978-4-7973-8014-9", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出中")))));
                bookList.add(new Book("978-4-7973-4778-4", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "準備中")))));
                bookList.add(new Book("978-4-87311-704-1", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書なし")))));
                return bookList;
            }
        };

        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);

        /**
         * 検証
         */
        ..........
    }

    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/testdata/001")
    public void xtestReceiveMessage() throws Exception {
        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);
    }

}

テストクラス内のテストメソッドを全て実行してみると、全て成功しました。

f:id:ksby:20151108152942p:plain

まとめ

  • テストの時にモックにしたい処理は @Autowired で DI したフィールドのオブジェクトを直接呼び出さず、内部メソッド経由で利用した方がよいです。テストの時は内部メソッドをモックメソッドに置き換えます。
  • 内部メソッド経由にできない場合には MockUp.getMockInstance でモックメソッドを定義してモックオブジェクトを取得し、Deencapsulation.setField でフィールドのオブジェクトをモックに置き換えます。ただしテストメソッド開始前に置き換える前の実体オブジェクトを取得しておき、テストメソッド終了時に元に戻す必要があります。
  • JMockit の Tutorial には @Tested + @Injectable の例が書かれていますが、正直使い勝手が悪いので使わなくてよいのではないでしょうか。。。 Spring Boot のアプリケーションでテストする時には MockUp によるモックの定義と利用の仕方を覚えておけばよいような気がします。

履歴

2015/11/09
初版発行。