如何为使用ViewBinding的Fragment编写场景化UI测试
现有实现的核心问题
- 业务代码存在笔误:ViewModel中对外暴露的
getBikeLiveData返回值类型错误、和同名方法冲突,且引用了未定义的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
相关产品推荐
相关产品推荐

