Powermock + SpringでJUnitをやってみた
Spring Bootはやり方があるらしいが、素のSpring Frameworkだったら、どうやるんだろう。
ぜんぜん、ぐぐっても出てこないので、はまりつつ、やってみました。
まず、アノテーション設定はこうします
- @RunWithと@PowerMockRunnerDelegateで、JUnit + PowerMock + Spring の組みあわせる。
- @PowerMockIgnoreしないとAESのエラー出る。
- @PrepareForTestにMockにかかわるものを追加しておいた方がよいかも?
@ContextConfiguration(locations = { "classpath:PAHT/TO/application-context.xml"}) @RunWith(PowerMockRunner.class) @PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class) @PowerMockIgnore ("javax.crypto.*") @PrepareForTest({Target.class}) public class MockTest { ...
テスト対象のクラスTargetを@Autowiredつけて、ユニットテストクラスにDIする。
Targetクラスの中で、webServiceTemplateを使用して、外部システムのAPIを呼び出しているので、これをMockと差し替えたい。
だから、@MockをつけてMockしたいクラスとして宣言する。
public class MockTest { @Autowired private Target target; @Mock WebServiceTemplate webServiceTemplate;
Targetクラスの中で、webServiceTemplateは、実際は、`Response response = webServiceTemplate.marshalSendAndReceive(xxx, xxx)`というように使われています。
このメソッドをMockと差し替えて、意図した戻り値を返すように仕向けます。
@Test public void test() throws Exception { Response response = new Response(); response.setResultCode("XXXXX"); PowerMockito.doReturn(response).when(webServiceTemplate).marshalSendAndReceive(anyString(), any()); Whitebox.setInternalState(target, WebServiceTemplate.class, webServiceTemplate);
そして、targetを実行します。
Result res = target.excute(); Assert.assertEquals(XXX, res); }
エラーが出てしまった。
これがイケナイらしいです。
Whitebox.setInternalState(target, WebServiceTemplate.class, webServiceTemplate);
なにをしているかというと、TargetクラスのPrivateフィールド変数webServiceTemplateを、
モック化したwebServiceTemplateで差し替えようとしています。
ところが、TargetクラスにPrivateフィールド変数webServiceTemplateが存在しないと言われました。
ん?そんなはずはない。
と思い、デバッガで、targetに、何が格納されているか見たら、Proxy〜みたいな見慣れないものが出ました。
それで、思いあたったのが、Spring AOP。
実行時は、targetの中身は本来のインスタンスをラップしたインスタンスが格納されています。
困ったあげく、本来のインスタンスを取得できないか調べていたら、ありました。
targetが、AOPによって注入されたインスタンスだったら、そこから、本来のインスタンスをgetできるはず。
しかも、何個もAOPの処理を挟んでいるので、再帰的に剥がしていく必要がありました。
static class AopUtils extends org.springframework.aop.support.AopUtils { @SuppressWarnings("unchecked") public static <T> T unwrapProxy(Object candidate) { try { if (isAopProxy(candidate) && (candidate instanceof Advised)) { return (T) unwrapProxy(((Advised) candidate).getTargetSource().getTarget()); } } catch (Exception e) { throw new IllegalStateException("Failed to unwrap proxied object.", e); } // else return (T) candidate; } }
で、このようにしたら、うまくいきました。
Whitebox.setInternalState(AopUtils.unwrapProxy(taget), WebServiceTemplate.class, webServiceTemplate);
jmockitのDeencapsulationがdeletedされてた
jmockitには、DeencapsulationというマジカルなUtilityクラスがあります。
通常は、アクセスできないフィールドやメソッドにアクセスすることができちゃう禁断のクラスです。
このクラスが、最新のバージョンで削除されていることが発覚しました。
ことの経緯としては、
1. Java 1.6からJava 1.8にVersion Upしたら
2. jmockitが動かない。内部で、JDKのVersionを判定しているようだ。
3. jmockitをVersion Upしたら
4. DeencapsulationがNoClassDef...
となって、まじか.. ぼくの大すきなDeencapsulationが・・・
とっても便利に使っていたのにどうしよぉおおおパキャマラド パキャマラド
パオパオパオパオ パオパオパオパオと考えていたら、
そうだ。似たものをつくって代用しちゃおうということになりました。
package util; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import org.apache.commons.lang.reflect.FieldUtils; /** * Reflection Utilities. * * */ public class ReflectionUtils { /** * デフォルトコンストラクタの禁止. */ private ReflectionUtils() { } /** * 対象のスタティックフィールド変数を指定された値で書き換えます. * * @param cls 書き換えたいフィールド変数を持つクラス(スタティック変数の変更. static finalもOK. ) * @param fieldName フィールド変数名 * @param value 値 * @throws NoSuchFieldException * @throws SecurityException * @throws IllegalArgumentException * @throws IllegalAccessException */ public static void writeDeclaredStaticField(@SuppressWarnings("rawtypes") Class cls, String fieldName, Object value) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { Field field = FieldUtils.getDeclaredField(cls, fieldName, true); setFinalStatic(field, value); } /** * * Deencapsulation#setFieldの代用(インスタンス変数の変更。privateもOK.) * * 対象インスタンスのフィールド変数を指定された値で書き換えます. * * @param cls 書き換えたいフィールド変数を持つクラス * @param fieldName フィールド変数名 * @param value 値 * @throws IllegalAccessException */ public static void writeField(Object target, String fieldName, Object value) throws IllegalAccessException { FieldUtils.writeField(target, fieldName, value, true); } private static void setFinalStatic(Field field, Object newValue) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { field.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(null, newValue); } }
しばらく、javaから離れていたので、トレンドがよくわかりませんが
今、Mockライブラリといえば、PowerMockitoが主流なのでしょうかね。