如何模拟android instrumentation测试的sharedpreferences?

时间:2016-07-01 21:35:02

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

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

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。

如何解决上述问题?

2 个答案:

答案 0 :(得分:3)

<强>方法-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来自chiuki
  3. Dagger 2 + Espresso 2 + Mockito
  4. <强>代码:

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

答案 1 :(得分:1)

不幸的是,Mockito无法独立完成你想要的任务。您有两个选择,一个是使用Power Mock,另一个是将Prefutils更改为普通类,而是使用依赖注入框架。

Power Mock

简单明了,这将让你模拟静态方法check out this SO post for details。在不利方面,它可能会导致其他问题基于SO帖子中的评论。

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

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

您可以看到有关如何在这些测试中使用mockito的一些示例(12)。他们都没有测试UI,而是实例化一个对象&#34; stub&#34; /&#34; mock&#34;一些如果它的行为,然后测试其余的。

要实现您的目标,您需要一个依赖注入框架。这允许您更改&#34;实施&#34;根据您运行的是实际应用程序还是测试,您的某些应用程序。

模拟类/对象行为的细节因框架而异。这个blog post讨论了如何将Dagger 2与Mockito和espresso一起使用,您可以为测试应用相同的方法。它还提供了演示文稿的链接,提供了更多关于匕首2的背景信息。

如果您不喜欢匕首2,那么您还可以结帐RoboGuiceDagger。请注意,我不认为黄油刀会满足您的需求,因为它不支持注射Pojos。