【问题标题】:How to mock sharedpreferences for android instrumentation tests?如何模拟 android 仪器测试的 sharedpreferences?
【发布时间】:2016-07-01 21:35:02
【问题描述】:

我有一个偏好实用程序类,可以在一个地方存储和检索共享偏好中的数据。

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

LoginActivity.java:

中显示 PrefUtils 用法的示例代码
@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());
  } 
}

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

如何解决上述问题?

【问题讨论】:

  • 我发布了一个推荐 DI 框架的答案。我仍然认为这将是一种更好的方法,但考虑到您正在为PrefUtils 使用静态单例,它不应该是必要的。你能分享你的HelperUtils吗?如果你在HelperUtils.getFakeOrganization() 中设置断点,它会被命中吗?
  • 啊,我发现了问题,你不能单独使用 mockito 来模拟静态方法。我已经更新了我的答案,为你提供了两种可能的解决方案。

标签: android sharedpreferences mockito android-testing android-espresso


【解决方案1】:

方法一:

使用 Dagger2 将 SharedPreference 暴露在应用程序范围内,并在活动/片段中像 @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 原则,因为代码会在多个地方重复。


方法 2:

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

在继续解决方案之前,请阅读以下帖子以获得详细的想法:

  1. Persist your data elegantly: U2020 way @tasomaniac
  2. Espresso 2.1: ActivityTestRule by 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 测试配置:

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

【讨论】:

  • 并且没有 DI ??有什么想法吗??
【解决方案2】:

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

电源模拟

又好又简单,这将让您模拟静态方法,check out this SO post for details。不利的一面是,根据该 SO 帖子中的 cmets,它可能会导致其他问题。

依赖注入方法(我的原始答案)

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

您可以查看在这些测试中如何使用 mockito 的一些示例(12)。他们都没有测试 UI,而是实例化一个对象“存根”/“模拟”一些行为,然后测试其余的。

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

如何模拟类/对象的行为的细节因框架而异。这个blog post 介绍了如何将 Dagger 2 与 Mockito 和 espresso 一起使用,您可以将相同的方法应用于您的测试。它还包含指向 Dagger 2 更多背景的演示文稿的链接。

如果您不喜欢匕首 2,您也可以查看 RoboGuiceDagger。请注意,我认为黄油刀不适合您的需求,因为它不支持注射 Pojos。

【讨论】:

  • 感谢您的链接。我已经在使用 Dagger2,Butteknife 是一个视图注入库,它在这里没有任何作用。我很快就会用我自己的解决方案进行更新。
  • 完全正确(就黄油刀而言)。对我来说听起来不错,你应该能够用 Dagger2 实现你想要的。
  • 只有在使用匕首的时候才能解决?如果没有怎么办?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-07-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多