Javaでテストを書いていて、System.currentTimeMillis()
というおなじみのメソッドをモックしようとしたら例外が出たのでメモしておく。
[前提条件]
使用しているJava、ライブラリのバージョンなどは以下の通り。諸々ちょっと古いのは色々と事情がありまして……。
$ java -version openjdk version "1.8.0_242" OpenJDK Runtime Environment Corretto-8.242.08.2 (build 1.8.0_242-b08) OpenJDK 64-Bit Server VM Corretto-8.242.08.2 (build 25.242-b08, mixed mode)
<dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.19.0-GA</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.10.19</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>1.7.4</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito</artifactId> <version>1.7.4</version> <scope>test</scope> </dependency> </dependencies>
テスト対象のクラスはこちら。テストしたいメソッドはtimeToLive()
の部分。
public class MyUtil { public static boolean isEqualOrBefore( final ZonedDateTime a, final ZonedDateTime b) { return a.isEqual(b) || a.isBefore(b); } public static long timeToLive(final int hours) { return TimeUnit.HOURS.toSeconds(hours) + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); } }
staticなメソッドのモック化する方法はこのあたりに詳しい。
上記サイトを参考に実装したテストクラスがこちら。
@RunWith(PowerMockRunner.class) @PrepareForTest({ System.class, MyUtil.class }) public class MyUtilTest { @Test public void testTimeToLive() { PowerMockito.mockStatic(System.class); when(System.currentTimeMillis()).thenReturn(5000L); long actual = MyUtil.timeToLive(1); assertEquals(3605, actual); } }
[発生した問題]
前提条件で記載したJUnitテストを実行すると、次のようなエラーが発生した(長すぎるのでスタックトレースは部分的に省略した)。
initializationError(com.kdnakt.MyUtilTest) Time elapsed: 0.006 sec <<< ERROR! java.lang.IllegalStateException: Failed to transform class with name com.kdnakt.MyUtil. Reason: [source error] isEqual(java.time.chrono.ChronoZonedDateTime) not found in java.time.ZonedDateTime at org.powermock.core.classloader.MockClassLoader.loadMockClass(MockClassLoader.java:296) at org.powermock.core.classloader.MockClassLoader.loadModifiedClass(MockClassLoader.java:204) at org.powermock.core.classloader.DeferSupportingClassLoader.loadClass1(DeferSupportingClassLoader.java:89) at org.powermock.core.classloader.DeferSupportingClassLoader.loadClass(DeferSupportingClassLoader.java:79) at java.lang.ClassLoader.loadClass(ClassLoader.java:352) (略) Caused by: javassist.CannotCompileException: [source error] isEqual(java.time.chrono.ChronoZonedDateTime) not found in java.time.ZonedDateTime at javassist.expr.MethodCall.replace(MethodCall.java:241) at org.powermock.core.transformers.impl.AbstractMainMockTransformer$PowerMockExpressionEditor.edit(AbstractMainMockTransformer.java:370) at javassist.expr.ExprEditor.loopBody(ExprEditor.java:192) at javassist.expr.ExprEditor.doit(ExprEditor.java:91) at javassist.CtClassType.instrument(CtClassType.java:1431) at org.powermock.core.transformers.impl.ClassMockTransformer.transformMockClass(ClassMockTransformer.java:65) at org.powermock.core.transformers.impl.AbstractMainMockTransformer.transform(AbstractMainMockTransformer.java:62) at org.powermock.core.classloader.MockClassLoader.loadMockClass(MockClassLoader.java:277) ... 61 more Caused by: compile error: isEqual(java.time.chrono.ChronoZonedDateTime) not found in java.time.ZonedDateTime at javassist.compiler.TypeChecker.atMethodCallCore(TypeChecker.java:749) at javassist.compiler.TypeChecker.atCallExpr(TypeChecker.java:695) (略) ... 68 more
[修正方法]
自分の実装したクラス名は関係ないので、「java.lang.IllegalStateException: Failed to transform class Reason: [source error]」 で検索してみる。
いくつかそれらしい回答を見つけた。javassist
とPowerMock
の組み合わせが良くない場合があるらしい。上記のサンプルコードでは特にjavassistを利用していないが、実際のコードでは別の場所で使っているため、それが問題を引き起こしているようだ。
いくつかのバージョンを試した結果、最終的には以下のように修正することでテストが通ることを確認できた。
- PowerMock:1.7.4
- Mockito:1.10.19
- Javassist:3.19.0-GA → 3.20.0-GA
ただし、途中で、javassistの3.23.0-GAを利用すると以下の問題に遭遇した。
java.lang.NoClassDefFoundError: java/lang/StackWalker$Option at javassist.util.proxy.DefineClassHelper$SecuredPrivileged$1.<init>(DefineClassHelper.java:67) at javassist.util.proxy.DefineClassHelper$SecuredPrivileged.<clinit>(DefineClassHelper.java:39) at javassist.util.proxy.DefineClassHelper.<clinit>(DefineClassHelper.java:186) at javassist.ClassPool.toClass(ClassPool.java:1120) at javassist.CtClass.toClass(CtClass.java:1319) at org.powermock.core.ClassReplicaCreator.createClassReplica(ClassReplicaCreator.java:63) at org.powermock.api.mockito.internal.mockcreation.DefaultMockCreator.createMock(DefaultMockCreator.java:64) at org.powermock.api.mockito.internal.mockcreation.DefaultMockCreator.mock(DefaultMockCreator.java:46) at org.powermock.api.mockito.PowerMockito.mockStatic(PowerMockito.java:71) at com.kdnakt.MyUtilTest.testTimeToLive(MyUtilTest.java:21) (略) Caused by: java.lang.ClassNotFoundException: java.lang.StackWalker$Option at java.net.URLClassLoader.findClass(URLClassLoader.java:382) at java.lang.ClassLoader.loadClass(ClassLoader.java:419) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352) at java.lang.ClassLoader.loadClass(ClassLoader.java:352) at org.powermock.core.classloader.DeferSupportingClassLoader.loadClass1(DeferSupportingClassLoader.java:87) at org.powermock.core.classloader.DeferSupportingClassLoader.loadClass(DeferSupportingClassLoader.java:79) at java.lang.ClassLoader.loadClass(ClassLoader.java:352) ... 48 more
StackWalkerはJava 9で導入されたクラスだ。どうやらJavassistのバージョン3.23.0-GAはJava 9以上が必要らしい。それ以降の、3.24.0-GAから3.27.0-GAまでのバージョンではこの例外は発生しなかったので、修正されている模様。
[まとめ]
- PowerMock+javassistでテストを実行すると
IllegalStateException
やClassNotFoundException
が発生する場合がある - javassistのバージョンを上げることで解消できるかも
- 実装したサンプルコードは以下のリポジトリにまとめてある