Mockitoでfinalクラスをモック化

2013-06-28T00:00:00+00:00 Java

※解釈が正しいかは微妙なのであしからず

通常Mockitoではfinalクラス等はモック化出来ない。やってもエラーになるのだけど、それを無茶ぶりでやっちゃう方法がある模様

  • ClassLoaderを継承したクラスを作る。loadClassをオーバーライドしてモック化を対象とするクラスのターゲットにfinal修飾子を削除したクラスを定義させてそのClassを返すように実装
  • Mockito.mockで指定するClassを上記のClassLoader経由で取得する。

っていう工程を行う事で可能らしい。後述する参考ではPowerMockitoを使ってる模様。とりまぁやってみるか

モックするクラス

package sample;

public interface Sample {
    String say();
}

なインターフェースがあって

package sample;

public final class SampleImpl implements Sample {
    public String say() {
        return "hoge";
    }
}

というようなクラスを対象にする

ClassLoaderを継承したクラスを作成

package sample;

import java.lang.reflect.Modifier;

import javassist.ClassPool;
import javassist.CtClass;

public class SampleClassLoader extends ClassLoader {

    private ClassPool pool;

    public SampleClassLoader() {
        pool = ClassPool.getDefault();
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (name.equals("sample.SampleImpl")) {
            return findClass(name);
        }

        return super.loadClass(name);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = null;

        try {
            CtClass clazz = pool.get(name);
            clazz.setModifiers(clazz.getModifiers() & ~Modifier.FINAL);

            b = clazz.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (b == null) {
            throw new ClassNotFoundException();
        }

        return defineClass(name, b, 0, b.length);
    }
}

利用するにはjavassistが必要

テストを書く

package sample;

import org.junit.Before;
import org.junit.Test;

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class SampleTestCase {

    private ClassLoader classLoader;

    @Before
    public void setUp() throws Exception {
        classLoader = new SampleClassLoader();
    }

    @SuppressWarnings("unchecked")
    @Test
    public void test1() throws Exception {
        Class<SampleImpl> clazz = (Class<SampleImpl>)classLoader.loadClass("sample.SampleImpl");

        Sample sample = (Sample)mock(clazz);
        when(sample.say()).thenReturn("fuga");

        assertThat(sample.say(), is("hoge")); // fail
    }
}

っていう感じ。で

when(sample.say()).thenCallRealMethod();

とかにしとけばテストは通る。でテストスタックトレース的には

java.lang.AssertionError:
Expected: is "hoge"
     but: was "fuga"

んで

Sample sample = (Sample)mock(SampleImpl.class);

なんていうのをやるとどうなるか

org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class sample.SampleImpl
Mockito cannot mock/spy following:
  - final classes
  - anonymous classes
  - primitive types

というようにfinal classはmock出来ねーよって怒られる。っていうような感じでfinalクラスをmock化する方法があったのでやってみた程度で

参考: http://www.objectpartners.com/2012/03/15/how-to-mock-final-classes-in-unit-tests

追記

続きとなる別アプローチを書きました

FactoryGirlとDatabaseCleaner PullToRefreshでListFragment