SharedPreferencesを使った、カンタンな KVS をデータソースに持つモデルを作った時、そのモデルをテストしようとすると、どうしても SharedPreferencesをモックしたくなります。
SharedPreferencesは、その取得方法によって、2 種類のものが存在します。
1 つは、Context#getSharedPreferences(String, int)で、自分で好きに名前を決めたSharedPreferences。もう一つは、PreferenceManager#getDefaultSharedPreferences(Context)で、システムによって生成されたSharedPreferencesです。
SharedPreferencesそのものはインタフェースですので、SharedPreferencesのインスタンスをモックするのであれば、以下のようにモックのクラスを作って、Contextから返すインスタンスをそのモックのオブジェクトにすれば良いだけです。
import android.content.SharedPreferences;
import java.util.Map;
import java.util.Set;
public class MockSharedPreferences implements SharedPreferences {
@Override
public boolean contains(String key) {
throw new UnsupportedOperationException();
}
@Override
public Editor edit() {
throw new UnsupportedOperationException();
}
@Override
public Map<String, ?> getAll() {
throw new UnsupportedOperationException();
}
@Override
public boolean getBoolean(String key, boolean defValue) {
throw new UnsupportedOperationException();
}
@Override
public float getFloat(String key, float defValue) {
throw new UnsupportedOperationException();
}
@Override
public int getInt(String key, int defValue) {
throw new UnsupportedOperationException();
}
@Override
public long getLong(String key, long defValue) {
throw new UnsupportedOperationException();
}
@Override
public String getString(String key, String defValue) {
throw new UnsupportedOperationException();
}
@Override
public Set<String> getStringSet(String arg0, Set<String> arg1) {
throw new UnsupportedOperationException();
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
throw new UnsupportedOperationException();
}
public static class MockEditor implements Editor {
@Override
public void apply() {
throw new UnsupportedOperationException();
}
@Override
public Editor clear() {
throw new UnsupportedOperationException();
}
@Override
public boolean commit() {
throw new UnsupportedOperationException();
}
@Override
public Editor putBoolean(String key, boolean value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putFloat(String key, float value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putInt(String key, int value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putLong(String key, long value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putString(String key, String value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putStringSet(String arg0, Set<String> arg1) {
throw new UnsupportedOperationException();
}
@Override
public Editor remove(String key) {
throw new UnsupportedOperationException();
}
}
}
public class MockPreferenceContext extends MockContext {
private Context mTestContext;
public MockPreferenceContext(Context context) {
mTestContext = context;
}
@Override
public String getPackageName() {
return mTestContext.getPackageName();
}
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return new MockSharedPreferences();
}
}
このようにしておけば、どの方法でSharedPreferencesを取得したとしても、少なくともインスタンスをモックした状態でテストができます。
さて、SharedPreferencesをモックするということは、実データへの書き込み等もすべて無視して、自分が好きに書き換えた振る舞いしかしないことになります。これを、実データへの書き込み等の振る舞いを変えないで、しかし、本番データをいじらないようにするようなテストを書く場合、どうしたらよいかということが問題になります。
SharedPreferencesは、Context#getSharedPreferences(String, int)で取得する場合は、引数に与えた名前をファイル名に、PreferenceManager#getDefaultSharedPreferences(Context)で取得する場合は、{$package_name}_preferences.xmlをファイル名にしてデータをストレージに保存しています。
つまり、Context#getSharedPreferences(String, int)を使ってSharedPreferencesを取得する場合、Context#getSharedPreferences(String, int)をモックした上で、引数の文字列に適当な文字列を付け加えて、親へ丸投げしてしまえば、テスト用にファイルを用意した状態で、振る舞いを得ることなくSharedPreferencesのインスタンスを生成できるようになります。
public class MockPreferenceContext extends MockContext {
private Context mTestContext;
public MockPreferenceContext(Context context) {
mTestContext = context;
}
@Override
public String getPackageName() {
return mTestContext.getPackageName();
}
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mTestContext.getSharedPreferences(name + ".test", mode);
}
}
一方、PreferenceManager#getDefaultSharedPreferences(Context)は static なメソッドです。
つまり、Mockito とか使わない限り、このメソッドの振る舞いをモックすることは叶いません。
ただ、先にも述べたように、このメソッドは、{$package_name}_preferences.xmlをファイル名にしたSharedPreferencesを取得するように作られています。つまり、以下のようにモックしたContextを渡してやれば、テスト用にファイルをモックしたものが得られることになります。
public class MockPreferenceContext extends MockContext {
private Context mTestContext;
public MockPreferenceContext(Context context) {
mTestContext = context;
}
@Override
public String getPackageName() {
return mTestContext.getPackageName() + ".test";
}
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mTestContext.getSharedPreferences(name + ".test", mode);
}
}
テスト用に、データベースを別の場所に向けてくれるIsolatedContextや、ファイルストレージを別の場所にむけてくれるRenamingDelegatingContextなどがある中、SharedPreferencesのファイルを別のところに向けてくれるContextが無いのはなかなか苦労しますが、一度作ってしまえばどこでも使えるようになるので便利ですね。