Тестирование общего кода

Главный дивиденд KMP: пишете бизнес-тесты один раз — они защищают обе платформы.

commonTest — исходный набор для тестов общего кода; они запускаются под все цели, проверяя, что логика ведёт себя одинаково на Android и iOS.

Почему это важнее, чем кажется

Главная боль двух кодовых баз — расходящиеся бизнес-правила. В KMP логика общая, и тесты к ней тоже общие. Один тест «скидка 110% запрещена» защищает обе платформы. Это, возможно, самый недооценённый выигрыш KMP: не только меньше кода, но и единая гарантия корректности.

Общий тест

// commonTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class PriceCalculatorTest {
    private val calc = PriceCalculator()

    @Test fun appliesDiscount() {
        assertEquals(80.0, calc.finalPrice(100.0, 20))
    }

    @Test fun rejectsInvalidDiscount() {
        assertFailsWith<IllegalArgumentException> {
            calc.finalPrice(100.0, 110)
        }
    }
}

Этот тест выполнится и под JVM, и под Kotlin/Native — если логика где-то разойдётся, упадёт соответствующая цель.

Мокаем зависимости интерфейсами

Здесь окупается архитектура на интерфейсах: чтобы протестировать репозиторий, подставляем фейковый API без всякой сети.

// commonTest
class FakeOrderApi(private val data: List<OrderDto>) : OrderApiContract {
    override suspend fun fetchOrders() = data
}

class OrderRepositoryTest {
    @Test fun mapsToDomain() = runTest {
        val repo = OrderRepository(FakeOrderApi(listOf(OrderDto(1, 50.0))), FakeCache())
        repo.refresh()
        // проверяем содержимое кэша
    }
}

runTest из kotlinx-coroutines-test запускает корутины в тесте детерминированно.

Как работает под капотом

Тесты commonTest компилируются под каждую цель как обычный код и запускаются её средствами: JVM-тесты через JUnit-подобный раннер, iOS-тесты — через нативный тест-раннер на симуляторе. Поэтому общий тест реально исполняется дважды (или больше), на разных бэкендах. Если бы где-то поведение зависело от платформы (например, форматирование чисел), это всплыло бы как падение одной из целей. Так KMP заодно ловит скрытые платформенные различия.

Частые ошибки

Полагаться на expect/actual в тестируемом коде так, что тест требует реальной платформенной реализации — лучше абстрагировать через интерфейс и подменять фейком. Вторая ошибка — тестировать корутины без runTest, получая флаки из-за реального времени и потоков. Третья — гонять тесты только под JVM (быстрее) и пропускать iOS-цель, теряя смысл «одинаково на обеих платформах».

Итоги

  • Общие тесты в commonTest защищают логику на всех платформах сразу.
  • Архитектура на интерфейсах позволяет мокать зависимости без сети и БД.
  • runTest делает тесты корутин детерминированными.
  • Тесты исполняются под каждую цель, попутно ловя платформенные различия.
Проверьте себя
1. В чём главный выигрыш общих тестов в KMP?
AОни быстрее обычных
BОдин тест бизнес-логики защищает обе платформы и ловит расхождения
CОни не требуют кода
DОни работают без компиляции
2. Что делает runTest из kotlinx-coroutines-test?
AЗапускает реальную сеть
BВыполняет корутины в тесте детерминированно, без реального времени
CСобирает фреймворк
DМокает UI