Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( 番外編 )( static メソッドをモック化してテストするには? )
概要
記事一覧はこちらです。
Spring Framework の DI コンテナが管理するクラスのインスタンスのメソッドから DI コンテナで管理していないクラスの static メソッドが呼び出されている場合に、static メソッドをモック化して、Spring Framework の DI コンテナが管理するクラスのテストを書く方法があるのか知りたくなったので、いろいろ調べてみました。今回はそのメモ書きです。
モック化しないテストは Groovy + Spock で書きたいので、モック化する場合のテストも可能なら Groovy + Spock で、Spock が無理なら Groovy + JUnit4 で書く方法を調べています。
参照したサイト・書籍
Spock Framework Reference Documentation - Interaction Based Testing
http://spockframework.org/spock/docs/1.1/interaction_based_testing.htmlPowerMock
https://github.com/powermock/powermockPowerMockとJUnitのRuleを使うときのメモ
http://irof.hateblo.jp/entry/20130517/p1Mockito + PowerMock LinkageError while mocking system class
https://stackoverflow.com/questions/16520699/mockito-powermock-linkageerror-while-mocking-system-class
目次
- テストで使用するクラスを書く
- Spock で正常処理のテストを書く
- static メソッドをモック化して実施したいテストとは?
- Groovy + Spock + GroovyMock で static メソッドをモック化してテストを書いてみる
- Groovy + JUnit4 + PowerMock で static メソッドをモック化してテストを書いてみる
- Groovy + Spock + PowerMock で static メソッドをモック化してテストを書いてみる
- Spock ではうまく動作しないので JUnit4 でテストを書いてみる
- まとめ
- PowerMock で void のメソッドをモック化するには?
手順
テストで使用するクラスを書く
今回テストで使用するクラスを2つ書きます。SampleHelper クラスと BrowfishUtils クラスです。
最終的に static メソッドのモックを作成してテストを書きたいのは SampleHelper クラスです。以下の仕様です。
- SampleHelper クラスは @Component アノテーションを付加して Spring Framework の DIコンテナでインスタンス化します。
- SampleHelper#encrypt メソッドは BrowfishUtils#encrypt メソッドを呼び出して渡された文字列を Browfish で暗号化します。
- BrowfishUtils#encrypt メソッドは static メソッドです。
- BrowfishUtils#encrypt メソッドは処理の中で発生する例外をそのまま throw しますが、SampleHelper#encrypt メソッドは RuntimeException に置き換えます。
- 余談ですが、今回は static メソッドのテストの方法を調べるのが目的なので BrowfishUtils クラスでは暗号化モードには ECB を使用したのですが、ECB を使うと Error Prone が InsecureCryptoUsage というチェックエラーを出力します(ECB で暗号化した文字列は常に同じになるので暗号化としてふさわしくない、というエラーのようです)。何となく作ったサンプルでしたが、Error Prone はこんな所までチェックするのか、と驚きました。
package ksbysample.webapp.lending; import org.springframework.stereotype.Component; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @Component public class SampleHelper { public String encrypt(String str) { try { return BrowfishUtils.encrypt(str); } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { throw new RuntimeException(e); } } }
package ksbysample.webapp.lending; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; public class BrowfishUtils { private static final String KEY = "sample"; private static final String ALGORITHM = "Blowfish"; private static final String TRANSFORMATION = "Blowfish/ECB/PKCS5Padding"; public static String encrypt(String str) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException , BadPaddingException, IllegalBlockSizeException { SecretKeySpec secretKeySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); byte[] encryptedBytes = cipher.doFinal(str.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encryptedBytes); } }
Spock で正常処理のテストを書く
以下のテストを書きます。
package ksbysample.webapp.lending import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification import spock.lang.Unroll @SpringBootTest class SampleHelperTest extends Specification { @Autowired private SampleHelper sampleHelper @Unroll def "SampleHelper.encrypt(#str) --> #result"() { expect: sampleHelper.encrypt(str) == result where: str || result "test" || "bjMKKlhqu/c=" "xxxxxxxx" || "ERkXmOeArBKwGbCh+M6aHw==" } }
実行して成功することを確認します。
static メソッドをモック化して実施したいテストとは?
最終的に書きたいのは以下の内容のテストです。
- BrowfishUtils#encrypt メソッドをモック化して、呼び出されたら NoSuchPaddingException が throw されるようにします。
- SampleHelper#encrypt メソッドを呼び出したら RuntimeException が throw されることを確認します。
Groovy + Spock + GroovyMock で static メソッドをモック化してテストを書いてみる
Interaction Based Testing の中の「Mocking Static Methods」を見ると Spock でも static メソッドをモック化できるようです。
最初は SampleHelper#encrypt メソッドではなく BrowfishUtils#encrypt メソッドでテストを書いてみます。
モックを使ったテストは正常処理のテストとは別にしたいので、SampleHelperTest クラスは @RunWith(Enclosed)
を付加して extends Specification
を削除し、「正常処理のテスト」static class と「異常処理のテスト」static class の2つに分けます。
package ksbysample.webapp.lending import org.junit.experimental.runners.Enclosed import org.junit.runner.RunWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification import spock.lang.Unroll import javax.crypto.NoSuchPaddingException @RunWith(Enclosed) class SampleHelperTest { @SpringBootTest static class 正常処理のテスト extends Specification { @Autowired private SampleHelper sampleHelper @Unroll def "SampleHelper.encrypt(#str) --> #result"() { expect: sampleHelper.encrypt(str) == result where: str || result "test" || "bjMKKlhqu/c=" "xxxxxxxx" || "ERkXmOeArBKwGbCh+M6aHw==" } } static class 異常処理のテスト extends Specification { def "BrowfishUtils.encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() { given: GroovyMock(BrowfishUtils, global: true) BrowfishUtils.encrypt(_) >> { throw new NoSuchPaddingException() } when: BrowfishUtils.encrypt("test") then: thrown(NoSuchPaddingException) } } }
テストを実行すると成功しました。
SampleHelper#encrypt メソッドを呼び出すテストに変更してみます。
@SpringBootTest static class GroovyMockを使用した異常処理のテスト extends Specification { @Autowired private SampleHelper sampleHelper def "SampleHelper.encryptを呼ぶとRuntimeExceptionをthrowする"() { given: GroovyMock(BrowfishUtils, global: true) BrowfishUtils.encrypt(_) >> { throw new NoSuchPaddingException() } when: sampleHelper.encrypt("test") then: thrown(RuntimeException) } }
テストを実行すると Expected exception of type 'java.lang.RuntimeException', but no exception was thrown
というメッセージが出力されて失敗しました。例外を throw するようモックにした BrowfishUtils#encrypt メソッドが SampleHelper#encrypt メソッドからは呼び出されていないようです。
GroovyMock
を使う方法だと Spring Framework の DI コンテナが絡んだ時にうまく動作しないようなので、他の方法を探します。
Groovy + JUnit4 + PowerMock で static メソッドをモック化してテストを書いてみる
Spring Boot がサポートしている Mockito で static メソッドをモック化できないか調べてみたところ、Mocito 自体は static メソッドのモック化には対応していませんが、PowerMock を使用するとモック化できるようです。
ライブラリを依存関係に追加します。Mockito_maven のページを参考に build.gradle を以下のように変更した後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
dependencies { .......... // PowerMock testCompile("org.powermock:powermock-module-junit4:1.6.6") testCompile("org.powermock:powermock-api-mockito:1.6.6") }
SampleHelper#encrypt メソッドではなく、BrowfishUtils#encrypt メソッドをモックにして BrowfishUtils#encrypt メソッド自体を呼ぶテストを書いてみます。最初は Groovy + JUnit4 + PowerMock で書きます。
例外が throw されているかチェックするのに AssertJ の assertThatExceptionOfType メソッドを使用したのですが、lambda 式のところで赤波線が表示されてエラーになりました。なぜ?と思って Web で調べてみると、Groovy は lambda 式がサポートされていないことが原因でした。知りませんでした。。。
調べてみたところ、Java の lambda 式は Groovy の Closure でそのまま書けるようです。上のエラーの場合には () -> { ... }
→ { ... }
で書けますので、テストを以下のように書きます。
@RunWith(PowerMockRunner) @PowerMockRunnerDelegate(JUnit4) @PrepareForTest(BrowfishUtils) static class 異常処理のテスト { @Test void "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() { // setup: PowerMockito.mockStatic(BrowfishUtils) PowerMockito.when(BrowfishUtils.encrypt(Mockito.any())) .thenThrow(new NoSuchPaddingException()) // expect: assertThatExceptionOfType(NoSuchPaddingException).isThrownBy({ BrowfishUtils.encrypt("test") }) } }
テストを実行すると成功しました。
Groovy + Spock + PowerMock で static メソッドをモック化してテストを書いてみる
書いたテストを Spock で書き直します。Web で調べると Spock で PowerMock を使いたい場合には PowerMockRule を使うよう書かれている記事を見かけるのですが、PowerMockとJUnitのRuleを使うときのメモ には複数の @Rule が指定されている場合、PowerMockRule は動かなくなる、とも書かれていました。PowerMockRule って使えないのでは?。。。と思いましたが、まずは試してみます。
PowerMockRule を見て build.gradle を以下のように変更します。変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
dependencies { .......... // PowerMock testCompile("org.powermock:powermock-module-junit4:1.6.6") testCompile("org.powermock:powermock-api-mockito:1.6.6") testCompile("org.powermock:powermock-module-junit4-rule:1.6.6") testCompile("org.powermock:powermock-classloading-xstream:1.6.6") }
まずは JUnit4 のまま PowerMockRule を使う方式へ変えてみます。先程作成した 「異常処理のテスト」static class を以下のように変更します。
@RunWith(JUnit4) @PrepareForTest(BrowfishUtils) static class 異常処理のテスト { @Rule public PowerMockRule powerMock = new PowerMockRule() @Test void "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() { // setup: PowerMockito.mockStatic(BrowfishUtils) PowerMockito.when(BrowfishUtils.encrypt(Mockito.any())) .thenThrow(new NoSuchPaddingException()) // expect: assertThatExceptionOfType(NoSuchPaddingException).isThrownBy( new ThrowableAssert.ThrowingCallable() { @Override void call() throws Throwable { BrowfishUtils.encrypt("test") } }) } }
テストを実行すると成功します。
「異常処理のテスト」static class を Spock で書き直してみます。
@PrepareForTest(BrowfishUtils) static class 異常処理のテスト extends Specification { @Rule public PowerMockRule powerMock = new PowerMockRule() def "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() { given: PowerMockito.mockStatic(BrowfishUtils) PowerMockito.when(BrowfishUtils.encrypt(Mockito.any())) .thenThrow(new NoSuchPaddingException()) when: BrowfishUtils.encrypt("test") then: thrown(NoSuchPaddingException) } }
テストを実行すると、PowerMockRule 内の処理で NullPointerException が発生して失敗しました。JUnit4 だと動きますが Spock に変えると動きませんね。。。
PowerMockRule ではなく PowerMockRunner を使用する方法に変えてみます。spock.lang.Specification を見ると @RunWith(Sputnik)
と記述されていました。Spock の Runner は Sputnik.class のようなので、以下のように書いてみます。
@RunWith(PowerMockRunner) @PowerMockRunnerDelegate(Sputnik) @PrepareForTest(BrowfishUtils) static class 異常処理のテスト extends Specification { def "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() { given: PowerMockito.mockStatic(BrowfishUtils) PowerMockito.when(BrowfishUtils.encrypt(Mockito.any())) .thenThrow(new NoSuchPaddingException()) when: BrowfishUtils.encrypt("test") then: thrown(NoSuchPaddingException) } }
テストを実行すると、成功はしますが Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
というメッセージが出力されています。動きはするようなので、このまま進めます。
今度は @SpringBootTest
アノテーションを付けて、SampleHelper#encrypt メソッドのテストを書いてみます。
@RunWith(PowerMockRunner) @PowerMockRunnerDelegate(Sputnik) @SpringBootTest @PrepareForTest(BrowfishUtils) static class 異常処理のテスト extends Specification { @Autowired private SampleHelper sampleHelper def "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() { given: PowerMockito.mockStatic(BrowfishUtils) PowerMockito.when(BrowfishUtils.encrypt(Mockito.any())) .thenThrow(new NoSuchPaddingException()) when: sampleHelper.encrypt("test") then: thrown(RuntimeException) } }
テストを実行すると、java.lang.IllegalStateException: Failed to load ApplicationContext
のメッセージが出力されて失敗しました。Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.beans.factory.support.BeanNameGenerator]: Specified class is an interface
というメッセージも出力されています。
Spock ではうまく動作しないので JUnit4 でテストを書いてみる
以下のようにテストを書き直します。
@RunWith(PowerMockRunner) @PowerMockRunnerDelegate(SpringRunner) @SpringBootTest @PrepareForTest(BrowfishUtils) static class 異常処理のテスト { @Autowired private SampleHelper sampleHelper @Test void "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() { // setup: PowerMockito.mockStatic(BrowfishUtils) PowerMockito.when(BrowfishUtils.encrypt(Mockito.any())) .thenThrow(new NoSuchPaddingException()) // expect: assertThatExceptionOfType(RuntimeException).isThrownBy({ sampleHelper.encrypt("test") }) } }
テストを実行すると、こちらも java.lang.IllegalStateException: Failed to load ApplicationContext
のメッセージが出力されて失敗しました。ただし、こちらは Caused by: java.lang.LinkageError: loader constraint violation: loader (instance of org/powermock/core/classloader/MockClassLoader) previously initiated loading for a different type with name "javax/management/MBeanServer"
というメッセージが出力されます。
stackoverflow を検索してみると Mockito + PowerMock LinkageError while mocking system class の記事が見つかりました。@PowerMockIgnore
アノテーションを記述して無視させればよいようです。
テストを以下のように修正します。
@RunWith(PowerMockRunner) @PowerMockRunnerDelegate(SpringRunner) @SpringBootTest @PrepareForTest(BrowfishUtils) @PowerMockIgnore("javax.management.*") static class 異常処理のテスト { @Autowired private SampleHelper sampleHelper @Test void "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() { // setup: PowerMockito.mockStatic(BrowfishUtils) PowerMockito.when(BrowfishUtils.encrypt(Mockito.any())) .thenThrow(new NoSuchPaddingException()) // expect: assertThatExceptionOfType(RuntimeException).isThrownBy({ sampleHelper.encrypt("test") }) } }
テストを実行すると、何もエラー・警告のメッセージは出力されずに成功しました。
まとめ
- Groovy + Spock でテストを書く方法は分かりませんでした。。。 PowerMockRule を使用する記事をよく見かけるのですが、なぜかうまく動作しません。
- Groovy + JUnit4 + PowerMock で動作する方法は分かりました。この方法なら Spock で書いたテストと同じファイル内に書けます。
- build.gradle の dependencies には以下の2行を記述します。バージョン番号は Mockito_maven のページを見て、その時の最新バージョンにします。
testCompile("org.powermock:powermock-module-junit4:1.6.6")
testCompile("org.powermock:powermock-api-mockito:1.6.6")
- static メソッドをモック化するテストを書くクラスに以下のアノテーションを付加します。
@RunWith(PowerMockRunner)
@PowerMockRunnerDelegate(SpringRunner)
@SpringBootTest
@PowerMockIgnore("javax.management.*")
- static メソッドをモック化するテストを書くクラスに、以下のアノテーションでモック化するクラスを記述します。
@PrepareForTest(~)
- テストメソッドの最初に
PowerMockito.mockStatic(...)
でモック化するクラスを宣言し、PowerMockito.when(...).then~(...)
でモック化する static メソッドとモック化した時の挙動を記述します。
- build.gradle の dependencies には以下の2行を記述します。バージョン番号は Mockito_maven のページを見て、その時の最新バージョンにします。
テストクラスは最終的に以下のようになりました。
package ksbysample.webapp.lending import org.junit.Test import org.junit.experimental.runners.Enclosed import org.junit.runner.RunWith import org.mockito.Mockito import org.powermock.api.mockito.PowerMockito import org.powermock.core.classloader.annotations.PowerMockIgnore import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.modules.junit4.PowerMockRunner import org.powermock.modules.junit4.PowerMockRunnerDelegate import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.junit4.SpringRunner import spock.lang.Specification import spock.lang.Unroll import javax.crypto.NoSuchPaddingException import static org.assertj.core.api.Assertions.assertThatExceptionOfType @RunWith(Enclosed) class SampleHelperTest { @SpringBootTest static class 正常処理のテスト extends Specification { @Autowired private SampleHelper sampleHelper @Unroll def "SampleHelper.encrypt(#str) --> #result"() { expect: sampleHelper.encrypt(str) == result where: str || result "test" || "bjMKKlhqu/c=" "xxxxxxxx" || "ERkXmOeArBKwGbCh+M6aHw==" } } @RunWith(PowerMockRunner) @PowerMockRunnerDelegate(SpringRunner) @SpringBootTest @PrepareForTest(BrowfishUtils) @PowerMockIgnore("javax.management.*") static class 異常処理のテスト { @Autowired private SampleHelper sampleHelper @Test void "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() { // setup: PowerMockito.mockStatic(BrowfishUtils) PowerMockito.when(BrowfishUtils.encrypt(Mockito.any())) .thenThrow(new NoSuchPaddingException()) // expect: assertThatExceptionOfType(RuntimeException).isThrownBy({ sampleHelper.encrypt("test") }) } } }
PowerMock で void のメソッドをモック化するには?
戻り値が void の場合、モック化の書き方が上とは変わります。SampleHelper クラス、BrowfishUtils クラスに以下のように void のメソッドを追加して、
@Component public class SampleHelper { .......... public void noReturn(String arg) { BrowfishUtils.noReturn(arg); } }
public class BrowfishUtils { .......... public static void noReturn(String arg) { } }
BrowfishUtils#noReturn が RuntimeException を throw するようモック化してテストを書くと以下のようになります。
@RunWith(PowerMockRunner) @PowerMockRunnerDelegate(SpringRunner) @SpringBootTest @PrepareForTest(BrowfishUtils) @PowerMockIgnore("javax.management.*") static class voidメソッドをモック化するテスト { @Autowired private SampleHelper sampleHelper @Test void "SampleHelper_noReturnを呼ぶとRuntimeExceptionをthrowする"() { // setup: PowerMockito.mockStatic(BrowfishUtils) PowerMockito.doThrow(new RuntimeException()) .when(BrowfishUtils.class, "noReturn", Mockito.any()) // expect: assertThatExceptionOfType(RuntimeException).isThrownBy({ sampleHelper.encrypt("test") }) } }
このテストを実行すると成功します。
履歴
2017/06/15
初版発行。
2017/07/09
* Groovy では “.class” の記述は不要だったので削除しました(例:@RunWith(Enclosed.class)
→ @RunWith(Enclosed)
)。画像でキャプチャしていたソースは除きます。
* Java の lambda 式は Groovy の Closure で書けることを知ったので、isThrownBy メソッド内の記述を匿名クラスから Groovy の Closure へ修正しました。