Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その16 )( テストクラスのモックを @MockBean + Mockito で作り直す )
概要
記事一覧はこちらです。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その15 )( テストクラスのアノテーションを 1.4 のものに変更する ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
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-beansMockito
http://site.mockito.org/Mocking static methods with Mockito
http://stackoverflow.com/questions/21105403/mocking-static-methods-with-mockitoHow can I mock a private static method with PowerMockito?
http://stackoverflow.com/questions/31796736/how-can-i-mock-a-private-static-method-with-powermockitopowermock/powermock - MockitoUsage
https://github.com/powermock/powermock/wiki/MockitoUsageCan Mockito stub a method without regard to the argument?
http://stackoverflow.com/questions/5969630/can-mockito-stub-a-method-without-regard-to-the-argument
目次
- @MockBean アノテーションは Mockito を使うが、バージョンは 2 ではなく 1 らしい
- 変更手順を考える
- build.gradle を修正する
- clean タスク → Rebuild Project を実行して修正対象のソースを洗い出す
- LibraryHelperTest.java を修正する
- BooklistServiceTest.java を修正する
- InquiringStatusOfBookQueueListenerTest.java を修正する
- 全てのテストを実行してみる
- 感想&次回は。。。
手順
@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 系を使うことにします。
- Upgrade to Mockito 2
https://github.com/spring-projects/spring-boot/issues/7770 - Document how to use Mockito 2 with Spring Boot 1.5
https://github.com/spring-projects/spring-boot/issues/8217
変更手順を考える
以下の手順で変更していきます。
- build.gradle を編集して JMockit を外して Mockito を追加します。
- clean タスク → Rebuild Project を実行してエラーが出る箇所 ( JMockit を利用している箇所 ) を洗い出します。
- 一旦 build.gradle に JMockit を戻します。
- エラーが出た箇所を @MockBean + Mockito で書き直します。
- build.gradle から JMockit を外します。
- 最後に全てのテストを通しで実行して成功することを確認します。
build.gradle を修正する
build.gradle を リンク先のその1内容 に変更します。
変更後、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 クラスのテストのみ実行してみると、全てのテストが成功しました。
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
のエラーメッセージが表示されてテストが失敗しました。。。
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 クラスのテストのみ実行してみると、全てのテストが成功しました。
全てのテストを実行してみる
BooklistServiceTest.java はまだ JMockit を使用したままなので build.gradle から JMockit は外しません。
今の状態で全てのテストを通しで実行して成功するか確認します。まずは clean タスク → Rebuild Project → build タスクを実行してみます。
“BUILD SUCCESSFUL” のメッセージが表示されました。問題ないようです。
今度は Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行してみます。
こちらも全てのテストが成功しました。
感想&次回は。。。
@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
初版発行。