2015/05/09

Android : Dagger

DaggerはSquare製のAndroidに特化された軽量で高速なDI Framework.

@Injectによる依存関係の構築

Google製のGuiceと比べてパフォーマンスが数倍速い
@Injectでアノテートされたクラス間に依存関係をもたせ, ObjectGraphを構築していく.
@Injectのアノテートはメソッドとフィールドに対して有効.

class CoffeeMaker {
  @Inject Heater heater;
  ...
}

class Thermosiphon implements Pump {
  private final Heater heater;

  @Inject
  Thermosiphon(Heater heater) {
    this.heater = heater;
  }
  ...
}

まずHeaderへの@InjectアノテートによりHeaderの引数なしコンストラクタが呼ばれてインスタンス化される.
Thermosiphonクラスも@Injectでアノテートされた引数にHeaderを受け取るコンストラクタを持つ.
ObjectGraphはこれらのクラス間に依存性を持たせ, Thermosiphonのコンストラクタに先ほど@InjectアノテートされたHeaterインスタンスを渡す.

@Providesによる依存性の提供

@injectアノテーションでは次のことができない.

  • インタフェースを生成できない.
  • 3rdParty製のクラスをアノテートできない.
  • 初期化・構築されたオブジェクトを返却できない.

これらの機能を設けたい場合は@Providesアノテーションを使える.

下記はHeaterの要求に対してElectricHeaderを返す. つまりインタフェースを依存性として注入できる.

@Provides Heater provideHeater() {
  return new ElectricHeater();
}

3rdParty製のクラスに対して@Injectアノテートを付けることはできない.
@Providesを使えば3rdParty製クラスを返却するprovideメソッドを作ることでこれを実現できる.
またセットアップ済みのインスタンスをprovideしたい場合があるかもしれない. この場合にも@Provideは有効に作用する.

制約として, @Providesアノテーションメソッドを持つクラスは@Moduleアノテートされている必要がある.

この他にも多くのオプションがDaggerには備わっている.

2015/04/28

Android : AndroidStudio + PowerMock でstaticメソッドをmockする

確認環境

  • Android Studio 1.1
  • Android Gradle Plugin 1.1
  • JUnit 4.12
  • PowerMock 1.6.2

はじめに

Android Studio 1.1(android gradle plugin 1.1)からUnitTestがサポートされた.
今回はPowerMockを使ってstatic methodをモックするUnitTestを書いた.

Android StudioでUnitTestを開始するには下記を参考.

UnitTestの準備が整ったならモジュールのbuild.gradleに下記の依存ライブラリを記述する.

    // UnitTestにはtestCompileキーワードを使用
    testCompile 'junit:junit:4.12'

    // PowerMockのライブラリを指定
    testCompile 'org.powermock:powermock-module-junit4:1.6.2'
    testCompile 'org.powermock:powermock-api-mockito:1.6.2'

次はoptionだが, Android StudioのUnitTestは実行時にandroid.jarのモックとなるmockable-android*.jarを作成する.
ここで定義されるメソッドスタブはすべてRuntimeExceptionをスローするように実装される.
これをExceptionではなくDefault value(false etc.)を返却するように変更するには下記を指定する.

    testOptions {
        unitTests.returnDefaultValues = true
    }

あとはいつもの方法でPowerMockを使えばOK.

  • TestRunnerにPowerMockRunnerを指定
    @RunWith(PowerMockRunner.class)
    public class MyTestCase extends TestCase {
  • TestCaseを継承したクラスにアノテーションを追加
    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Static.class) // Staticメソッドをモックするクラスを指定(,で複数指定可)
    public class MyTestCase extends TestCase {

テストコード

    @Test
    public void testMockStaticMethod() throws Exception {
        assertFalse("オリジナルはfalseを返す", Target.staticBoolean());

        PowerMockito.mockStatic(Target.class);
        when(Target.staticBoolean()).thenReturn(true);
        assertTrue("モックはtrueを返す", Target.staticBoolean());
        verifyStatic(times(1));
    }

    @Test
    public void testMockConstructor() throws Exception {
        assertEquals("Valueは引数なしのコンストラクタで初期化される",
                new Value(), new Target().getValue());

        Value mockValue = new Value(100);
        assertNotEquals("引数ありで初期化されたValueとでは等価ではなくなる",
                mockValue, new Target().getValue());

        PowerMockito.whenNew(Value.class).withNoArguments().thenReturn(mockValue);
        assertEquals("Valueの引数なしコンストラクタでモックが返される",
                mockValue, new Target().getValue());
        verifyNew(Value.class).withNoArguments();

        mockValue = new Value(500);
        assertEquals("Valueの引数ありコンストラクタはモックされていない",
                mockValue, new Value(500));

        mockValue = new Value(1000);
        PowerMockito.whenNew(Value.class).withArguments(anyInt()).thenReturn(mockValue);
        assertEquals("Valueの引数ありコンストラクタもモックされる",
                mockValue, new Target(500).getValue());
        verifyNew(Value.class, atLeastOnce()).withArguments(anyInt());
    }

    @Test
    public void testLogger() throws Exception {
        PowerMockito.mockStatic(Log.class);
        Mockito.when(Log.isLoggable(anyString(), anyInt())).thenReturn(true);
        assertTrue("モックしてtrueを返す", Log.isLoggable("mocktag", Log.VERBOSE));

        assertTrue(BuildConfig.DEBUG);

        // static finalな定数をreflectionで変更.
        Field field = BuildConfig.class.getField("DEBUG");
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(null, false);

        assertFalse(BuildConfig.DEBUG);
    }

以上.