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

如何为使用ViewBinding的Fragment编写场景化UI测试

现有实现的核心问题
  • 业务代码存在笔误:ViewModel中对外暴露的getBike LiveData返回值类型错误、和同名方法冲突,且引用了未定义的bikeType变量,本身无法正常返回Bike数据
  • Fragment的ViewModel实例由内部的ViewModelProvider自行创建,测试代码中Mock的ViewModel没有注入到Fragment中,Mock完全不生效,Fragment运行时会调用真实的BikeService发起网络请求,无法拿到测试假数据
  • 测试逻辑错误:构造的假数据fakeBike的ownerName为Joske,但校验时匹配的文本是Klaartje,必然校验失败;且把Espresso的onView校验逻辑放在了scenario.onFragment闭包中(该闭包运行在主线程),不符合Espresso的调用规则
  • 缺少RxJava调度器替换规则,真实的网络请求/异步逻辑会导致测试时序错乱
修复步骤

1. 修正业务代码笔误

ViewModel修正:

class BikeViewModel : ViewModel() {
    private val disposeBag = CompositeDisposable()
    private val bikeId = "你的bikeId逻辑"
    // 私有可变LiveData
    private val _bike: MutableLiveData<Bike> = MutableLiveData()
    // 对外暴露不可变LiveData,避免和方法重名
    val bikeLiveData: LiveData<Bike>
        get() = _bike

    fun loadBike() {
        BikeService().getBike(bikeId)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                _bike.postValue(it)
            }, {
                Timber.e(it)
            }).let { disposeBag.add(it) }
    }

    override fun onCleared() {
        super.onCleared()
        disposeBag.dispose()
    }
}

Fragment修正:

class BikeFragment : AppAbstractBaseFragment<Any>(), BikeView,
    OnBackPressedListener {

    var mBinding: FragmentBikeBinding? = null
    // 开放viewModel变量方便测试注入,也可以用FragmentFactory实现构造注入
    var viewModel: BikeViewModel? = null

    var cancellable: Boolean = false

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mBinding = FragmentBikeBinding.inflate(inflater, container, false)
        // 开启DataBinding的生命周期绑定,避免手动更新
        mBinding?.lifecycleOwner = viewLifecycleOwner
        return mBinding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        if (viewModel == null) {
            viewModel = ViewModelProvider(this).get(BikeViewModel::class.java)
        }
        mBinding?.apply {
            view = this@BikeFragment
            viewModel?.bikeLiveData?.observe(viewLifecycleOwner, { newBike ->
                bike = newBike
            })
        }
        viewModel?.loadBike()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        mBinding = null
    }
}

2. 修正测试用例

首先添加RxJava测试依赖(如果没加的话),然后调整测试代码:

class BikeTest {

    @get:Rule
    val screenshotTestRule = ScreenshotTestRule()

    @get:Rule
    val dataBindingIdlingResourceRule = DataBindingIdlingResourceRule()

    @get:Rule
    val instantExecutor = InstantTaskExecutorRule()

    // 添加RxJava调度器替换规则,把所有异步调度改成同步执行
    @get:Rule
    val rxSchedulerRule = RxJavaSchedulerRule()

    private lateinit var scenario: FragmentScenario<BikeFragment>
    private val fakeBike = Bike(id = 1, cancellable = true, linkable = true, ownerName = "Joske")
    // Mock ViewModel
    private val mockViewModel = mockk<BikeViewModel>(relaxed = true)

    @Before
    fun setUp() {
        // 给Mock的ViewModel设置桩:返回包含假数据的LiveData
        every { mockViewModel.bikeLiveData } returns MutableLiveData(fakeBike)
        // 让loadBike方法不执行真实的网络请求
        every { mockViewModel.loadBike() } just Runs
    }

    @Test
    fun loadBikeSeeTheRightBikeData() {
        // 启动Fragment后立即替换ViewModel
        scenario = launchFragmentInContainer(themeResId = R.style.Theme_Brand)
        dataBindingIdlingResourceRule.monitorFragment(scenario)
        scenario.onFragment { fragment ->
            fragment.viewModel = mockViewModel
        }
        // 校验文本和假数据一致
        onView(withId(R.id.ownerNameTextView)).check(matches(withText("Joske")))
    }
}

RxJavaSchedulerRule参考实现:

class RxJavaSchedulerRule : TestRule {
    override fun apply(base: Statement, description: Description): Statement {
        return object : Statement() {
            override fun evaluate() {
                RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
                RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() }
                RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() }
                RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
                try {
                    base.evaluate()
                } finally {
                    RxJavaPlugins.reset()
                    RxAndroidPlugins.reset()
                }
            }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 08:45:01