かんがるーさんの日記

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

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その16 )( テストクラスのモックを @MockBean + Mockito で作り直す )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その15 )( テストクラスのアノテーションを 1.4 のものに変更する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 現在テストクラスでモックを使いたい場合 JMockit を利用して作成していますが、Spring Boot 1.4 で提供されている @MockBean アノテーションで作り直してみます。

参照したサイト・書籍

  1. Spring Boot Reference Guide - 40.3.4 Mocking and spying beans
    http://docs.spring.io/spring-boot/docs/1.4.x/reference/htmlsingle/#boot-features-testing-spring-boot-applications-mocking-beans

  2. Mockito
    http://site.mockito.org/

  3. Mocking static methods with Mockito
    http://stackoverflow.com/questions/21105403/mocking-static-methods-with-mockito

  4. How can I mock a private static method with PowerMockito?
    http://stackoverflow.com/questions/31796736/how-can-i-mock-a-private-static-method-with-powermockito

  5. powermock/powermock - MockitoUsage
    https://github.com/powermock/powermock/wiki/MockitoUsage

  6. Can Mockito stub a method without regard to the argument?
    http://stackoverflow.com/questions/5969630/can-mockito-stub-a-method-without-regard-to-the-argument

目次

  1. @MockBean アノテーションは Mockito を使うが、バージョンは 2 ではなく 1 らしい
  2. 変更手順を考える
  3. build.gradle を修正する
  4. clean タスク → Rebuild Project を実行して修正対象のソースを洗い出す
  5. LibraryHelperTest.java を修正する
  6. BooklistServiceTest.java を修正する
  7. InquiringStatusOfBookQueueListenerTest.java を修正する
  8. 全てのテストを実行してみる
  9. 感想&次回は。。。

手順

@MockBean アノテーションは Mockito を使うが、バージョンは 2 ではなく 1 らしい

40.3.4 Mocking and spying beans を読むと @MockBean は Mockito を利用してモックを作成するようです。

Mockito のホームページ を見ると “Current version is 2” と記述されており、jcenter で検索すると最新バージョンは 2.7.20 と表示されるのですが、Spring Boot Reference Guide の Appendix F. Dependency versions を見ると mockito-core のバージョンは 1.10.19 が記述されており、Athens-SR3 の Spring IO Platform Reference Guide の Appendix A. Dependency versions でも 1.10.19 でした。

Mockito のバージョン 2 系は使用できないのかな?と思って調べると、Spring Boot の以下の Issue が見つかりました。2 系が使えるのは 1.5 以降のようです。今回は 1 系を使うことにします。

変更手順を考える

以下の手順で変更していきます。

  • build.gradle を編集して JMockit を外して Mockito を追加します。
  • clean タスク → Rebuild Project を実行してエラーが出る箇所 ( JMockit を利用している箇所 ) を洗い出します。
  • 一旦 build.gradle に JMockit を戻します。
  • エラーが出た箇所を @MockBean + Mockito で書き直します。
  • build.gradle から JMockit を外します。
  • 最後に全てのテストを通しで実行して成功することを確認します。

build.gradle を修正する

  1. build.gradle を リンク先のその1内容 に変更します。

  2. 変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

clean タスク → Rebuild Project を実行して修正対象のソースを洗い出す

clean タスク → Rebuild Project を実行すると以下のソースでエラーが出ました。

  • src/test/java/ksbysample/webapp/lending/helper/library/LibraryHelperTest.java
  • src/test/java/ksbysample/webapp/lending/web/booklist/BooklistServiceTest.java
  • src/test/java/ksbysample/webapp/lending/listener/rabbitmq/InquiringStatusOfBookQueueListenerTest.java

1つずつ見ていきます。

build.gradle は testCompile("org.jmockit:jmockit:1.30")コメントアウトを一旦元に戻して JMockit が入っている状態にします。

LibraryHelperTest.java を修正する

テストクラスは JMockit を使用して以下のように実装されています。

package ksbysample.webapp.lending.helper.library;

import ksbysample.webapp.lending.dao.LibraryForsearchDao;
import ksbysample.webapp.lending.entity.LibraryForsearch;
import mockit.Delegate;
import mockit.Expectations;
import mockit.Injectable;
import mockit.Tested;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class LibraryHelperTest {

    @Tested
    private LibraryHelper libraryHelper;

    @Injectable
    private LibraryForsearchDao libraryForsearchDao;

    @Test
    public void testGetSelectedLibrary_図書館が選択されていない場合() throws Exception {
        new Expectations() {{
            libraryForsearchDao.selectSelectedLibrary();
            result = null;
        }};

        String result = libraryHelper.getSelectedLibrary();
        assertThat(result).isEqualTo("※図書館が選択されていません");
    }

    @Test
    public void testGetSelectedLibrary_図書館が選択されている場合() throws Exception {
        new Expectations() {{
            libraryForsearchDao.selectSelectedLibrary();
            result = new Delegate<LibraryForsearch>() {
                LibraryForsearch aDelegateMethod() {
                    LibraryForsearch libraryForsearch = new LibraryForsearch();
                    libraryForsearch.setSystemid("System_Id");
                    libraryForsearch.setFormal("図書館名");
                    return libraryForsearch;
                }
            };
        }};

        String result = libraryHelper.getSelectedLibrary();
        assertThat(result).isEqualTo("選択中:図書館名");
    }

}

これを @MockBean アノテーションを使用して以下のように変更します。

package ksbysample.webapp.lending.helper.library;

import ksbysample.webapp.lending.dao.LibraryForsearchDao;
import ksbysample.webapp.lending.entity.LibraryForsearch;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

@RunWith(SpringRunner.class)
@SpringBootTest
public class LibraryHelperTest {

    @Autowired
    private LibraryHelper libraryHelper;

    @MockBean
    private LibraryForsearchDao libraryForsearchDao;

    @Test
    public void testGetSelectedLibrary_図書館が選択されていない場合() throws Exception {
        given(libraryForsearchDao.selectSelectedLibrary()).willReturn(null);

        String result = libraryHelper.getSelectedLibrary();
        assertThat(result).isEqualTo("※図書館が選択されていません");
    }

    @Test
    public void testGetSelectedLibrary_図書館が選択されている場合() throws Exception {
        LibraryForsearch libraryForsearch = new LibraryForsearch();
        libraryForsearch.setSystemid("System_Id");
        libraryForsearch.setFormal("図書館名");
        given(libraryForsearchDao.selectSelectedLibrary()).willReturn(libraryForsearch);

        String result = libraryHelper.getSelectedLibrary();
        assertThat(result).isEqualTo("選択中:図書館名");
    }

}
  • private LibraryHelper libraryHelper;アノテーション@Tested@Autowired へ変更します。
  • private LibraryForsearchDao libraryForsearchDao;アノテーション@Injectable@MockBean へ変更します。
  • testGetSelectedLibrary_図書館が選択されていない場合() テストメソッド内のモック化の処理を new Expectations() {{ ... }};given(libraryForsearchDao.selectSelectedLibrary()).willReturn(null); へ変更します。
  • testGetSelectedLibrary_図書館が選択されている場合() テストメソッド内のモック化の処理を new Expectations() {{ ... }};given(libraryForsearchDao.selectSelectedLibrary()).willReturn(libraryForsearch); へ変更します。

LibraryHelperTest クラスのテストのみ実行してみると、全てのテストが成功しました。

f:id:ksby:20170401091222p:plain

BooklistServiceTest.java を修正する

テストクラスは JMockit を使用して以下のように実装されています。

package ksbysample.webapp.lending.web.booklist;

import ksbysample.common.test.rule.db.TableDataAssert;
import ksbysample.common.test.rule.db.TestDataResource;
import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.security.LendingUserDetailsHelper;
import ksbysample.webapp.lending.service.file.BooklistCsvFileServiceTest;
import mockit.Expectations;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.sql.DataSource;
import java.io.File;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class BooklistServiceTest {

    private static final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com";

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private BooklistService booklistService;

    @Test
    public void testTemporarySaveBookListCsvFile() throws Exception {
        new Expectations(LendingUserDetailsHelper.class) {{
            LendingUserDetailsHelper.getLoginUserId();
            result = Long.valueOf(1L);
        }};

        UploadBooklistForm uploadBooklistForm = new UploadBooklistForm();
        // テスト用のユーティリティクラスを作るべきですが、今回は他のテストクラスのメソッドをそのまま使います
        BooklistCsvFileServiceTest booklistCsvFileServiceTest = new BooklistCsvFileServiceTest();
        uploadBooklistForm.setFileupload(booklistCsvFileServiceTest.createNoErrorCsvFile());

        Long lendingAppId = booklistService.temporarySaveBookListCsvFile(uploadBooklistForm);
        assertThat(lendingAppId).isNotEqualTo(Long.valueOf(0L));
        IDataSet dataSet = new CsvDataSet(
                new File("src/test/resources/ksbysample/webapp/lending/web/booklist/assertdata/001"));
        TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
        tableDataAssert.assertEquals("lending_app"
                , new String[]{"lending_app_id", "approval_user_id", "version"});
        tableDataAssert.assertEquals("lending_book"
                , new String[]{"lending_book_id", "lending_app_id", "lending_state", "lending_app_flg"
                        , "lending_app_reason", "approval_result", "approval_reason", "version"});

        List<LendingBook> lendingBookList = booklistService.getLendingBookList(lendingAppId);
        assertThat(lendingBookList).hasSize(5);
    }

    @Test
    public void testSendMessageToInquiringStatusOfBookQueue() throws Exception {
        // 現在の実装では InquiringStatusOfBookQueueServiceTest が通ればOKなので、こちらは実装しない
    }

}

よく見たらモックにしている LendingUserDetailsHelper.getLoginUserId(); は static メソッドでした。調べてみると Mockito では static メソッドはモックにできないので、対応するとすれば以下のいずれかの方法になるようです。

  • PowerMock を使用する。
  • LendingUserDetailsHelper に @Component アノテーションを付加して Bean にし、LendingUserDetailsHelper#getLoginUserId メソッドから static を取り除く。

Helper クラスなのに static メソッドにしていたのか。。。ということに気付いたので、出来れば後者の方法にしてしまいたいところですが、PowerMock を使ってみたいので、前者の方法で対応してみます。

まず build.gradle をリンク先のその2内容 に変更し、変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

BooklistServiceTest.java を以下のように変更します。

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@SpringBootTest
@PrepareForTest(LendingUserDetailsHelper.class)
public class BooklistServiceTest {

    ..........

    @Test
    public void testTemporarySaveBookListCsvFile() throws Exception {
        PowerMockito.mockStatic(LendingUserDetailsHelper.class);
        given(LendingUserDetailsHelper.getLoginUserId()).willReturn(1L);

        UploadBooklistForm uploadBooklistForm = new UploadBooklistForm();
  • @RunWith(SpringRunner.class)@RunWith(PowerMockRunner.class) + @PowerMockRunnerDelegate(SpringRunner.class) へ変更します。
  • @PrepareForTest(LendingUserDetailsHelper.class) を class に付加します。
  • testTemporarySaveBookListCsvFile メソッド内で new Expectations(LendingUserDetailsHelper.class) {{ ... }};PowerMockito.mockStatic(LendingUserDetailsHelper.class); + given(LendingUserDetailsHelper.getLoginUserId()).willReturn(1L); へ変更します。

BooklistServiceTest クラスのテストのみ実行してみると、java.lang.IllegalStateException: Failed to load ApplicationContext のエラーメッセージが表示されてテストが失敗しました。。。

f:id:ksby:20170401184410p:plain

Web で調べたり、コードをいろいろ変えて試してみましたが、テストが成功しません。stackoverflow を見ると上記のアノテーションの組み合わせで PowerMock が使えている人もいるようですが、正直原因が全然分かりません。。。 一旦元に戻して(build.gradle の powermock の記述も削除します)、次のファイルを見ることにします。

InquiringStatusOfBookQueueListenerTest.java を修正する

テストクラスは JMockit を使用して以下のように実装されています。

package ksbysample.webapp.lending.listener.rabbitmq;

import com.google.common.base.Charsets;
import ksbysample.common.test.rule.db.TableDataAssert;
import ksbysample.common.test.rule.db.TestData;
import ksbysample.common.test.rule.db.TestDataResource;
import ksbysample.common.test.rule.mail.MailServerResource;
import ksbysample.webapp.lending.dao.LibraryForsearchDao;
import ksbysample.webapp.lending.entity.LibraryForsearch;
import ksbysample.webapp.lending.service.calilapi.CalilApiService;
import ksbysample.webapp.lending.service.calilapi.response.Book;
import ksbysample.webapp.lending.service.calilapi.response.Libkey;
import ksbysample.webapp.lending.service.calilapi.response.SystemData;
import ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueMessage;
import mockit.Mock;
import mockit.MockUp;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.mail.internet.MimeMessage;
import javax.sql.DataSource;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static mockit.Deencapsulation.getField;
import static mockit.Deencapsulation.setField;
import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class InquiringStatusOfBookQueueListenerTest {

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Rule
    @Autowired
    public MailServerResource mailServer;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private MessageConverter messageConverter;

    @Autowired
    private InquiringStatusOfBookQueueListener listener;

    @Test
    @TestData("listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        // モックに入れ替える前のフィールドの実体を退避する
        LibraryForsearchDao libraryForsearchDaoOrg = getField(listener, "libraryForsearchDao");
        CalilApiService calilApiServiceOrg = getField(listener, "calilApiService");
        try {
            /**
             * モック定義部
             */
            // InquiringStatusOfBookQueueListener.libraryForsearchDao をモックに入れ替える
            LibraryForsearchDao libraryForsearchDao = new MockUp<LibraryForsearchDao>() {
                @Mock
                LibraryForsearch selectSelectedLibrary() {
                    LibraryForsearch libraryForsearch = new LibraryForsearch();
                    libraryForsearch.setSystemid("System_Id");
                    libraryForsearch.setFormal("図書館名");
                    return libraryForsearch;
                }
            }.getMockInstance();
            setField(listener, "libraryForsearchDao", libraryForsearchDao);

            // InquiringStatusOfBookQueueListener.calilApiService をモックに入れ替える
            CalilApiService calilApiService = new MockUp<CalilApiService>() {
                @Mock
                public 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);

            /**
             * 検証
             */
            // テーブルの以下のカラムのデータを検証する
            //  ・lending_app.status
            //  ・lending_book.lending_state
            IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/assertdata/001"));
            TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
            tableDataAssert.assertEquals("lending_app", new String[]{"lending_app_id", "lending_user_id", "approval_user_id", "version"});
            tableDataAssert.assertEquals("lending_book", new String[]{"lending_book_id", "lending_app_id", "isbn", "book_name", "lending_app_flg", "lending_app_reason", "approval_result", "approval_reason", "version"});

            // 送信されたメールを検証する
            assertThat(mailServer.getMessagesCount()).isEqualTo(1);
            MimeMessage mimeMessage = mailServer.getFirstMessage();
            assertThat(mimeMessage.getRecipients(javax.mail.Message.RecipientType.TO))
                    .extracting(Object::toString)
                    .containsOnly("tanaka.taro@sample.com");
            assertThat(mimeMessage.getContent())
                    .isEqualTo(com.google.common.io.Files.toString(
                            new File("src/test/resources/ksbysample/webapp/lending/helper/mail/assertdata/001/message.txt")
                            , Charsets.UTF_8));
        } finally {
            // モックに差し替えたフィールドを退避しておいた元の実体に戻す
            setField(listener, "libraryForsearchDao", libraryForsearchDaoOrg);
            setField(listener, "calilApiService", calilApiServiceOrg);
        }
    }

}

これを @MockBean アノテーションを使用して以下のように変更します。

@RunWith(SpringRunner.class)
@SpringBootTest
public class InquiringStatusOfBookQueueListenerTest {

    ..........

    @Autowired
    private InquiringStatusOfBookQueueListener listener;

    @MockBean
    private LibraryForsearchDao libraryForsearchDao;

    @MockBean
    private CalilApiService calilApiService;

    @Test
    @TestData("listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        /**
         * モック定義部
         */
        LibraryForsearch libraryForsearch = new LibraryForsearch();
        libraryForsearch.setSystemid("System_Id");
        libraryForsearch.setFormal("図書館名");
        given(libraryForsearchDao.selectSelectedLibrary()).willReturn(libraryForsearch);

        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, "蔵書なし")))));
        given(calilApiService.check(any(), any())).willReturn(bookList);

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

        ..........
    }

}
  • @MockBean private LibraryForsearchDao libraryForsearchDao; を追加します。
  • @MockBean private CalilApiService calilApiService; を追加します。
  • // モックに入れ替える前のフィールドの実体を退避する ... try { の部分と } finally { ... } の部分を削除します。
  • libraryForsearch のデータ生成処理 + given(libraryForsearchDao.selectSelectedLibrary()).willReturn(libraryForsearch); を追加します。
  • bookList のデータ生成処理 + given(calilApiService.check(any(), any())).willReturn(bookList); を追加します。

InquiringStatusOfBookQueueListenerTest クラスのテストのみ実行してみると、全てのテストが成功しました。

f:id:ksby:20170401234026p:plain

全てのテストを実行してみる

BooklistServiceTest.java はまだ JMockit を使用したままなので build.gradle から JMockit は外しません。

今の状態で全てのテストを通しで実行して成功するか確認します。まずは clean タスク → Rebuild Project → build タスクを実行してみます。

“BUILD SUCCESSFUL” のメッセージが表示されました。問題ないようです。

f:id:ksby:20170402000224p:plain

今度は Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行してみます。

こちらも全てのテストが成功しました。

f:id:ksby:20170402000641p:plain

感想&次回は。。。

@MockBean がすごく使いやすいです。前にモックを作った時はいろいろできる JMockit がいいかな、と思いましたが、この使い勝手なら @MockBean + Mockito に切り替え確定です。Spring Boot 1.4 のテストアノテーションの変更ですが、本当にテストがやりやすくなっていると思います。

PowerMock を使う方法が分からなかったので static メソッドをモック化したい場合が少し不安ですが、static メソッドをモック化しないとテストできないのはそもそも作りがおかしいのでは?という気がしています。

次回は、LendingUserDetailsHelper に @Component アノテーションを付加して Bean にし、static メソッドを止めます。その後で今回修正できなかった BooklistServiceTest.java を修正します。

ソースコード

build.gradle

■その1

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    ..........
    testCompile("org.yaml:snakeyaml")
    testCompile("org.mockito:mockito-core")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    ..........
    testCompile("com.jayway.jsonpath:json-path:2.2.0")
//    testCompile("org.jmockit:jmockit:1.30")
    testCompile("org.spockframework:spock-core:${spockVersion}") {
        exclude module: "groovy-all"
    }
    ..........
  • testCompile("org.mockito:mockito-core") を追加します。
  • testCompile("org.jmockit:jmockit:1.30")コメントアウトします。

■その2

dependencies {
    ..........
    def powermockitoVersion = "1.6.6"

    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    ..........
    testCompile("com.google.code.findbugs:jsr305:3.0.1")
    testCompile("org.powermock:powermock-module-junit4:${powermockitoVersion}")
    testCompile("org.powermock:powermock-api-mockito:${powermockitoVersion}")
  • def powermockitoVersion = "1.6.6" を追加します。
  • testCompile("org.powermock:powermock-module-junit4:${powermockitoVersion}") を追加します。
  • testCompile("org.powermock:powermock-api-mockito:${powermockitoVersion}") を追加します。

履歴

2017/04/02
初版発行。

IntelliJ IDEA を 2016.3.5 → 2017.1 へ、Git for Windows を 2.12.0 → 2.12.2 へバージョンアップ

IntelliJ IDEA を 2016.3.5 → 2017.1 へバージョンアップする

IntelliJ IDEA の 2017.1 がリリースされたのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

  2. 「Platform and Plugin Updates」ダイアログが表示されます。今回は左下に「Update and Restart」ボタンが表示されていませんので、「Download」ボタンをクリックします。

    f:id:ksby:20170329213306p:plain

  3. ブラウザが起動して Download IntelliJ IDEA ページが表示されますので ideaIU-2017.1.exe をダウンロードします。

  4. 起動している IntelliJ IDEA を終了します。

  5. ideaIU-2017.1.exe を実行します。

  6. IntelliJ IDEA Setup」ダイアログが表示されます。「Next >」ボタンをクリックします。

    f:id:ksby:20170329231025p:plain

  7. 「Uninstall old versions」画面が表示されます。以下の画面のように画面上のチェックボックスを全てチェックした後、「Next >」ボタンをクリックします。

    f:id:ksby:20170329231146p:plain

  8. 「Choose Install Location」画面が表示されます。「Destination Folder」を C:\IntelliJ_IDEA\2017.1 へ変更した後、「Next >」ボタンをクリックします。

    f:id:ksby:20170329231434p:plain

  9. 「Installation Options」画面が表示されます。何もチェックせずに「Next >」ボタンをクリックします。

    f:id:ksby:20170329231551p:plain

  10. 「Choose Start Menu Folder」画面が表示されます。何も変更せずに「Install」ボタンをクリックします。

    f:id:ksby:20170329231751p:plain

  11. 「Installing」画面が表示され、インストールが行われます。

  12. インストールが完了すると「Completing the IntelliJ IDEA Setup Wizard」画面が表示されます。「Finish」ボタンをクリックします。

    f:id:ksby:20170329232105p:plain

  13. C:\IntelliJ_IDEA\2016.3 フォルダが残っているので削除します。

  14. C:\IntelliJ_IDEA\2017.1\bin\idea64.exe を実行します。

  15. 「Complete Installatioin」ダイアログが表示されます。表示された設定はそのままで「OK」ボタンをクリックします。

    f:id:ksby:20170329232458p:plain

  16. IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20170329232807p:plain

  17. Gradle projects View のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    f:id:ksby:20170329233201p:plain

  18. 「other」以外に「build」等も表示された状態に戻ります。

    f:id:ksby:20170329233512p:plain

  19. C:\Users\<ユーザ名>\.IntelliJIdea2016.3 が残っているので削除します。

  20. Plugin にアップデートがないか確認します。IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

    今回はアップデートされた Plugin はありませんでした。

    f:id:ksby:20170329234200p:plain

  21. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して “BUILD SUCCESSFUL” のメッセージが表示されることを確認します。

    f:id:ksby:20170329235409p:plain

  22. Project View で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20170330000251p:plain

Git for Windows を 2.12.0 → 2.12.2 へバージョンアップする

Git for Windows の 2.12.2 がリリースされていたのでバージョンアップします。

  1. https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.12.2-64-bit.exe をダウンロードします。

  2. Git-2.12.2-64-bit.exe を実行します。

  3. 「Git 2.12.2 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。

  4. 「Select Components」画面が表示されます。全てのチェックが外れたままであることを確認した後、[Next >]ボタンをクリックします。

  5. 「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。

  6. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Configuring the line ending conversions」画面が表示されます。「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  8. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  9. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Install]ボタンをクリックします。

  10. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View ReleaseNotes.html」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

  11. コマンドプロンプトを起動して git のバージョンが git version 2.12.2.windows.1 になっていることを確認します。

    f:id:ksby:20170330005133p:plain

  12. git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。

    f:id:ksby:20170330005326p:plain

  13. 特に問題はないようですので、2.12.2 で作業を進めたいと思います。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その15 )( テストクラスのアノテーションを 1.4 のものに変更する )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その14 )( spring-boot-gradle-plugin は dependency-management-plugin を自動的に適用するので build.gradle に記述する必要がありませんでした ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。

参照したサイト・書籍

  1. Spring Boot 1.4.0 Release Notes - Test utilities and classes
    https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.4-Release-Notes#test-utilities-and-classes

  2. Spring Boot Reference Guide - 40. Testing
    http://docs.spring.io/spring-boot/docs/1.4.5.RELEASE/reference/htmlsingle/#boot-features-testing

目次

  1. どう書き換えればよいのか?
  2. 変更する方針を決める
  3. 全てのテストを @RunWith(SpringRunner.class) + @SpringBootTest に変更してみる
  4. Controller, RestController 以外のテストを @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) に変更してみる
  5. 次回は。。。

手順

どう書き換えればよいのか?

最低限の変更だけ行うのであれば、以下のように書き換えれば動きます。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
@WebAppConfiguration
public class LendingapprovalFormValidatorTest {

↓↓↓

@RunWith(SpringRunner.class)
@SpringBootTest
public class LendingapprovalFormValidatorTest {
  • @RunWith(SpringJUnit4ClassRunner.class)@RunWith(SpringRunner.class) へ変更します。
  • @SpringBootTest(classes = Application.class), @WebAppConfiguration@SpringBootTest へ変更します。

@RunWith(SpringRunner.class) の方は単純な書き換えなので迷わないのですが、@SpringBootTest の方は webEnvironment 属性で NONE, MOCK, RANDOM_PORT, DEFINED_PORT のどれを指定すればよいのかが最初よく分かりませんでした。結論としては @SpringBootTest だけ書けば問題なくテストは動きます(この時はデフォルト値である MOCK が指定された状態です)。

1.3 のアノテーションの時と MOCK, RANDOM_PORT, DEFINED_PORT それぞれを指定した時のテストの実行速度がどれくらい違うのか気になったので src/test/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalControllerTest.java で試してみます。NONE はエラーになるので最初から除外します。

  • テストは LendingapprovalControllerTest.java のクラス名の左側のアイコンをクリックして表示される「Run ‘LendingapprovalControllerTest'」を選択して実行します。

    f:id:ksby:20170326090354p:plain

  • 計測時間は IntelliJ IDEA の以下の場所に表示されるものを使用します。

    f:id:ksby:20170326090707p:plain

1回目 2回目 3回目 4回目 5回目 平均
1.3 5s 791ms 6s 128ms 5s 720ms 5s 609ms 6s 56ms 5s 961ms
MOCK 5s 906ms 6s 43ms 5s 979ms 6s 225ms 5s 910ms 6s 9ms
RANDOM_PORT 5s 657ms 5s 766ms 5s 555ms 5s 207ms 5s 388ms 5s 514ms
DEFINED_PORT 6s 702ms 5s 542ms 5s 694ms 5s 632ms 5s 787ms 5s 871ms

MOCK で遅いの?と思ったので、この後3回程試してみたところ 5s 625ms、6s 8ms、5s 274ms でした。最後の1回だけはそこそこ速いです。MOCK, RANDOM_PORT, DEFINED_PORT でたいした差はないのではないか?という気がしますが、今ひとつはっきりしませんね。。。

もう1つ思ったのは、Controller のテストでなければ @SpringBootTest ではなく @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) を指定した方がテストが速いのではないか?という点です。

src/test/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalFormValidatorTest.java で試してみます。

1回目 2回目 3回目 4回目 5回目 平均
1.3 215ms 167ms 254ms 207ms 180ms 204ms
NONE 190ms 291ms 180ms 182ms 199ms 208ms
MOCK 208ms 197ms 201ms 211ms 196ms 202ms

NONE は2回目が遅かった影響で平均が他と大差なくなっていますが、MOCK と比較すると 10~20ms 程度速くなるような気がします。

変更する方針を決める

まずはシンプルに一律以下のアノテーションに変更してテストが全て成功することを確認してみます。MOCK, RANDOM_PORT, DEFINED_PORT で差がない気がするので、シンプルに @SpringBootTest だけ書くことにします。

@RunWith(SpringRunner.class)
@SpringBootTest
public class LendingapprovalFormValidatorTest {

その後で Controller, RestController 以外のテストを @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) に変えて、テストの総実行時間が速くなるのか確認してみたいと思います。

また、Controller, RestController のテストを Controller の機能だけのテストにしていれば @WebMvcTest アノテーションを使用するのもありだな、と思いましたが、作成してあるテストは Service まで含めたものになっているので、今回 @WebMvcTest は使いません。Controller は Mock を利用して Controller だけのテストをした方がよいのかもしれません。

全てのテストを @RunWith(SpringRunner.class) + @SpringBootTest に変更してみる

以下の手順でアノテーションを変更します。

  • Ctrl+Alt+F で「Find in Path」ダイアログを表示した後、@RunWith(SpringJUnit4ClassRunner.class) で project 全体を検索します。
  • ヒットした箇所を @RunWith(SpringRunner.class) + @SpringBootTest の組み合わせに変更した後、Ctrl+Alt+o を押して不要な import 文を削除します。

また Spock で書いてあるテストについては以下のように変更します。

@ContextConfiguration(loader = SpringApplicationContextLoader.class, classes = Application.class)
@WebAppConfiguration
class ValuesHelperTest extends Specification {

↓↓↓

@SpringBootTest
class ValuesHelperTest extends Specification {

全てのテストを変更したら clean タスク → Rebuild Project → build タスクを実行してみます。

build タスクは “BUILD SUCCESSFUL” が表示されて成功しました。

f:id:ksby:20170326201050p:plain

Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行してみます。

こちらも全てのテストが成功しました。テストの実行速度も体感的には以前と変わりません、というより少し速くなっているような気がします。

f:id:ksby:20170326202958p:plain

5回計測してみます。

1回目 2回目 3回目 4回目 5回目 平均
29s 709ms 29s 431ms 28s 175ms 29s 293ms 29s 344ms 29s 190ms

IntelliJ IDEA を 2016.3.4 → 2016.3.5 へ、Git for Windows を 2.11.1 → 2.12.0 へバージョンアップ の記事だと 1m 3s 669ms、Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その13 )( RestTemplate で WebAPI を呼び出している処理に spring-retry でリトライ処理を入れる ) の記事だと 49s 111ms でしたので、やはり速くなっているようです。

Controller, RestController 以外のテストを @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) に変更してみる

以下の手順でアノテーションを変更します。

  • Ctrl+Alt+F で「Find in Path」ダイアログを表示した後、@SpringBootTest で project 全体を検索します。
  • ヒットした箇所のうち、Controller, RestController のテスト以外を @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) に変更します。

全てのテストを変更したら clean タスク → Rebuild Project → build タスクを実行してみます。

build タスクは “BUILD SUCCESSFUL” が表示されて成功しました。

f:id:ksby:20170326212455p:plain

Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行してみると、こちらも全てのテストが成功しました。

f:id:ksby:20170326212821p:plain

5回計測してみます。

1回目 2回目 3回目 4回目 5回目 平均
29s 314ms 31s 566ms 30s 978ms 30s 473ms 29s 504ms 30s 367ms

少しですが遅くなるようです。MOCK と NONE が混在しているからでしょうか。。。? こちらの変更は元に戻して @SpringBootTest のみにします。

最初テストクラスのアノテーションはなんか複雑になったような気がしていましたが、@RunWith(SpringRunner.class) + @SpringBootTest を書けば十分でした。実はシンプルになったんですね。

次回は。。。

モックは JMockit で作成していますが、Spring Boot 1.4 から @MockBean アノテーションが提供されて Mockito と組み合わせてモックを作ることが推奨されているようなので、@MockBean + Mockito で作り直してみます。

ソースコード

履歴

2017/03/27
初版発行。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その14 )( spring-boot-gradle-plugin は dependency-management-plugin を自動的に適用するので build.gradle に記述する必要がありませんでした )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その13 )( RestTemplate で WebAPI を呼び出している処理に spring-retry でリトライ処理を入れる ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。

参照したサイト・書籍

目次

  1. まずは単に classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE") をコメントアウトしてみる
  2. dependencyManagement { ... } もコメントアウトするとどうなるのか?
  3. Spring IO Platform の POM を記述した場合と記述しない場合で何が違うのか?
  4. build.gradle を修正する
  5. 次回は。。。

手順

まずは単に classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE")コメントアウトしてみる

build.gradle から dependency-management-plugin に関する部分をコメントアウトしてみます。

buildscript {
    ext {
        springBootVersion = '1.4.4.RELEASE'
    }
    repositories {
        jcenter()
        maven { url "http://repo.spring.io/repo/" }
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
//        classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE")
        // for Error Prone ( http://errorprone.info/ )
        classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.9")
        // for Grgit
        classpath("org.ajoberstar:grgit:1.8.0")
        // Gradle Download Task
        classpath("de.undercouch:gradle-download-task:3.2.0")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
//apply plugin: 'io.spring.dependency-management'
apply plugin: 'de.undercouch.download'
apply plugin: 'groovy'
apply plugin: 'net.ltgt.errorprone'
apply plugin: 'checkstyle'
apply plugin: 'findbugs'
  • 以下の記述の行をコメントアウトします。
    • classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE")
    • apply plugin: 'io.spring.dependency-management'

コメントアウト後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新すると、エラーは出ずに終了しました。

clean タスク → Rebuild Project → build タスク も実行してみましたが、こちらも無事 “BUILD SUCCESSFUL” が表示されました。

f:id:ksby:20170324004205p:plain

gradlew dependencies コマンドを実行してみると checkstyle が依存する guava のバージョンが 21.0 になっています。Spring Boot 1.4.5 の Appendix F. Dependency versions には com.google.guava:guava の記述はないので、build.gradle に記述している dependencyManagement { imports { mavenBom("io.spring.platform:platform-bom:Athens-SR3") { ... } } } の BOM の内容が反映されていることが確認できます。

checkstyle - The Checkstyle libraries to be used for this project.
\--- com.puppycrawl.tools:checkstyle:7.5.1
     +--- antlr:antlr:2.7.7
     +--- org.antlr:antlr4-runtime:4.6
     +--- commons-beanutils:commons-beanutils:1.9.3
     |    \--- commons-collections:commons-collections:3.2.2
     +--- commons-cli:commons-cli:1.3.1
     \--- com.google.guava:guava:19.0 -> 21.0

よって、Spring IO Platform の BOM を利用したい場合には spring-boot-gradle-plugin だけ記述すればよく、dependency-management-plugin を記述する必要はありませんでした。

dependencyManagement { ... }コメントアウトするとどうなるのか?

更に build.gradle の dependencyManagement { ... } を削除するとどうなるのか興味が湧いたので、試してみます。

//dependencyManagement {
//    imports {
//        mavenBom("io.spring.platform:platform-bom:Athens-SR3") {
//            bomProperty 'commons-lang3.version', '3.5'
//            bomProperty 'guava.version', '21.0'
//        }
//    }
//}

コメントアウト後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新すると、以下のエラーが表示されました。

f:id:ksby:20170324010643p:plain

これだけでは原因が分からないので、コマンドラインから gradlew dependencies コマンドを実行してみると以下のエラーが表示されました。guava と commons-lang3 の依存性解決に失敗したのが原因でした。

f:id:ksby:20170324011130p:plain

Spring Boot 1.4.5 の Appendix F. Dependency versions には guava も commons-lang3 も記述されていないので当然と言えば当然ですね。

build.gradle の dependencies 内で guava と commons-lang3 にバージョン番号を明記してみます。

    compile("com.google.guava:guava:21.0")
    compile("org.apache.commons:commons-lang3:3.5")

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新すると、エラーは出ずに終了しました。

clean タスク → Rebuild Project → build タスク を実行すると、無事 “BUILD SUCCESSFUL” が表示されました。ただしなんか足りないライブラリをダウンロードしています。

f:id:ksby:20170324013347p:plain

gradlew dependencies コマンドを実行してみると checkstyle が依存する Guava のバージョンは 19.0 のままでした。

checkstyle - The Checkstyle libraries to be used for this project.
\--- com.puppycrawl.tools:checkstyle:7.5.1
     +--- antlr:antlr:2.7.7
     +--- org.antlr:antlr4-runtime:4.6
     +--- commons-beanutils:commons-beanutils:1.9.3
     |    \--- commons-collections:commons-collections:3.2.2
     +--- commons-cli:commons-cli:1.3.1
     \--- com.google.guava:guava:19.0

Spring IO Platform の POM を記述した場合と記述しない場合で何が違うのか?

両方の場合の gradlew dependencies コマンドの出力結果を比較してみました。

checkstyle - The Checkstyle libraries to be used for this project.

component 記述した場合 記述しない場合
com.google.guava:guava 21.0 19.0

■errorprone

component 記述した場合 記述しない場合
com.google.guava:guava 21.0 20.0
com.google.code.findbugs:jsr305 3.0.1 3.0.0

findbugs - The FindBugs libraries to be used for this project.

component 記述した場合 記述しない場合
com.google.code.findbugs:jsr305 3.0.1 3.0.0

■上記以外

component 記述した場合 記述しない場合
javax.activation:activation 1.1.1 1.1
com.rabbitmq:amqp-client 3.6.6 3.6.5
org.objenesis:objenesis 2.4 2.1

guava, commons-lang3 以外にも BOM を記述した方がバージョンが上がるものがあります。

build.gradle を修正する

Spring IO Platform の BOM を記述した方が guava や commons-lang3 も依存性解決の対象になって個人的には好みなので、今回は単に dependency-management-plugin の記述だけを削除することにします。

build.gradle を リンク先の内容 に変更した後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。問題が出ないことは上に書きましたので、ここでは改めて記述しません。

次回は。。。

Spring Boot 1.4 で一番変更が入ったと聞いているテストクラスのアノテーションを見直します。

ソースコード

build.gradle

buildscript {
    ext {
        springBootVersion = '1.4.4.RELEASE'
    }
    repositories {
        jcenter()
        maven { url "http://repo.spring.io/repo/" }
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        // for Error Prone ( http://errorprone.info/ )
        classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.9")
        // for Grgit
        classpath("org.ajoberstar:grgit:1.8.0")
        // Gradle Download Task
        classpath("de.undercouch:gradle-download-task:3.2.0")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'de.undercouch.download'
apply plugin: 'groovy'
apply plugin: 'net.ltgt.errorprone'
apply plugin: 'checkstyle'
apply plugin: 'findbugs'
  • classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE") を削除します。
  • apply plugin: 'io.spring.dependency-management' を削除します。

履歴

2017/03/24
初版発行。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( 番外編 )( IntelliJ IDEA に Request mapper Plugin をインストールする )

概要

記事一覧はこちらです。

今朝 Twitter を見ていたら、珍しく IntelliJ IDEA の Plugin に関するメッセージが流れてきました。

Request mapper という名称の Plugin で、@RequestMapping 系のアノテーションに記述した URL を検索・ジャンプすることができる Plugin のようです。

Spring Tool Window の MVC タブでも URL 一覧は表示できますが、Plugin を紹介している動画を見ていると軽快に検索・ジャンプできるように見えて便利そうだったので、入れて試してみたいと思います。

参照したサイト・書籍

目次

  1. Request mapper Plugin をインストールする
  2. Request mapper Plugin を使ってみる

手順

Request mapper Plugin をインストールする

  1. IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択します。

  2. 「Settings」ダイアログが表示されます。画面左側のリストから「Plugins」を選択した後、画面中央下にある「Browse repositories…」ボタンをクリックします。

    f:id:ksby:20170322022832p:plain

  3. 「Browse Repositories」ダイアログが表示されます。左上の検索フィールドに “request mapper” と入力すると、その下のリストに Request mapper Plugin が表示されますので、画面右側に表示される「Install」ボタンをクリックします。

    f:id:ksby:20170322023029p:plain

    Plugin がダウンロードされた後「Install」ボタンが「Restart IntelliJ IDEA」ボタンに変わりますので、クリックします。

  4. 一旦「Settings」ダイアログに戻りますので、画面下の「OK」ボタンをクリックします。

  5. 「Platform and Plugin Updates」ダイアログが表示されますので「Restart」ボタンをクリックします。

  6. IntelliJ IDEA が再起動してインストールは完了です。

Request mapper Plugin を使ってみる

Ctrl+Shift+¥ を入力すると画面中央に検索フィールドが表示されます。

f:id:ksby:20170322024218p:plain

“/” だけ入力すると全ての URL がヒットするようです。URL 一覧を見てみるとテストクラスに記述した “/exceptionHandlerAdviceTest” もヒットしていました。src/main/java の下だけでなく src/main/test の下も対象になるようです。

f:id:ksby:20170322024629p:plain

中間一致検索もしてくれます。例えば “download” という文字を入力すると、URL に “download” という文字が含まれているものだけが表示されます。

f:id:ksby:20170322025026p:plain

またそのまま続けて “view” と入力すると、"download" と “view” という文字が含まれている URL だけが表示されました。大文字小文字の区別はされていないので、検索しやすそうです。

f:id:ksby:20170322025402p:plain

検索文字列のヒットには、途中に “/” が入っているとかは関係ないようです。"resultresponse" と入力すると “result” と “response” という文字が含まれる URL が表示されました。

f:id:ksby:20170322025708p:plain

ただし “result” + “oad” ( “download” の最後の3文字 ) = “resultoad” と入力すると何もヒットしませんでした。単語を区別して検索しているのでしょうか? URL がヒットしないと検索文字列が赤く表示されます。

f:id:ksby:20170322025851p:plain

単に “oad” だとヒットしますね。

f:id:ksby:20170322031011p:plain

検索文字列を他に試していたところ、"resultfile" と “resultby” はヒットするのに “resultdownload” はヒットしませんでした。最初に文字列をヒットした後は、キャメルケースになっているところでヒットするか否かを判断しているようです。

f:id:ksby:20170322030432p:plain f:id:ksby:20170322030537p:plain f:id:ksby:20170322030647p:plain

ヒットした URL 一覧からソースコードにジャンプしたい URL を選択して Enter キーを押すと、対応する箇所にジャンプします。

少し使ってみただけですが、動作が軽快でいいですね! 以前から欲しいと思っていた機能の Plugin なので、しばらく使ってみたいと思います。

履歴

2017/03/22
初版発行。

IntelliJ IDEA を 2016.3.4 → 2016.3.5 へ、Git for Windows を 2.11.1 → 2.12.0 へバージョンアップ

IntelliJ IDEA を 2016.3.4 → 2016.3.5 へバージョンアップする

IntelliJ IDEA の 2016.3.5 がリリースされたのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

  2. 「Platform and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170321233338p:plain

  3. 再び「Platform and Plugin Updates」ダイアログが表示され CheckStyle-IDEA, JRebel for IntelliJ のアップデートも表示されますので、そのまま「Update and Restart」ボタンをクリックします。

  4. Patch と Plugin がダウンロードされて IntelliJ IDEA が再起動します。

  5. メイン画面が表示されると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20170322001938p:plain

  6. 処理が終了すると Gradle Tool Window のツリーの表示が other グループしかない初期の状態に戻っていますので、左上の「Refresh all Gradle projects」ボタンをクリックして更新します。更新が完了すると build グループ等が表示されます。

  7. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して “BUILD SUCCESSFUL” のメッセージが表示されることを確認します。

    f:id:ksby:20170322014357p:plain

  8. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20170322014714p:plain

Git for Windows を 2.11.1 → 2.12.0 へバージョンアップする

Git for Windows の 2.12.0 がリリースされていたのでバージョンアップします。

  1. https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.12.0-64-bit.exe をダウンロードします。

  2. Git-2.12.0-64-bit.exe を実行します。

  3. 「Git 2.12.0 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。

  4. 「Select Components」画面が表示されます。全てのチェックが外れたままであることを確認した後、[Next >]ボタンをクリックします。

  5. 「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。

  6. 「Configuring the line ending conversions」画面が表示されます。「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  7. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  8. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Install]ボタンをクリックします。

  9. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View ReleaseNotes.html」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

  10. コマンドプロンプトを起動して git のバージョンが git version 2.12.0.windows.1 になっていることを確認します。

    f:id:ksby:20170322015810p:plain

  11. git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。

    f:id:ksby:20170322015945p:plain

  12. 特に問題はないようですので、2.12.0 で作業を進めたいと思います。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その13 )( RestTemplate で WebAPI を呼び出している処理に spring-retry でリトライ処理を入れる )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その12 )( RestTemplateBuilder を使用するように変更したらテストが失敗するようになった理由とは? ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 前回の検証で WebAPI を呼び出している処理にリトライ処理を入れればテストが成功することが分かったので、spring-retry でリトライ処理を入れます。
    • @Retryable アノテーションではなく RetryTemplate クラスで実装します。
    • リトライ処理は、以下のルールにします。
      • Exception.class ( あるいはその継承クラス ) が throw されたらリトライします。
      • 最大5回リトライします。
      • リトライ間隔は5秒固定にします。
      • リトライが全てエラーになった場合には、発生した例外がそのまま throw されるようにします。
  • spring-retry の処理については以前書いた以下の記事を参考にして実装します。

参照したサイト・書籍

目次

  1. build.gradle を変更する
  2. spring-retry でリトライ処理を入れる
  3. 動作確認
  4. メモ書き

手順

build.gradle を変更する

gradlew dependencies を実行すると org.springframework.boot:spring-boot-starter-amqp の依存関係で spring-retry は入っていることが確認できたのですが、

compile - Dependencies for source set 'main'.
..........
+--- org.springframework.boot:spring-boot-starter-amqp: -> 1.4.4.RELEASE
|    +--- org.springframework.boot:spring-boot-starter:1.4.4.RELEASE (*)
|    +--- org.springframework:spring-messaging:4.3.6.RELEASE
|    |    +--- org.springframework:spring-beans:4.3.6.RELEASE (*)
|    |    +--- org.springframework:spring-context:4.3.6.RELEASE (*)
|    |    \--- org.springframework:spring-core:4.3.6.RELEASE
|    \--- org.springframework.amqp:spring-rabbit:1.6.7.RELEASE
|         +--- org.springframework:spring-web:4.2.9.RELEASE -> 4.3.6.RELEASE (*)
|         +--- org.springframework.retry:spring-retry:1.1.5.RELEASE
|         |    \--- org.springframework:spring-core:4.0.4.RELEASE -> 4.3.6.RELEASE
|         +--- org.springframework:spring-messaging:4.2.9.RELEASE -> 4.3.6.RELEASE (*)
|         +--- org.springframework:spring-context:4.2.9.RELEASE -> 4.3.6.RELEASE (*)
|         +--- org.springframework:spring-tx:4.2.9.RELEASE -> 4.3.6.RELEASE (*)
|         +--- com.rabbitmq:http-client:1.0.0.RELEASE
|         |    +--- org.apache.httpcomponents:httpclient:4.3.6 -> 4.5.2
|         |    |    +--- org.apache.httpcomponents:httpcore:4.4.4 -> 4.4.6
|         |    |    \--- commons-codec:commons-codec:1.9 -> 1.10
|         |    \--- com.fasterxml.jackson.core:jackson-databind:2.5.1 -> 2.8.6 (*)
|         +--- org.springframework.amqp:spring-amqp:1.6.7.RELEASE
|         |    \--- org.springframework:spring-core:4.2.9.RELEASE -> 4.3.6.RELEASE
|         \--- com.rabbitmq:amqp-client:3.6.5 -> 3.6.6
..........

明示的に build.gradle に記述することにします。

  1. build.gradle を リンク先の内容 に変更します。変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

spring-retry でリトライ処理を入れる

  1. src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.javaリンク先の内容 に変更します。

  2. src/main/java/ksbysample/webapp/lending/service/calilapi/CalilApiService.javaリンク先の内容 に変更します。

動作確認

  1. clean タスク → Rebuild Project → build タスク を実行して “BUILD SUCCESSFUL” が表示されることを確認します。

    f:id:ksby:20170320190250p:plain

  2. Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行して、テストが全て成功することを確認します。

    f:id:ksby:20170320190701p:plain

メモ書き

メソッドに @Retryable アノテーションを付けるだけでリトライしてくれる訳ではないらしい。@Retryable アノテーションでリトライさせるための条件を忘れているな。。。

ソースコード

build.gradle

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    ..........
    compile("org.springframework.session:spring-session")
    compile("org.springframework.retry:spring-retry")
    compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
    ..........
  • compile("org.springframework.retry:spring-retry") を追加します。

ApplicationConfig.java

@Configuration
public class ApplicationConfig {

    ..........

    @Bean
    public RetryTemplate simpleRetryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        retryTemplate.setRetryPolicy(
                new SimpleRetryPolicy(5, singletonMap(Exception.class, true)));
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(5000);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        return retryTemplate;
    }

}
  • simpleRetryTemplate メソッドを追加します。

CalilApiService.java

@Service
@PropertySource("classpath:calilapi.properties")
public class CalilApiService {

    ..........

    private final RestTemplate restTemplateForCalilApi;

    private final RestTemplate restTemplateForCalilApiByXml;

    private final RetryTemplate simpleRetryTemplate;

    public CalilApiService(@Qualifier("restTemplateForCalilApi") RestTemplate restTemplateForCalilApi
            , @Qualifier("restTemplateForCalilApiByXml") RestTemplate restTemplateForCalilApiByXml
            , @Qualifier("simpleRetryTemplate") RetryTemplate simpleRetryTemplate) {
        this.restTemplateForCalilApi = restTemplateForCalilApi;
        this.restTemplateForCalilApiByXml = restTemplateForCalilApiByXml;
        this.simpleRetryTemplate = simpleRetryTemplate;
    }

    ..........

    public Libraries getLibraryList(String pref) throws Exception {
        // 図書館データベースAPIを呼び出して XMLレスポンスを受信する
        ResponseEntity<String> response = getForEntityWithRetry(this.restTemplateForCalilApi
                , URL_CALILAPI_LIBRALY, String.class, this.calilApiKey, pref);

        // 受信した XMLレスポンスを Javaオブジェクトに変換する
        Serializer serializer = new Persister();
        Libraries libraries = serializer.read(Libraries.class, response.getBody());

        return libraries;
    }

    public LibrariesForJackson2Xml getLibraryListByJackson2Xml(String pref) throws Exception {
        // 図書館データベースAPIを呼び出して XMLレスポンスを受信する
        ResponseEntity<LibrariesForJackson2Xml> response = getForEntityWithRetry(this.restTemplateForCalilApiByXml
                , URL_CALILAPI_LIBRALY, LibrariesForJackson2Xml.class, this.calilApiKey, pref);
        return response.getBody();
    }

    public List<Book> check(String systemid, List<String> isbnList) {
        ..........

        ResponseEntity<CheckApiResponse> response = null;
        String url = URL_CALILAPI_CHECK;
        for (int retry = 0; retry < RETRY_MAX_CNT; retry++) {
            // 蔵書検索APIを呼び出して蔵書の有無と貸出状況を取得する
            response = getForEntityWithRetry(this.restTemplateForCalilApiByXml, url, CheckApiResponse.class, vars);
            logger.info("カーリルの蔵書検索API を呼び出し、レスポンスを取得しました。{}", response.getBody().toString());
            if (response.getBody().getContinueValue() == 0) {
                break;
            }

            ..........
        }

        return response.getBody().getBookList();
    }

    private <T> ResponseEntity<T> getForEntityWithRetry(RestTemplate restTemplate, String url
            , Class<T> responseType, Object... uriVariables) {
        ResponseEntity<T> response = this.simpleRetryTemplate.execute(context -> {
            if (context.getRetryCount() > 0) {
                logger.info("★★★ リトライ回数 = " + context.getRetryCount());
            }
            ResponseEntity<T> innerResponse = restTemplate.getForEntity(url, responseType, uriVariables);
            return innerResponse;
        });

        return response;
    }
  • private final RetryTemplate simpleRetryTemplate; を追加し、コンストラクタインジェクションの処理も追加します。
  • getForEntityWithRetry メソッドを追加します。
  • 以下のメソッド内で RestTemplate#getForEntity を呼び出しているところを getForEntityWithRetry メソッドを使用するように変更します。
    • getLibraryList メソッド
    • getLibraryListByJackson2Xml メソッド
    • check メソッド

履歴

2017/03/20
初版発行。
2017/03/22
* @Retryable アノテーションで実装したのですが、例外が throw されてもリトライしないことに気付いたので、RetryTemplate クラスを使用する方法に修正しました。