PowerMockitoでテスト中にjava.lang.IllegalStateException: Reason: [source error] に遭遇した

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なメソッドのモック化する方法はこのあたりに詳しい。

kubo-shogun.com

 

上記サイトを参考に実装したテストクラスがこちら。

@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]」 で検索してみる。

 

いくつかそれらしい回答を見つけた。javassistPowerMockの組み合わせが良くない場合があるらしい。上記のサンプルコードでは特にjavassistを利用していないが、実際のコードでは別の場所で使っているため、それが問題を引き起こしているようだ。

stackoverflow.com

stackoverflow.com

 

いくつかのバージョンを試した結果、最終的には以下のように修正することでテストが通ることを確認できた。

  • 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でテストを実行するとIllegalStateExceptionClassNotFoundExceptionが発生する場合がある
  • javassistのバージョンを上げることで解消できるかも
  • 実装したサンプルコードは以下のリポジトリにまとめてある

github.com