简体   繁体   English

Android 使用 Mockito 进行仪器测试:mocking 不工作

[英]Android Instrumentation test using Mockito: mocking not working

I am trying to run an Instrumentation test (androidTest) using Mockito in Android with Java.我正在尝试使用 Android 中的 Mockito 和 Java 运行仪器测试(androidTest)。

The object under test is a simple fragment with and edittext in the layout.测试中的 object 是一个简单的片段,布局中包含和编辑文本。 The edittext is loaded from a shared preference value.编辑文本是从共享首选项值加载的。 This is the relevant code:这是相关代码:

public class KMCountFragment extends Fragment {

static final String SHARED_PREF_FILE = "cartrip_sharedpref";
static final String PREF_KEY_START_KM = "pref_start_km";


private SharedPreferences sharedPreferences;

private int startKMCount;
private int endKMCount;


@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    sharedPreferences = getContext().getSharedPreferences(SHARED_PREF_FILE, MODE_PRIVATE);

    startKMCount = sharedPreferences.getInt(PREF_KEY_START_KM, 0);     
}


public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
    ...
    binding.startKMCount.setText(String.valueOf(startKMCount));
}

This is the test:这是测试:

@MediumTest
@RunWith(MockitoJUnitRunner.class)
public class KMCountFragmentIT {

    @Mock
    Context context;

    @Mock
    SharedPreferences sharedPrefs;


    @Before
    public void setUp() {
        sharedPrefs = mock(SharedPreferences.class);
        context = mock(Context.class);

        when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs);
    }

    @Test
    public void testFragmentCountersInitStata() {
        when(sharedPrefs.getInt(anyString(), anyInt())).thenReturn(20);

        FragmentScenario<KMCountFragment> scenario = FragmentScenario.launchInContainer(KMCountFragment.class);
        scenario.moveToState(Lifecycle.State.RESUMED);

        onView(withId(R.id.startKMCount)).check(matches(withText("20")));
    }
}

These are my dependencies in build.gradle:这些是我在 build.gradle 中的依赖项:

dependencies {

    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'androidx.navigation:navigation-fragment:2.3.5'
    implementation 'androidx.navigation:navigation-ui:2.3.5'
    implementation files('libs/activation.jar')
    implementation files('libs/additionnal.jar')
    implementation files('libs/mail.jar')
    implementation 'androidx.preference:preference:1.1.1'
    implementation 'androidx.security:security-crypto:1.0.0'
    testImplementation 'junit:junit:4.13.2'

    testImplementation 'org.mockito:mockito-core:4.3.1'
    testImplementation 'androidx.test.ext:junit:1.1.3'

    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "org.mockito:mockito-android:4.3.1"
    androidTestImplementation "androidx.test:core:1.4.0"

    debugImplementation "androidx.fragment:fragment-testing:1.4.1"
}

The test keeps failing because the actual value in my app prefs is 999 and the test expects 20. The real context from the app is used and not the mocked one.测试一直失败,因为我的应用程序首选项中的实际值为 999,而测试预计为 20。使用的是应用程序的真实上下文,而不是模拟的上下文。 I tried to mock the whole context also but same result:我也尝试模拟整个上下文,但结果相同:

androidx.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: 'an instance of android.widget.TextView and view.getText() with or without transformation to match: is "20"' doesn't match the selected view.
Expected: an instance of android.widget.TextView and view.getText() with or without transformation to match: is "20"
Got: view.getText() was "999" transformed text was "999"

It seems like the mocking from Mockito is not working.似乎来自 Mockito 的 mocking 不起作用。 Maybe I'm missing something stupid.. but I can't figure it out!也许我错过了一些愚蠢的东西..但我想不通!

Your mocked Context will not be used when Espresso spawns your activity/fragment.当 Espresso 生成您的活动/片段时,您的模拟Context将不会被使用。 Essentially, you cannot ( or rather should not ) mock Context in Android.本质上,您不能(或者不应该)在 Android 中模拟 Context。 I can think of three ways you can perform this test scenario.我可以想出三种方法来执行这个测试场景。

  1. Use ApplicationProvider.getApplicationContext() to access application context in your tests and create a real SharedPreferences instance using it.使用ApplicationProvider.getApplicationContext()在您的测试中访问应用程序上下文并使用它创建一个真实的 SharedPreferences 实例。 When using this approach, do not forget to clear the SharedPreferences when tearing down the test (in the @After method).使用这种方法时,不要忘记在拆除测试时清除SharedPreferences (在@After方法中)。
  2. Inject SharedPreferences in Fragment after it has been created, by passing a callback to FragmentScenario#onFragment() .Fragment创建后,通过将回调传递给FragmentScenario#onFragment()在 Fragment 中注入SharedPreferences You can inject a mocked SharedPreferences this way, but this approach tends to be more smelly than the former one.您可以通过这种方式注入模拟的SharedPreferences ,但这种方法往往比前一种更臭。 Personally, I wouldn't recommend it unless you absolutely know what you're doing.就个人而言,除非您绝对知道自己在做什么,否则我不会推荐它。
  3. Use a DI framework for injecting dependencies into Android components, eg Hilt .使用 DI 框架将依赖项注入 Android 组件,例如Hilt This is a neater approach, but it requires much effort.这是一种更简洁的方法,但需要付出很多努力。 So it might not be worthwhile to do it for smaller projects.因此,对于较小的项目可能不值得这样做。

OK, so this is my test.好的,这是我的测试。 Not so clever, but it works in my case:不是那么聪明,但它适用于我的情况:

@MediumTest
@RunWith(MockitoJUnitRunner.class)
public class KMCountFragmentIT {

    private static final int START_KM_COUNT_TEST_VALUE = 10;
    private static final int END_KM_COUNT_TEST_VALUE = 90;

    private SharedPreferences sharedPreferences;

    private int startKMCount;
    private int endKMCount;
    private boolean clearPref;

    @Before
    public void setUp() {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();

        sharedPreferences = appContext.getSharedPreferences(SHARED_PREF_FILE, MODE_PRIVATE);

        if (sharedPreferences.contains(PREF_KEY_START_KM)) {
            clearPref = false;
            startKMCount = sharedPreferences.getInt(PREF_KEY_START_KM, 0);
        } else {
            clearPref = true;
            startKMCount = START_KM_COUNT_TEST_VALUE;
            SharedPreferences.Editor preferencesEditor = sharedPreferences.edit();
            preferencesEditor.putInt(PREF_KEY_START_KM, startKMCount);
            preferencesEditor.apply();
        }
    }

    @After
    public void tearDown() {
        if (clearPref) {
            sharedPreferences.edit().clear().apply();
        }
    }

    @Test
    public void testFragmentCountersInitState() {
        FragmentScenario<KMCountFragment> scenario = FragmentScenario.launchInContainer(KMCountFragment.class);
        scenario.moveToState(Lifecycle.State.RESUMED);

        onView(withId(R.id.startKMCount)).check(matches(withText(String.valueOf(startKMCount))));
    }
}

In the setup I check if the pref is present and use the value.在设置中,我检查首选项是否存在并使用该值。 In the end the fragment is the subject of the test.最后,片段是测试的主题。 The tearDown cleans the prefs only if modified by the setup... In this way the test doesn't modify pref data if already present.只有在被设置修改后,tearDown 才会清理首选项……这样,如果已经存在,测试就不会修改首选项数据。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM