如何模拟 android 仪器测试的共享首选项?

bli*_*ard 5 android mockito sharedpreferences android-testing android-espresso

我有一个首选项 util 类可以在一个地方存储和检索共享首选项中的数据。

Prefuutils.java:

public class PrefUtils {
  private static final String PREF_ORGANIZATION = "organization";

  private static SharedPreferences getPrefs(Context context) {
    return PreferenceManager.getDefaultSharedPreferences(context);
  }

  private static SharedPreferences.Editor getEditor(Context context) {
    return getPrefs(context).edit();
  }

  public static void storeOrganization(@NonNull Context context,
      @NonNull Organization organization) {
    String json = new Gson().toJson(organization);
    getEditor(context).putString(PREF_ORGANIZATION, json).apply();
  }

  @Nullable public static Organization getOrganization(@NonNull Context context) {
    String json = getPrefs(context).getString(PREF_ORGANIZATION, null);
    return new Gson().fromJson(json, Organization.class);
  }
}
Run Code Online (Sandbox Code Playgroud)

显示 LoginActivity.java 中 PrefUtils 用法的示例代码

@Override public void showLoginView() {
    Organization organization = PrefUtils.getOrganization(mActivity);
    mOrganizationNameTextView.setText(organization.getName());
  }
Run Code Online (Sandbox Code Playgroud)

build.gradle 中androidTestCompile依赖项列表

// Espresso UI Testing dependencies.
  androidTestCompile "com.android.support.test.espresso:espresso-core:$project.ext.espressoVersion"
  androidTestCompile "com.android.support.test.espresso:espresso-contrib:$project.ext.espressoVersion"
  androidTestCompile "com.android.support.test.espresso:espresso-intents:$project.ext.espressoVersion"

  androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
  androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2:'
Run Code Online (Sandbox Code Playgroud)

src/androidTest/../LoginScreenTest.java

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

@Rule public ActivityTestRule<LoginActivity> mActivityTestRule =
      new ActivityTestRule<>(LoginActivity.class);

  @Before public void setUp() throws Exception {
    when(PrefUtils.getOrganization(any()))
          .thenReturn(HelperUtils.getFakeOrganization());
  } 
}
Run Code Online (Sandbox Code Playgroud)

上面返回的代码fakeOrganization不起作用,mOrganizationNameTextView.setText(organization.getName());在上面的 LoginActivity.java 类中定义的行中运行对登录活动的测试结果为 NullPointerException 。

如何解决上述问题?

bli*_*ard 5

方法一:

SharedPreference使用 Dagger2公开应用程序范围,并像@Inject SharedPreferences mPreferences在活动/片段中一样使用它。

使用上述方法保存(写入)自定义首选项的示例代码:

SharedPreferences.Editor editor = mPreferences.edit();
    editor.putString(PREF_ORGANIZATION, mGson.toJson(organization));
    editor.apply();
Run Code Online (Sandbox Code Playgroud)

要读取自定义首选项:

 String organizationString = mPreferences.getString(PREF_ORGANIZATION, null);
    if (organizationString != null) {
      return mGson.fromJson(organizationString, Organization.class);
    }
Run Code Online (Sandbox Code Playgroud)

如果你像上面那样使用它会破坏 DRY 原则,因为代码将在多个地方重复。


方法2:

这种方法基于拥有一个单独的首选项类(如StringPreference/)的想法,BooleanPreference它为 SharedPreferences 代码提供包装器以保存和检索值。

在继续解决方案之前,请阅读以下帖子以了解详细信息:

  1. U2020方式:优雅的坚持你的数据通过@tasomaniac
  2. Espresso 2.1: ActivityTestRule by chiuki
  3. 匕首 2 + 浓缩咖啡 2 + Mockito

代码:

应用程序模块

@Module public class ApplicationModule {
  private final MyApplication mApplication;

  public ApplicationModule(MyApplication application) {
    mApplication = application;
  }

  @Provides @Singleton public Application provideApplication() {
    return mApplication;
  }
}
Run Code Online (Sandbox Code Playgroud)

数据模块.java

@Module(includes = ApplicationModule.class) public class DataModule {

  @Provides @Singleton public SharedPreferences provideSharedPreferences(Application app) {
    return PreferenceManager.getDefaultSharedPreferences(app);
  }
}
Run Code Online (Sandbox Code Playgroud)

GsonModule.java

@Module public class GsonModule {
  @Provides @Singleton public Gson provideGson() {
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    return gsonBuilder.create();
  }
}
Run Code Online (Sandbox Code Playgroud)

应用组件.java

@Singleton @Component(
    modules = {
        ApplicationModule.class, DataModule.class, GsonModule.class
    }) public interface ApplicationComponent {
  Application getMyApplication();
  SharedPreferences getSharedPreferences();
  Gson getGson();
}
Run Code Online (Sandbox Code Playgroud)

我的应用程序

public class MyApplication extends Application {
  @Override public void onCreate() {
    initializeInjector();
  }

   protected void initializeInjector() {
    mApplicationComponent = DaggerApplicationComponent.builder()
        .applicationModule(new ApplicationModule(this))
        .build();
  }
}
Run Code Online (Sandbox Code Playgroud)

组织首选项.java

public class OrganizationPreference {

  public static final String PREF_ORGANIZATION = "pref_organization";

  SharedPreferences mPreferences;
  Gson mGson;

  @Inject public OrganizationPreference(SharedPreferences preferences, Gson gson) {
    mPreferences = preferences;
    mGson = gson;
  }

  @Nullable public Organization getOrganization() {
    String organizationString = mPreferences.getString(PREF_ORGANIZATION, null);
    if (organizationString != null) {
      return mGson.fromJson(organizationString, Organization.class);
    }
    return null;
  }

  public void saveOrganization(Organization organization) {
    SharedPreferences.Editor editor = mPreferences.edit();
    editor.putString(PREF_ORGANIZATION, mGson.toJson(organization));
    editor.apply();
  }
}
Run Code Online (Sandbox Code Playgroud)

无论您在哪里需要首选项,只需使用 Dagger 注入即可@Inject OrganizationPreference mOrganizationPreference;

对于androidTest,我用模拟偏好覆盖了偏好。以下是我对 android 测试的配置:

测试数据模块.java

public class TestDataModule extends DataModule {

  @Override public SharedPreferences provideSharedPreferences(Application app) {
    return Mockito.mock(SharedPreferences.class);
  }
}
Run Code Online (Sandbox Code Playgroud)

模拟应用程序

public class MockApplication extends MyApplication {
  @Override protected void initializeInjector() {
    mApplicationComponent = DaggerTestApplicationComponent.builder()
        .applicationModule(new TestApplicationModule(this))
        .dataModule(new TestDataModule())
        .build();
  }
}
Run Code Online (Sandbox Code Playgroud)

登录屏幕测试.java

@RunWith(AndroidJUnit4.class) public class LoginScreenTest {

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

  @Inject SharedPreferences mSharedPreferences;
  @Inject Gson mGson;

 @Before public void setUp() {
    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();

    MyApplication app = (MyApplication) instrumentation.getTargetContext().getApplicationContext();
    TestApplicationComponent component = (TestApplicationComponent) app.getAppComponent();
    component.inject(this);
    when(mSharedPreferences.getString(eq(OrganizationPreference.PREF_ORGANIZATION),
        anyString())).thenReturn(mGson.toJson(HelperUtils.getFakeOrganization()));

    mActivityTestRule.launchActivity(new Intent());
  }
}
Run Code Online (Sandbox Code Playgroud)

确保在build.gradle 中添加了dexmaker mockito

androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2:'
Run Code Online (Sandbox Code Playgroud)