昨天已經把平時 UI 測試會用到的東西都稍微介紹過了,今天會開始為 TasksFragment 建立測試。
如果有在使用 Dagger 的話就會遇到一個問題,所以執行時會有一些問題。
至於是什麼問題呢?
DaggerFragment
及 DaggerAppCompatActivity
而不是原生的 Android 組件一般解決這個問題是寫另外的 Module 和 Component 來模擬 DI ,我們先從這裡開始吧。
首先要先創建一個測試用的 Application :
class TestTodoApplication : TodoApplication(), HasSupportFragmentInjector {
lateinit var fragmentInjector: DispatchingAndroidInjector<Fragment>
override fun supportFragmentInjector() = fragmentInjector
}
當然,由於我們使用 AndroidJUnitRunner 執行 Android Test ,他使用的是原始的 Application ,所以也要建立一個自定義的 Runner 執行新的測試:
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, TestTodoApplication::class.java.name, context)
}
}
替換掉原來的 AndroidJUnitRunner:
// app/build.gradle
android {
defaultConfig {
......
testInstrumentationRunner 'com.ininmm.todoapp.CustomTestRunner'
}
}
重新設計 Test 用的 Module 及 Component:
@Module
class TestApplicationModule {
@Singleton
@Provides
fun provideIoDispatcher() = Dispatchers.IO
@Provides
@Singleton
fun provideRepository(): ITasksRepository = FakeRepository()
}
@Singleton
@Component(
modules = [
TestApplicationModule::class,
AndroidSupportInjectionModule::class,
TasksActivityBinds::class,
TasksModule::class,
TaskDetailModule::class,
StatisticsModule::class,
TasksModule::class,
ViewModelBuilder::class
]
)
interface TestApplicationComponent : AndroidInjector<TestTodoApplication> {
@Component.Factory
interface Factory {
fun create(@BindsInstance applicationContext: Context): TestApplicationComponent
}
val tasksRepository: ITasksRepository
}
寫一個 rule 來把 application 傳給 Dagger :
class DaggerTestApplicationRule : TestWatcher() {
lateinit var component: TestApplicationComponent
private set
override fun starting(description: Description?) {
super.starting(description)
val app = ApplicationProvider.getApplicationContext<Context>() as TestTodoApplication
component = DaggerTestApplicationComponent.factory().create(app)
component.inject(app)
}
}
這樣就先準備好測試前的東西了。
一樣會先在開始時初始化一些必要的東西:
class TasksFragmentTest {
private lateinit var repository: ITasksRepository
@get:Rule
val rule = DaggerTestApplicationRule()
@MockK(relaxUnitFun = true)
private lateinit var navController: NavController
@Before
fun setupDaggerComponent() {
MockKAnnotations.init(this)
repository = rule.component.tasksRepository
runBlocking {
repository.deleteAllTasks()
}
}
}
這邊我只舉一些範例,如果想測試 Activity 上點擊 Menu 選擇 Filter Type 後是否會顯示對應的 UI ,一樣也是基於 Given-When-Then 的概念:
- 準備好進入 Activity
- 採取某些動作
- 驗證 UI 是否正確
如下表示:
class TasksFragmentTest {
......
@Test
fun displayCompletedTask() {
// Given
repository.saveTaskBlocking(createTasks()[1])
// When
launchActivity()
onView(withText("Title2")).check(matches(isDisplayed()))
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_active)).perform(click())
// Then
onView(withText("Title2")).check(matches(not(isDisplayed())))
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_completed)).perform(click())
// Then
onView(withText("Title2")).check(matches(isDisplayed()))
}
}
還有一個比較複雜的情況,點擊某個按鈕進入 Tasks 新增頁,以上很多行為都是 Android API 控制,所以我這裡就只針對某幾個關鍵方法做是否調用的檢查:
class TasksFragmentTest {
......
@Test
fun clickAddTaskButtonThenNavigateToAddEditFragment() {
// GIVEN - 在 TasksFragment
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - 點擊 "+" button
onView(withId(R.id.taskFabAddTask)).perform(click())
// THEN - 驗證調用方法 navigate 到 AddEditTaskFragment
val navDirections = TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
null,
getApplicationContext<Context>().getString(R.string.add_task)
)
every { navController.navigate(navDirections) } just Runs
verify { navController.navigate(navDirections) }
}
}
以上就是 Fragment 測試的簡單介紹,明天會繼續完成 Activity 的測試。