You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Android Espresso检测Toast消息遇阻,寻求替代方案

检测跨页面Toast的可行方案

针对登录页触发Toast但只能在后续页面检测的场景,以下几种方法可以尝试:

1. 自定义IdlingResource捕获Toast内容

Toast默认显示时间短,页面跳转后可能已消失,可通过IdlingResource在Toast显示时记录文本,后续在目标页面断言:

class ToastIdlingResource : IdlingResource {
    private var callback: IdlingResource.ResourceCallback? = null
    private var capturedToastText: String? = null

    override fun getName() = "ToastIdlingResource"

    override fun isIdleNow() = capturedToastText != null

    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
        this.callback = callback
    }

    fun captureToast(text: String) {
        capturedToastText = text
        callback?.onTransitionToIdle()
    }

    fun getCapturedText() = capturedToastText
}

测试中注册并使用:

@Rule
@JvmField
val activityRule = ActivityTestRule(LoginActivity::class.java)

private lateinit var toastIdlingResource: ToastIdlingResource

@Before
fun setup() {
    toastIdlingResource = ToastIdlingResource()
    Espresso.registerIdlingResources(toastIdlingResource)
    
    // 通过Hook替换Toast的show逻辑捕获文本
    ToastHook.setCallback { text ->
        toastIdlingResource.captureToast(text)
    }
}

@Test
fun testLoginToast() {
    // 执行登录操作触发Toast
    onView(withId(R.id.btn_login)).perform(click())
    
    // 等待页面跳转完成
    onView(withId(R.id.home_layout)).check(matches(isDisplayed()))
    
    // 断言Toast内容
    assertEquals("登录成功", toastIdlingResource.getCapturedText())
}

@After
fun teardown() {
    Espresso.unregisterIdlingResources(toastIdlingResource)
}

通过反射实现ToastHook:

object ToastHook {
    private var callback: ((String) -> Unit)? = null

    fun setCallback(callback: ((String) -> Unit)?) {
        this.callback = callback
        hookToastShowMethod()
    }

    private fun hookToastShowMethod() {
        try {
            val toastClass = Class.forName("android.widget.Toast")
            val showMethod = toastClass.getDeclaredMethod("show")
            showMethod.isAccessible = true

            val originalMethod = showMethod
            val hookedMethod = object : Method() {
                override fun invoke(obj: Any?, vararg args: Any?): Any? {
                    val toast = obj as Toast
                    val messageView = toast.view.findViewById<TextView>(android.R.id.message)
                    callback?.invoke(messageView.text.toString())
                    return originalMethod.invoke(obj, *args)
                }
            }

            // 替换Toast的show方法(不同Android版本可能需要调整反射逻辑)
            val methodField = Method::class.java.getDeclaredField("method")
            methodField.isAccessible = true
            methodField.set(hookedMethod, originalMethod)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

2. 利用Activity生命周期回调监听Toast

在测试中注册Activity生命周期回调,在登录页切换到后台时检查Toast是否存在:

class ToastLifecycleListener : Application.ActivityLifecycleCallbacks {
    private var toastWasShown = false

    override fun onActivityPaused(activity: Activity) {
        if (activity is LoginActivity) {
            toastWasShown = isToastVisible()
        }
    }

    private fun isToastVisible(): Boolean {
        val windowManager = InstrumentationRegistry.getInstrumentation()
            .targetContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val rootView = windowManager.decorView.rootView
        return findToastInView(rootView)
    }

    private fun findToastInView(view: View): Boolean {
        if (view.javaClass.simpleName.contains("Toast")) {
            return view.isShown
        }
        if (view is ViewGroup) {
            for (i in 0 until view.childCount) {
                if (findToastInView(view.getChildAt(i))) {
                    return true
                }
            }
        }
        return false
    }

    fun wasToastShown() = toastWasShown
}

测试中使用:

@Test
fun testLoginToast() {
    val listener = ToastLifecycleListener()
    val app = InstrumentationRegistry.getInstrumentation().targetContext.application as Application
    app.registerActivityLifecycleCallbacks(listener)
    
    onView(withId(R.id.btn_login)).perform(click())
    onView(withId(R.id.home_layout)).check(matches(isDisplayed()))
    
    assertTrue(listener.wasToastShown())
    
    app.unregisterActivityLifecycleCallbacks(listener)
}

3. 改造App的Toast工具类(推荐)

如果是自家App,封装统一的Toast工具类,测试模式下将Toast内容存入全局变量,后续任意页面可直接读取验证:

object ToastUtils {
    @VisibleForTesting
    var testLastToastText: String? = null

    fun showToast(context: Context, text: String) {
        testLastToastText = text
        Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
    }
}

测试中直接断言:

@Test
fun testLoginToast() {
    onView(withId(R.id.btn_login)).perform(click())
    onView(withId(R.id.home_layout)).check(matches(isDisplayed()))
    
    assertEquals("登录成功", ToastUtils.testLastToastText)
}

内容的提问来源于stack exchange,提问作者Jefferson Jales

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 17:48:11