2021年8月24日火曜日

Jetpack Compose + ViewModel + Navigation + Hilt で UI テスト

テスト対象の Compose
  1. @HiltAndroidApp  
  2. class MyApplication : Application()  
  3.   
  4. @AndroidEntryPoint  
  5. class MainActivity : ComponentActivity() {  
  6.     override fun onCreate(savedInstanceState: Bundle?) {  
  7.         super.onCreate(savedInstanceState)  
  8.         setContent {  
  9.             MaterialTheme {  
  10.                 Surface(color = MaterialTheme.colors.background) {  
  11.                     MyApp()  
  12.                 }  
  13.             }  
  14.         }  
  15.     }  
  16. }  
  17.   
  18. @Composable  
  19. fun MyApp() {  
  20.     val navController = rememberNavController()  
  21.   
  22.     NavHost(navController, startDestination = "Screen1") {  
  23.   
  24.         composable("Screen1") {  
  25.             Screen1(hiltViewModel()) {  
  26.                 navController.navigate("Screen2/$it")  
  27.             }  
  28.         }  
  29.   
  30.         composable(  
  31.             route = "Screen2/{value}",  
  32.             arguments = listOf(  
  33.                 navArgument("value") { type = NavType.IntType },  
  34.             )  
  35.         ) {  
  36.             val arguments = requireNotNull(it.arguments)  
  37.             val value = arguments.getInt("value")  
  38.   
  39.             Screen2(value)  
  40.         }  
  41.     }  
  42. }  
  43.   
  44. @Composable  
  45. fun Screen1(  
  46.     viewModel: Screen1ViewModel,  
  47.     onClickItem: (Int) -> Unit,  
  48. ) {  
  49.     LazyColumn(modifier = Modifier.fillMaxSize()) {  
  50.         items(viewModel.values) { value ->  
  51.             Text(  
  52.                 text = value.toString(),  
  53.                 modifier = Modifier  
  54.                     .fillMaxWidth()  
  55.                     .clickable {  
  56.                         onClickItem(value)  
  57.                     }  
  58.                     .padding(16.dp)  
  59.             )  
  60.         }  
  61.     }  
  62. }  
  63.   
  64. @Composable  
  65. fun Screen2(value: Int) {  
  66.     Text(  
  67.         text = "value = $value",  
  68.         modifier = Modifier  
  69.             .fillMaxWidth()  
  70.             .padding(16.dp)  
  71.     )  
  72. }  
  73.   
  74. @HiltViewModel  
  75. class Screen1ViewModel @Inject constructor(  
  76.     valueProvider: ValueProvider  
  77. ) : ViewModel() {  
  78.   
  79.     val values: List<Int> = valueProvider.getValues()  
  80.   
  81. }  
  82.   
  83. interface ValueProvider {  
  84.   
  85.     fun getValues(): List<Int>  
  86. }  
  87.   
  88. class DefaultValueProvider @Inject constructor() : ValueProvider {  
  89.   
  90.     override fun getValues(): List<Int> = (0 until 20).map { Random.nextInt() }  
  91. }  
  92.   
  93. @Module  
  94. @InstallIn(SingletonComponent::class)  
  95. interface ValueProviderModule {  
  96.   
  97.     @Binds  
  98.     @Singleton  
  99.     fun bindValueProvider(provider: DefaultValueProvider): ValueProvider  
  100. }  


MyApp composable では最初に Screen1が表示される。
Screen1 にはランダムな Int 値のリストが表示され、タップすると Screen2 に遷移し、タップした値が表示される。

Screen1 のリストをタップして Screen2 に遷移し、タップしたところの値が表示されていることをテストしたいとする。

Screen1 の引数の Screen1ViewModel は hiltViewModel() で取得しているので、テストでも Hilt が動くようにしたい。
ただし、Screen1ViewModel で使っている ValueProvidier はテスト用のものに差し替えたい。


1. テスト用のライブラリを追加する

参考 (Hilt) : https://developer.android.com/training/dependency-injection/hilt-testing#testing-dependencies
参考 (Compose) : https://developer.android.com/jetpack/compose/testing#setup
  1. dependencies {  
  2.     ...  
  3.   
  4.     androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"  
  5.     debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"  
  6.   
  7.     androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"  
  8.     kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"  
  9. }  

2. テスト時に HiltTestApplication が使われるように CustomTestRunner を用意する

参考 : https://developer.android.com/training/dependency-injection/hilt-testing#instrumented-tests

(継承元のApplicationが必要な場合は https://developer.android.com/training/dependency-injection/hilt-testing#custom-application に方法が書いてある)

app/src/androidTest
  1. class CustomTestRunner : AndroidJUnitRunner() {  
  2.   
  3.     override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {  
  4.         return super.newApplication(cl, HiltTestApplication::class.java.name, context)  
  5.     }  
  6. }  
build.gradle (app)
  1. android {  
  2.     ...  
  3.   
  4.     defaultConfig {  
  5.         ...  
  6.   
  7.         testInstrumentationRunner "com.sample.myapplication.CustomTestRunner"  
  8.     }  
  9.     
  10.     ...  
  11. }  
この設定をしないと

java.lang.IllegalStateException: Hilt test, com.sample.myapplication.MyAppTest, cannot use a @HiltAndroidApp application but found com.sample.myapplication.MyApplication. To fix, configure the test to use HiltTestApplication or a custom Hilt test application generated with @CustomTestApplication.

のようなエラーがでる


3. @AndroidEntryPoint がついたテスト用の Activity を debug に用意する

app/src/debug
  1. @AndroidEntryPoint  
  2. class HiltTestActivity : ComponentActivity()  
app/src/debug/AndroidManifest.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     package="com.sample.myapplication">  
  4.   
  5.     <application>  
  6.         <activity  
  7.             android:name=".HiltTestActivity"  
  8.             android:exported="false" />  
  9.     </application>  
  10.   
  11. </manifest>  

4. テストを書く

テストクラスに @HiltAndroidTest をつける

HiltAndroidRule が先になるように oder を指定する。
参考 : https://developer.android.com/training/dependency-injection/hilt-testing#multiple-testrules

createAndroidComposeRule() に 3. で作った HiltTestActivity を指定する。createComposeRule() だと

Given component holder class androidx.activity.ComponentActivity does not implement interface dagger.hilt.internal.GeneratedComponent or interface dagger.hilt.internal.GeneratedComponentManager

のようなエラーがでる

binding の差し替えは https://developer.android.com/training/dependency-injection/hilt-testing#testing-features に書いてある。
  1. @UninstallModules(ValueProviderModule::class)  
  2. @HiltAndroidTest  
  3. class MyAppTest {  
  4.   
  5.     @get:Rule(order = 0)  
  6.     val hiltRule = HiltAndroidRule(this)  
  7.   
  8.     @get:Rule(order = 1)  
  9.     val composeTestRule = createAndroidComposeRule<HiltTestActivity>()  
  10.   
  11.     @BindValue  
  12.     val repository: ValueProvider = object : ValueProvider {  
  13.         override fun getValues(): List<Int> {  
  14.             return listOf(100)  
  15.         }  
  16.     }  
  17.   
  18.     @Test  
  19.     fun test() {  
  20.         composeTestRule.setContent {  
  21.             MaterialTheme {  
  22.                 Surface(color = MaterialTheme.colors.background) {  
  23.                     MyApp()  
  24.                 }  
  25.             }  
  26.         }  
  27.   
  28.         // Screen1 に 100 が表示されている  
  29.         composeTestRule.onNodeWithText("100").assertIsDisplayed()  
  30.   
  31.         // Screen1 の 100 が表示されている Node をクリック  
  32.         composeTestRule.onNodeWithText("100").performClick()  
  33.   
  34.         // Screen2 に value = 100 が表示されている  
  35.         composeTestRule.onNodeWithText("value = 100").assertIsDisplayed()  
  36.     }  
  37. }  




0 件のコメント:

コメントを投稿