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

How to mock sharedpreferences for android instrumentation tests?

我有一个首选项实用程序 class 可以在一个地方存储和检索共享首选项中的数据。

Prefutils.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);
  }
}

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

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

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:'

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());
  } 
}

上面 return fakeOrganization 的代码没有工作,运行 登录测试 activity 导致 NullPointerException 行 mOrganizationNameTextView.setText(organization.getName()); 定义在以上 LoginActivity.java class.

如何解决上述问题?

不幸的是,Mockito 无法自行执行您正在寻找的内容。您有两种选择,一种是使用 Power Mock,另一种是将 Prefutils 更改为普通 class 而使用依赖注入框架。

电源模拟

很好很简单,这会让你模拟静态方法,check out this SO post for details。不利的一面是,根据该 SO post.

中的评论,它可能会导致其他问题

依赖注入方法(我原来的回答)

您正在尝试使用应用程序 "mocked" 的某些行为编写 UI 测试。 Mockito 旨在让您编写单元测试,您可以在其中测试特定对象(或一组对象)并模拟它们的某些行为。

您可以看到一些示例,了解如何在这些测试中使用 mockito (1, 2)。 None 他们测试 UI,而不是实例化对象 "stub"/"mock" 一些行为,然后测试其余部分。

要实现你想要的,你需要一个依赖注入框架。这允许您根据您是 运行 实际应用程序还是测试来更改某些应用程序的 "implementation"。

如何模拟 classes/objects 行为的细节因框架而异。 blog post 介绍了如何将 Dagger 2 与 Mockito 和 espresso 结合使用,您可以对测试应用相同的方法。它还包含指向提供有关匕首 2 的更多背景的演示文稿的链接。

如果你不喜欢 dagger 2 那么你也可以查看 RoboGuice and Dagger。请注意,我不认为 butter-knife 会满足您的需求,因为它不支持注入 Pojos。

方法一:

使用 Dagger2 公开 SharedPreference 的应用程序范围,并像 activity/fragment 中的 @Inject SharedPreferences mPreferences 一样使用它。

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

SharedPreferences.Editor editor = mPreferences.edit();
    editor.putString(PREF_ORGANIZATION, mGson.toJson(organization));
    editor.apply();

阅读自定义首选项:

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

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


方法二:

此方法基于具有单独首选项的想法 class,例如 StringPreference/BooleanPreference,它提供围绕 SharedPreferences 代码的包装器以保存和检索值。

在继续解决方案之前阅读以下帖子以了解详细想法:

  1. Persist your data elegantly: U2020 way @tasomaniac
  2. Espresso 2.1: ActivityTestRule 来自 chiuki
  3. Dagger 2 + Espresso 2 + Mockito

代码:

ApplicationModule.java

@Module public class ApplicationModule {
  private final MyApplication mApplication;

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

  @Provides @Singleton public Application provideApplication() {
    return mApplication;
  }
}

DataModule.java

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

  @Provides @Singleton public SharedPreferences provideSharedPreferences(Application app) {
    return PreferenceManager.getDefaultSharedPreferences(app);
  }
}

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();
  }
}

ApplicationComponent.java

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

MyApplication.java

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

   protected void initializeInjector() {
    mApplicationComponent = DaggerApplicationComponent.builder()
        .applicationModule(new ApplicationModule(this))
        .build();
  }
}

OrganizationPreference.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();
  }
}

只要您需要首选项,只需使用 Dagger @Inject OrganizationPreference mOrganizationPreference; 将其注入即可。

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

测试DataModule.java

public class TestDataModule extends DataModule {

  @Override public SharedPreferences provideSharedPreferences(Application app) {
    return Mockito.mock(SharedPreferences.class);
  }
}

MockApplication.java

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

LoginScreenTest.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());
  }
}

确保在 build.gradle

中添加了 dexmaker mockito
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2:'