四畳半の秘密基地

さあ、今日はどんな実験をしよう

MENU

Androidでテスト入門してみた②

前回は、基本的なEspressoの使い方を学びました。
今回は試合回数を保存する機能をSharedPreferencesを使って実装します。

その前にまずは、UIが余りにもひどいので整えます。

ソースはこちらを参照。

f:id:k-seito:20160123111037p:plain

UI改修後はこんな感じです。カラーもスプラトゥーンを意識して入れました。



それではSharedPreferencesの実装をしていきたいと思います。
実装自体は
・呼び出し時に保存している値を取得して表示
・カウントアップ・ダウンボタンを押されたら値を変更してその値を保存
しているだけです。

    public void initCounter() {
        int count = mSharedPreferences.getInt(COUNT, 0);
        mTextCounter.setText(String.valueOf(count));
    }

    @OnClick(R.id.count_up_button)
    public void clickCountUp(View view) {
        int count = Integer.parseInt(mTextCounter.getText().toString()) + 1;
        mTextCounter.setText(String.valueOf(count));
        saveCount(count);
    }

    @OnClick(R.id.count_down_button)
    public void clickCountDown(View view) {
        int count = Integer.parseInt(mTextCounter.getText().toString()) - 1;
        if (count < 0) {
            return;
        }
        mTextCounter.setText(String.valueOf(count));
        saveCount(count);
    }

    private void saveCount(int count) {
        mSharedPreferences.edit().putInt(COUNT, count).commit();
    }


そしてテストの方はなかなか良いプラクティスが見つからなくて迷った結果、Dagger2を使うことになりました。
Dagger自体使うのが初めてであまり理解できてないので勘違いしていたらすいません。
まず、やってることはテスト時にSharedPreferencesの参照先を変えて通常起動時とカウントの保存先を分けてるだけです。
言ってしまえば簡単なんですが、ここまでたどり着くのに色々試行錯誤しました・・・。その結果、既にTDDが破綻して記事のタイトルを変えることにしました(泣)

まずは定義
app/build.gradle

    apt "com.google.dagger:dagger-compiler:2.0"
    compile 'com.google.dagger:dagger:2.0'
    compile 'org.glassfish:javax.annotation:10.0-b28'

次に本番で使用するSharedPreferencesを生成するモジュール

@Module
public class PreferencesModule {

    Context context;

    public PreferencesModule(Context context) {
        this.context = context;
    }

    @Singleton
    @Provides
    SharedPreferences providePreferences() {
        return context.getSharedPreferences(MyApplication.PREF, Context.MODE_PRIVATE);
    }
}

テスト時は下のモジュールを使用します。

@Module
public class MockPreferencesModule {
    public static final String POSTFIX = "_test";

    Context context;

    public MockPreferencesModule(Context context) {
        this.context = context;
    }

    @Singleton
    @Provides
    SharedPreferences provideSharedPreferences() {
        return context.getSharedPreferences(MyApplication.PREF + POSTFIX, Context.MODE_PRIVATE);
    }
}

上記Moduleを束ねる?Componentのベースとなるインターフェースを準備します。

public interface PrefsComponent {
    void inject(MainActivity mainActivity);
}

これをまた本番用・テスト用に分けます。

本番用

@Singleton
@Component(modules = PreferencesModule.class)
public interface AppPrefsComponent extends PrefsComponent {
}

テスト用

    @Singleton
    @Component(modules = MockPreferencesModule.class)
    public interface TestComponent extends PrefsComponent {
        void inject(MainActivityTest mainActivityTest);
    }

Componentのinject内で定義しているクラスの@Injectがついてるメンバ変数にModule内の@Provideをつけてるメソッドの戻り値を返すって感じでしょうか。文章にすると分かりづらいですね・・・

あとはテストの@Beforeで使用するComponentを差し替えることでテスト用のSharedPreferencesを使うようにしました。
保存機能を入れると前回書いたテストにも影響が出てしまったのですべてのテストを修正する必要がありました。最終系は下記のようになりました。

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityTest {

    @Inject
    SharedPreferences mPrefs;

    @Singleton
    @Component(modules = MockPreferencesModule.class)
    public interface TestComponent extends PrefsComponent {
        void inject(MainActivityTest mainActivityTest);
    }

    @Rule
    public ActivityTestRule<MainActivity> mActivityRule =
            new ActivityTestRule<>(MainActivity.class, true, false);

    @Before
    public void setUp() {
        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
        MyApplication application = (MyApplication) instrumentation.getTargetContext().getApplicationContext();

        TestComponent component = DaggerMainActivityTest_TestComponent.builder()
                .mockPreferencesModule(new MockPreferencesModule(instrumentation.getContext()))
                .build();
        application.setmPrefsComponent(component);
        component.inject(this);

    }

    @Test
    public void checkDefaultText() {

        mActivityRule.launchActivity(new Intent());

        onView(withId(R.id.text_counter)).check(matches(withText("0")));
    }

    @Test
    public void clickCountUp() {

        mActivityRule.launchActivity(new Intent());

        onView(withId(R.id.count_up_button)).perform(click());
        onView(withId(R.id.text_counter)).check(matches(withText("1")));
    }

    @Test
    public void clickCountDown() {

        mActivityRule.launchActivity(new Intent());

        onView(withId(R.id.count_up_button)).perform(click());
        onView(withId(R.id.text_counter)).check(matches(withText("1")));

        onView(withId(R.id.count_down_button)).perform(click());
        onView(withId(R.id.text_counter)).check(matches(withText("0")));

        onView(withId(R.id.count_down_button)).perform(click());
        onView(withId(R.id.text_counter)).check(matches(withText("0")));

    }

    @Test
    public void checkSaveCount() {

        mActivityRule.launchActivity(new Intent());

        onView(withId(R.id.count_up_button)).perform(click());
        onView(withId(R.id.count_up_button)).perform(click());
        onView(withId(R.id.count_up_button)).perform(click());

        int count = mPrefs.getInt(MainActivity.COUNT, 0);
        Assert.assertEquals(3, count);
    }
}

次回はお気に入りのTodoistのAPIを使ってTODOをこなすとポイント追加みたいなことをしてみたいです。
いつになるのか。

一応、ソース置いておきます