Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Activity/Fragment単位でVisual Regression Testingを実行する

Activity/Fragment単位でVisual Regression Testingを実行する

MAO YUFENG

July 14, 2020
Tweet

More Decks by MAO YUFENG

Other Decks in Programming

Transcript

  1. CL AndroidでやっているVRTの流れ Screenshot Firebase Test Lab Instrumented Test REG SUIT

    Cloud Storage VRT Github Report どうスクリーンショットを 撮っているかを話す
  2. 例:Activity class MyActivity : AppCompatActivity() { // Extension fun from

    activity-ktx private val viewModel by viewModels<MyViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.my_layout) viewModel.itemsLiveData.observe(this) { // Bind data to UI } } } MyActivity.kt
  3. ActivityScenarioでスクリーンショットを撮る @Test fun screenshot() { // TODO: 撮りたいUI状態を作る ActivityScenario.launch(MyActivity::class.java) //

    Screenshotのライブラリを使う // Dialogなど撮れるFalconおすすめ https://github.com/jraska/Falcon takeScreenshot("MyActivity") } MyActivityScreenshot.kt
  4. 例:ViewModel class MyViewModel( private val repository: Repository ) : ViewModel()

    { // UIの状態を持つ val itemsLiveData = MutableLiveData<List<Item>>() fun loadItems() { itemsLiveData.value = repository.getItems() } } MyViewModel.kt
  5. Activity内ViewModelをインスタンス化 MyActivity.kt class MyActivity : AppCompatActivity() { private val viewModel

    by viewModels<MyViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... } }
  6. ViewModelFactoryをDIする MyActivity.kt class MyActivity : AppCompatActivity() { private val viewModel

    by viewModels<MyViewModel> { myViewModelFactory } @Inject lateinit var myViewModelFactory: ViewModelProvider.Factory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // DaggerでMemberInjectionする DaggerMyComponent.builder() .build() .inject(this) } } ActivityにInjectする ViewModelFactoryをモックしたもの に差し替えできれば、適切なUI状態が 作れる
  7. MemberInjectionの解決案:InjectorWrapper class InjectorWrapper<T : Any>( private val defaultInjector: (T) ->

    Unit // DaggerのInjector ) { fun inject(target: T) { defaultInjector.invoke(target) } }
  8. InjectorWrapper class InjectorWrapper<T : Any>( private val targetClass: Class<T>, private

    val defaultInjector: (T) -> Unit // DaggerのInjector ) { fun inject(target: T) { val injector = injectorDelegateMap[targetClass] ?: defaultInjector injector.invoke(target) } companion object { private val injectorDelegateMap = mutableMapOf<Class<*>, (Any) -> Unit>() // Test用のInjector @VisibleForTesting fun <T> setDelegate(targetClass: Class<T>, injector: (T) -> Unit) { @Suppress("UNCHECKED_CAST") injectorDelegateMap[targetClass] = { injector.invoke(it as T) } } } }
  9. InjectorWrapper class InjectorWrapper<T : Any>( private val targetClass: Class<T>, private

    val defaultInjector: (T) -> Unit // DaggerのInjector ) { fun inject(target: T) { val injector = injectorDelegateMap[targetClass] ?: defaultInjector injector.invoke(target) } companion object { private val injectorDelegateMap = mutableMapOf<Class<*>, (Any) -> Unit>() // Test用のInjector @VisibleForTesting fun <T> setDelegate(targetClass: Class<T>, injector: (T) -> Unit) { @Suppress("UNCHECKED_CAST") injectorDelegateMap[targetClass] = { injector.invoke(it as T) } } } } テスト用InjectorをMapに保存する
  10. InjectorWrapper class InjectorWrapper<T : Any>( private val targetClass: Class<T>, private

    val defaultInjector: (T) -> Unit // DaggerのInjector ) { fun inject(target: T) { val injector = injectorDelegateMap[targetClass] ?: defaultInjector injector.invoke(target) } companion object { private val injectorDelegateMap = mutableMapOf<Class<*>, (Any) -> Unit>() // Test用のInjector @VisibleForTesting fun <T> setDelegate(targetClass: Class<T>, injector: (T) -> Unit) { @Suppress("UNCHECKED_CAST") injectorDelegateMap[targetClass] = { injector.invoke(it as T) } } } } テストInjectorあれば使う、無いならデフォルトの使う
  11. ActivityでInjectorWrapperを使う override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val component =

    DaggerMyComponent.builder() .build() // MyActivityをkeyに、DaggerのInjectorをデフォルトのInjectorに InjectorWrapper(MyActivity::class.java, component::inject) .inject(this) } After
  12. テスト依存をInjectorWrapperに設定する After @Test fun screenshot() { // 撮りたいUI状態持つViewModelFactoryをInjectorWrapperに設定する InjectorWrapper.setDelegate(MyActivity::class.java) {

    activity -> activity.myViewModelFactory = mockMyViewModelFactory() } ActivityScenario.launch(MyActivity::class.java) takeScreenshot("MyActivity") }
  13. 適切な状態持つViewModelを作る // MockKを例に https://mockk.io/ fun mockMyViewModelFactory( items: List<Item> = emptyList()

    ): ViewModelProvider.Factory { val viewModel = MyViewModel(mockk()) // spyが必要で、後説明する val spyViewModel = spyk(viewModel) { every { itemsLiveData } returns MutableLiveData(items) every { loadItems() } just runs } return object : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return spyViewModel as T } } }
  14. 適切な状態持つViewModelを作る // MockKを例に https://mockk.io/ fun mockMyViewModelFactory( items: List<Item> = emptyList()

    ): ViewModelProvider.Factory { val viewModel = MyViewModel(mockk()) // spyが必要で、後説明する val spyViewModel = spyk(viewModel) { every { itemsLiveData } returns MutableLiveData(items) every { loadItems() } just runs } return object : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return spyViewModel as T } } }
  15. モック作る時の注意点 • モックライブラリのmockはジェネリックスと相性悪い、spyを使う • API 27以下のInstrumented Testで、モックライブラリはfinal class が対応できない、allopenプラグインを使う //

    androidx.lifecycle.ViewModel <T> T setTagIfAbsent(String key, T newValue) https://github.com/mockk/mockk/blob/master/ANDROID.md https://kotlinlang.org/docs/reference/compiler-plugins.html plugins { id "org.jetbrains.kotlin.plugin.allopen" version "1.3.72" }
  16. 自動操作を実装する fun startAutomaticTakeScreenshot(scenario: ActivityScenario) { var rootView: View? = null

    // ActivityScenarioからルートViewを取得して走査を始める scenario.onActivity { activity -> rootView = activity.findViewById(android.R.id.content) } dfs(rootView!!) }
  17. 探索関数 fun dfs(view: View) { // Viewのタイプに応じてハンドリングする when (view) {

    is AppBarLayout -> handleAppBarLayout(view) is ViewPager -> handleViewPager(view) is RecyclerView -> handleRecyclerView(view) ... } }
  18. 特殊なViewをハンドリングする fun handleViewPager(view: ViewPager) { val startPosition = view.currentItem //

    Espressoで各Pageにスクロールする view.forEachIndexed { index, child -> if (index != startPosition) { performAction(view, ViewPagerActions.scrollToPage(index)) } // 現在のページを更に探索する dfs(child) } } ViewPagerの例
  19. 対象Viewを操作してスクリーンショットを撮る private fun performAction(view: View, viewAction: ViewAction) { // 同じIdもつViewがあり得るので、ユーニークのTagを作る

    val tagValue = "view-tree-operator-value-${tagCnt++}" view.setTag(R.id.view_tree_operator_target_tag_key, tagValue) val viewMatcher = ViewMatchers.withTagKey( R.id.view_tree_operator_target_tag_key, CoreMatchers.equalTo(tagValue) ) Espresso.onView(viewMatcher).perform(viewAction) takeScreenshot() }
  20. テストで自動操作 @Test fun screenshot() { InjectorWrapper.setDelegate(MyActivity::class.java) { activity -> activity.myViewModelFactory

    = mockMyViewModelFactory() } ActivityScenario.launch(MyActivity::class.java) takeScreenshot("MyActivity") } Before
  21. テストで自動操作 @Test fun screenshot() { InjectorWrapper.setDelegate(MyActivity::class.java) { activity -> activity.myViewModelFactory

    = mockMyViewModelFactory() } val scenario = ActivityScenario.launch(MyActivity::class.java) startAutomaticTakeScreenshot(scenario) } After