在Android导航组件中测试导航动作

Posted

技术标签:

【中文标题】在Android导航组件中测试导航动作【英文标题】:Testing navigation action in navigation component in Android 【发布时间】:2020-04-12 09:36:42 【问题描述】:

最近我将导航组件集成到一个基于单一活动模型的项目中。我尝试根据 android 文档中的教程添加 ui 测试。但是,当我对该教程进行一些修改时,它不起作用。我的片段中有以下代码。

val bundle = Bundle().apply 
   putString(UserTrackingConstants.VIEW, UserTrackingConstants.HOME)


btnSignUp.setOnClickListener 
findNavController().navigate(R.id.action_from_home_fragment_to_registration_fragment,bundle)

因此,每当单击 btnSignUp 时,它都会调用以下导航操作。它工作完美。然后我添加了以下测试。

@Test
    fun testRegisterButtonClicked() 
        val mockNavController = mock(NavController::class.java)
        val bundle = bundleOf(
                UserTrackingConstants.VIEW to UserTrackingConstants.HOME
        )
        val scenario = launchFragmentInContainer<HomeFragment>()
        scenario.onFragment  fragment ->
            Navigation.setViewNavController(fragment.requireView(), mockNavController)
        
        onView(withId(R.id.btnSignUp))
                .perform(click())
        verify(mockNavController).navigate(R.id.action_from_home_fragment_to_registration_fragment, bundle)
    

它抛出了以下异常

E/TestRunner: ----- begin exception -----
    Argument(s) are different! Wanted:
    navController.navigate(
        2131296310,
        Bundle[view=home]
    );
    -> at HomeFragmentTest.testRegisterButtonClicked(HomeFragmentTest.kt:87)
    Actual invocation has different arguments:
    navController.navigate(
        2131296310,
        Bundle[view=home]
    );
    -> at HomeFragment$setupObservers$2.onClick(HomeFragment.kt:84)

        at HomeFragmentTest.testRegisterButtonClicked(HomeFragmentTest.kt:87)
        at java.lang.reflect.Method.invoke(Native Method)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at androidx.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at androidx.test.ext.junit.runners.AndroidJUnit4.run(AndroidJUnit4.java:104)
        at org.junit.runners.Suite.runChild(Suite.java:128)
        at org.junit.runners.Suite.runChild(Suite.java:27)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
        at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
        at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:392)
        at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2189)
    ----- end exception -----

Argument(s) are different! Wanted:
navController.navigate(
2131296310,
Bundle[view=home]
);
-> at HomeFragmentTest.testRegisterButtonClicked(HomeFragmentTest.kt:87)
Actual invocation has different arguments:
navController.navigate(
2131296310,
Bundle[view=home]
);
-> at HomeFragment$setupObservers$2.onClick(HomeFragment.kt:84)

at HomeFragmentTest.testRegisterButtonClicked(HomeFragmentTest.kt:87)
at java.lang.reflect.Method.invoke(Native Method)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at androidx.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at androidx.test.ext.junit.runners.AndroidJUnit4.run(AndroidJUnit4.java:104)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:392)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2189)

我没有从 logcat 中得到任何有用的信息。似乎论点是相同的,它应该通过。如果我不通过捆绑,它工作正常。我关注了以下链接https://developer.android.com/guide/navigation/navigation-testing 中的文档。我的问题是是否有人遇到过此类异常以及如何使用 bundle 测试导航组件?

【问题讨论】:

【参考方案1】:

您遇到如此奇怪的测试结果的原因是 Bundle 类没有覆盖 equals()toString() 方法,因此 Bundle 实例通过引用进行比较(并且引用显然与您的预期不同bundle 和实际的)——由于 Mockito 依赖这些方法来验证对象是否相同,因此您的测试用例将失败。

要解决此问题,您可以为Mockito.verify() 使用自定义匹配器:

public class BundleEquals implements ArgumentMatcher, ContainsExtraTypeInfo, Serializable 

    @Nullable
    private final Bundle expected;

    public BundleEquals(@Nullable Bundle expected) 
        this.expected = expected;
    

    @Override
    public boolean matches(Bundle actual) 
        if (expected == null && actual == null) 
            return true;
        

        if (expected == null || actual == null) 
            return false;
        

        return areBundlesEqual(expected, actual);
    

    private boolean areBundlesEqual(@NonNull Bundle expected, @NonNull Bundle actual) 
        if (expected.size() != actual.size()) 
            return false;
        

        if (!expected.keySet().containsAll(actual.keySet())) 
            return false;
        

        for (String key : expected.keySet()) 
            @Nullable Object expectedValue = expected.get(key);
            @Nullable Object actualValue = actual.get(key);

            if (expectedValue instanceof Bundle && actualValue instanceof Bundle) 
                if (!areBundlesEqual((Bundle) expectedValue, (Bundle) actualValue)) 
                    return false;
                
             else if (!Objects.equals(expectedValue, actualValue)) 
                return false;
            
        

        return true;
    

    public String toStringWithType() 
        String clazz = expected != null ? expected.getClass().getSimpleName() : null;
        return "(" + clazz + ") " + describe(expected);
    

    private String describe(Object object) 
        return ValuePrinter.print(object);
    

    @Override
    public boolean typeMatches(Object actual) 
        return expected != null && actual != null && actual.getClass() == expected.getClass();
    

    public String toString() 
        return describe(expected);
    

注意: ContainsExtraTypeInfoSerializable 实现在这里不是必需的,但是当由于包不匹配而导致测试失败时,它们会为您提供详细的预期和实际参数描述,

要应用匹配器,您应该使用如下内容:

verify(mockNavController).navigate(eq(R.id.action_from_home_fragment_to_registration_fragment), argThat(new BundleEquals(bundle));

注意这里需要eq(),以便 Mockito 不会混淆不同的匹配器:让您的操作 id 未包装会导致验证失败。

【讨论】:

以上是关于在Android导航组件中测试导航动作的主要内容,如果未能解决你的问题,请参考以下文章

Android导航组件“向上按钮”打开抽屉导航

关于android导航架构组件的问题

android jetpack 导航仪器测试在返回导航上失败

为什么当我第二次在Android Studio中使用导航启动动作时,导航目的地丢失了?

导航抽屉切换图标

Android - 具有多个底部导航菜单的导航组件