Test Doubles — са заместващи обекти, използвани в единични тестове вместо реални зависимости. Терминът е введен от Gerard Meszaros в книгата „xUnit Test Patterns“ (2007) като обобщаващо понятие за Mock, Stub, Fake, Spy и Dummy. Според Martin Fowler (2024), Test Doubles позволяват изолиране на тествания компонент от неговата среда, правейки тестовете детерминистични, бързи и независими от външни услуги.
Основни моменти
Test Doubles — е термин от автомобилната индустрия (каскадьор, „dубл“ за актьори), пренесен в разработването на софтуер. Както каскадьорът замества актьор в опасна сцена, Test Double замества реален компонент в тестов сценарий. Това е необходимо, когато реалната зависимост е недостъпна, бавна, недетерминистична или има странични ефекти.
Концепцията Test Double обхваща пет конкретни вида, всеки от които решава своя задача. Типологията на Meszaros е канонична и се използва във всички модерни ръководства за тестване. Разликата между видовете е в степена на контрол и верификация: от просто попълване на параметри (Dummy) до пълна проверка на последователността на извиквания (Mock).
Основната цел на Test Doubles е изолирането на тествания модул. В мобилното разработване реалните зависимости са API сървъри, бази от данни, файлова система, сензори на устройството, системни услуги (LocationManager, Camera, Bluetooth). Прякото използване на тези компоненти прави тестовете бавни, крехки и зависими от средата. Според Google Testing Blog (2023), добре изолираните единични тестове се изпълняват за милисекунди, а интеграционните тестове за секунди и минути.
Класификацията на Gerard Meszaros включва пет вида Test Doubles, които се различават по поведение и цел на използване. Разбирането на разликата между тях е основата на правилното единично тестване.
Dummy — е обект, който се подава на тествания метод, но никога не се използва. Dummy е нужен само за задоволяване на подписа на метода. В Kotlin това е често null, emptyList() или обект с заместители. Dummy не трябва да съдържа никаква логика — ако бъде извикан, тестът трябва да се провали.
Fake — е опростена, но работеща имплементация на интерфейс. За разлика от Mock и Stub, Fake съдържа реална бизнес логика, но в опростена форма. Класическият пример — InMemoryUserRepository, който съхранява данни в HashMap вместо база от данни. Fake се използва, когато трябва да се тества логика, зависеща от състояние, но без натоварването на реалната инфраструктура.
| Вид | Предназначение | Пример |
|---|---|---|
| Dummy | Попълване на параметър | null, празен обект |
| Fake | Работеща опростена имплементация | InMemoryRepository |
| Stub | Връщане на фиксирана стойност | when(api.getUser()).thenReturn(user) |
| Spy | Записване на извиквания за проверка | verify(spy).save(user) |
| Mock | Проверка на интеракцията | verify(mock).sendEmail(email) |
Stub връща предварително определени стойности на конкретни извиквания. Stub не проверява дали е бил извикан — той просто предоставя данни. В Mockito Stub се създава чрез when(method).thenReturn(value). Stub е идеален за тестване, когато зависимостта трябва да върне конкретна стойност, но самият факт на извикването не е важен.
Spy — е обвивка около реален обект, която записва всички извиквания за последваща верификация. За разлика от Mock, Spy делегира извикванията към реалния обект, но позволява да се провери, че те са се случили. В Mockito Spy се създава чрез spy(realObject). Spy е полезен за частично mocking, когато искате да използвате реален обект, но да проверите някои извиквания.
Mock — е обект с предварително определени очаквания за извиквания. Mock проверява дали определени методи са били извикани с определени аргументи и в определен ред. За разлика от Stub, Mock се фокусира върху верификацията на поведението, а не върху връщането на данни. Mock е най-мощният и най-често използван тип Test Double в мобилното разработване.
Разликата между Mock и Stub често причинява объркване дори между опитни разработчици. Основната разлика е в целта: Stub проверява състояние (state verification), Mock проверява поведение (behavior verification).
Stub отговаря на въпроса: „кодът върна ли правилния резултат?“. Mock отговаря на въпроса: „кодът извика ли правилните методи с правилните аргументи?“. В мобилното разработване Stub се използва, когато резултатът е важен (напр. данни от репозиториум), а Mock — когато страничните ефекти са важни (напр. изпращане на email, запис в база от данни).
// Stub: проверка на състоянието
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: проверка на поведението
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
Практически примери за всички пет вида Test Doubles в Kotlin с използване на MockK — най-популярната библиотека за mocking за Android проекти.
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub: връщане на фиксиран API отговор
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: проверка, че потребителят е запазен
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: Context не се използва вътре в Logger
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
Изборът на тип Test Double зависи от това, какво точно се тества: състояние, поведение или интеграция. В мобилното разработване на Android и iOS са се формирали следните препоръки.
При тестване на ViewModel използвайте Mock за зависимостите, които произвеждат странични ефекти (репозиториуми, аналитика, навигация) и Stub за зависимостите, които връщат данни (API клиенти, ContentProvider). Това позволява да се провери, че ViewModel обработва правилно както успешните, така и грешните сценарии.
На ниво Repository се предпочитат Fake (in-memory имплементации на база от данни) и Stub (фиксирани API отговори). Fake позволява да се тества логиката на кеширането и офлайн режима без конфигурация на SQLite. Stub симулира различни HTTP статусове: 200, 404, 500, timeout.
Неправилното използване на Test Doubles — една от най-честите причини за крехки тестове, които се чупат при всяко рефакториране.
Най-честата грешка — mocking на всичко. Ако всяка зависимост в теста е заменена с Mock, тестът престава да проверява реалното поведение. Mock трябва да бъде само за външни зависимости (мрежа, база от данни, файлова система, системни услуги). Вътрешните компоненти на приложението (Value Object, data class, прости утилити) не трябва да бъдат заменяни.
Втората грешка — създаване на Mock без определяне на очаквания. Ако метод се извика без every / when, Mock връща стандартна стойност (null, 0, false). Това може да доведе до фалшиво позитивни тестове, когато Mock тяхо връща null, а тестът интерпретира това като правилно поведение.
Третата грешка — проверка на всяко извикване на всеки Mock. Verify трябва да се използва само за извиквания, които са критични от гледна точка на бизнес логиката. Прекалената верификация прави тестовете крехки: промяната на реда на извикванията в производствения код чупи тестовете без промяна на поведението.
Често задавани въпроси
Stub връща данни и проверява състояние (какво е било върнато), а Mock проверява поведение (кои методи са били извикани). Stub = „върни X“, Mock = „провери, че Y е бил извикан с аргумент Z“. В реалните тестове един обект често изполнява ролята и на Stub, и на Mock едновременно.
Fake се предпочита пред Mock, когато се тества логика, зависеща от състояние: кеширане, офлайн режим, транзакции. Fake (in-memory имплементация) позволява да се тестват тези сценарии без крехки verify извиквания. Mock е по-подходящ за проверка на изпращане на данни: аналитика, push известия, email.
За Android проекти в Kotlin се препоръчва MockK. То подкрепа корутини, suspend функции, sealed class и extension функции без допълнителна конфигурация. За Java проекти стандартът остава Mockito — най-популярната библиотека с обширна документация.
За тестване на Kotlin Flow използвайте библиотеката Turbine заедно с MockK. Turbine опрощава проверката на емисията на Flow: може да се провери редът на стойностите, завършването на потока и изключенията. Stub за Flow връща flowOf(value), Mock проверява дали Flow е бил събран.
Да, но на ниво API отговори, а не UI компоненти. Библиотеките MockWebServer (OkHttp) и WireMock позволяват да се заместват HTTP отговорите в UI тестове. Самите UI компоненти (Compose, SwiftUI Views) не трябва да бъдат заменяни — тяхното поведение се тества чрез screenshot тестове и Espresso.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също