Mockitoでfinalクラスをモック化 (2)
例えば
package sample;
public final class Sample {
public String say() {
return "hoge fuga foobar";
}
}
なんていうfinalクラスがあったとして
package sample;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
public class SampleTest {
@Test
public void test() {
Sample sample = mock(Sample.class);
when(sample.say()).thenReturn("hoge");
assertThat(sample.say(), is("hoge fuga foobar"));
verify(sample).say();
}
}
なんていうテストを普通にぶちかますと
org.mockito.exceptions.base.MockitoException: Cannot mock/spy class sample.Sample Mockito cannot mock/spy following: - final classes - anonymous classes - primitive types
っていうようにfinalクラスはモック化出来ねーよって怒られる訳ですが。で前回だと独自のクラスローダーを利用してjavassistを使ってfinal modifierを消したクラスバイトコードを動的にロードするっていう方式だったんですが、これだとこのクラスローダーからロードしないといけないとかめんどくさいよねっていう事で色々調べつつやってみた。無論上記のコードでテストは一切書き換えないっていう条件付き
その前に「このクラスはfinalクラスでモック出来るようにするよ」っていうのを識別させる為にアノテーションを利用
package sample;
public @interface FinalMock {
}
なアノテーションインターフェースを作ってSampleクラスにこのアノテーションを付与しておく
あとはクラスロードな仕組み辺りでこのアノテーションが付与されてるのだけfinal modifierを消して動的にロードするっていう方式を採用する
ClassFileTransformerを実装したクラスを作る
簡単に言うと-javaagentな仕組みを使ってクラスからfinal modifierを消してロードするっていう仕組みを作るだけ
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.FinalMock;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
public class SampleClassTransformer implements ClassFileTransformer {
private static final Logger logger = LoggerFactory.getLogger(SampleClassTransformer.class);
private ClassPool pool;
public SampleClassTransformer() {
pool = ClassPool.getDefault();
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] b = null;
try {
String name = className.replace("/", ".");
CtClass clazz = pool.get(name);
if (clazz.hasAnnotation(FinalMock.class)) {
clazz.setModifiers(clazz.getModifiers() & ~Modifier.FINAL);
b = clazz.toBytecode();
logger.info("processed: {}", clazz.getName());
}
} catch (NotFoundException e) {
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return b;
}
}
で-javaagentな仕組みを使うので
import java.lang.instrument.Instrumentation;
public class JavaAgent {
public static void premain(String args, Instrumentation inst) {
inst.addTransformer(new SampleClassTransformer(), inst.isRetransformClassesSupported());
}
}
っていうようなクラスを作る。んでMETA-INF/MANIFEST.MFで
Manifest-Version: 1.0
Premain-Class: JavaAgent
を定義して、上記のJavaAgentクラスとSampleClassTransformerクラスをぶち込んでjar化する
終わり。あとは実行時に-javaagent:sample.jar(作成したjarファイル)でテストを実行すると
java.lang.AssertionError: Expected: is "hoge fuga foobar" but: was "hoge"
っていうように「when(sample.say()).thenReturn("hoge")」としているのでまぁどっちにしろテストは成功しないけど、モック化してメソッドが返す値をfinalクラスでもぶち込んだり出来るっていう訳