Irbisa · Шпаргалка
Тестирование в Android
JUnit, MockK, Turbine, Robolectric, Espresso, тесты Compose UI, правила Hilt, скриншот-тесты
В чём разница между unit-, инструментальными и UI-тестами, и что должно лежать в
src/test, а что вsrc/androidTest?Что такое тестовые дублёры (dummy, stub, fake, mock, spy) и когда fake лучше mock?
Как правильно построить JUnit-тест: именование, Given/When/Then, проверки и
@Beforeпротив@BeforeClass?// wrong: two actions, boolean assertions, a name that describes the method@Test fun testCart() { val cart = Cart() cart.add(Item("apple", cents = 100)) cart.applyCoupon("HALF") assertTrue(cart.totalCents == 50) // fails with a bare AssertionError…Что такое TDD и какие правила тестирования вы бы ввели в Android-команде?
Почему JVM unit-тест падает с
Method ... in android.util.Log not mocked, и как это исправить?// fails on the JVM: Log and Uri are stubs in src/testclass DeepLinkParser { fun orderId(link: String): Long? { Log.d("DeepLink", "parsing $link") // RuntimeException: Method d ... not mocked return Uri.parse(link).getQueryParameter("order")?.toLongOrNull() }…Как в Espresso нажать на элемент
RecyclerView, в том числе на тот, которого ещё нет на экране?// androidTestImplementation("androidx.test.espresso:espresso-contrib:<version>") // by positiononView(withId(R.id.orders)) .perform(RecyclerViewActions.actionOnItemAtPosition<OrderViewHolder>(3, click()))…Что такое UI Automator и когда его используют вместо Espresso?
@RunWith(AndroidJUnit4::class)class ScanFlowTest { @get:Rule val activity = ActivityScenarioRule(MainActivity::class.java) private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) @Test fun scanAfterGrantingCamera() {…Mockito или MockK: как замокать final-классы и
suspend-функции, и что делаетrelaxed = true?class ArchiveUseCaseTest { private val repo = mockk<OrderRepo>(relaxUnitFun = true) // strict except Unit functions @Test fun `archives the loaded order`() = runTest { coEvery { repo.load(7) } returns Order(id = 7, archived = false) // suspend: coEvery val saved = slot<Order>()…Как протестировать ViewModel с корутинами:
runTest,Dispatchers.setMainиStandardTestDispatcherпротивUnconfinedTestDispatcher?class MainDispatcherRule( val dispatcher: TestDispatcher = StandardTestDispatcher(),) : TestWatcher() { override fun starting(description: Description) = Dispatchers.setMain(dispatcher) override fun finished(description: Description) = Dispatchers.resetMain()}…Этот тест никогда не завершается и падает по таймауту. Почему, и как протестировать
StateFlowправильно?@get:Rule val mainRule = MainDispatcherRule() @Test fun `search emits loading then results`() = runTest { val vm = SearchViewModel(FakeRepo()) // state: StateFlow<UiState> vm.search("room") val states = vm.state.toList() assertEquals( listOf(UiState.Loading, UiState.Results(listOf("Room"))), states, )}Найдите баг: этот тест ViewModel проходит локально, но на CI падает примерно в половине запусков.
class ProfileViewModel(private val repo: ProfileRepo) : ViewModel() { val state = MutableStateFlow<UiState>(UiState.Loading) fun load() = viewModelScope.launch { val name = withContext(Dispatchers.IO) { repo.profile() } state.value = UiState.Loaded(name) }} @Test fun `load shows the profile`() = runTest { // MainDispatcherRule is set val vm = ProfileViewModel(FakeProfileRepo()) vm.load() advanceUntilIdle() assertTrue(vm.state.value is UiState.Loaded)}Найдите баг: этот тест остаётся зелёным, даже если удалить
emitизpay().class CheckoutViewModel : ViewModel() { private val _events = MutableSharedFlow<Event>() val events: SharedFlow<Event> = _events fun pay() = viewModelScope.launch { _events.emit(Event.OpenReceipt) }} @Test fun `pay opens the receipt`() = runTest { // MainDispatcherRule is set val vm = CheckoutViewModel() backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { vm.events.collect { assertEquals(Event.OpenReceipt, it) } } vm.pay() advanceUntilIdle()}Как покрыть unit-тестом ViewModel, которая использует
SavedStateHandle, берёт строки изContextили наблюдает за жизненным циклом?// shown as an observer only to demonstrate TestLifecycleOwnerclass OrderViewModel( savedState: SavedStateHandle, private val repo: OrderRepository,) : ViewModel(), DefaultLifecycleObserver { val orderId: Long = checkNotNull(savedState["orderId"])…Что такое Robolectric и чего он не может доказать по сравнению с инструментальным тестом?
@RunWith(AndroidJUnit4::class) // Robolectric, because the file is in src/test@Config(sdk = [35])class ReceiptFormatterTest { private val context: Context = ApplicationProvider.getApplicationContext() @Test fun formatsFromRealStringResources() {…Чего Espresso ждёт автоматически и когда нужен
IdlingResource?// Ships in the app; the release build can use a no-op implementationobject AppIdling { val network = CountingIdlingResource("network")} class SearchRepository(private val api: Api) {…Этот Compose-тест падает с «could not find any node», хотя цена видна на экране. Почему и как исправить?
@Composablefun OrderRow(order: Order, onOpen: () -> Unit) { Button(onClick = onOpen) { Column { Text(order.title) Text(order.priceLabel, Modifier.testTag("price")) } }} @Test fun showsPrice() { rule.setContent { OrderRow(Order(42, "Socks", "12.00 €"), onOpen = {}) } rule.onNodeWithTag("price").assertTextEquals("12.00 €")}Как протестировать воркер WorkManager и доказать, что его constraints действительно его блокируют?
@RunWith(AndroidJUnit4::class)class UploadWorkTest { private val context: Context = ApplicationProvider.getApplicationContext() @Before fun setUp() { val config = Configuration.Builder()…Как работают screenshot-тесты на Android (Paparazzi, Roborazzi, на устройстве) и что держит golden-изображение стабильным?
// Paparazzi: layoutlib on the JVM, the device is a data classclass OrderCardTest { @get:Rule val paparazzi = Paparazzi( deviceConfig = DeviceConfig.PIXEL_5.copy(fontScale = 1.0f, locale = "en"), )…Почему инструментальные UI-тесты бывают флаки и как найти и устранить причину?
Что на самом деле показывает покрытие кода и на чём бы вы построили порог для CI-сборки?