Stub (șab, substituent) — un obiect de test care returnează răspunsuri predefinite la apelurile de metode în locul implementării reale. În dezvoltarea mobilă, stub-urile izolează modulul testat de cererile de rețea, baza de date și sistemul de fișiere, permițând verificarea logicii fără configurarea mediului. Spre deosebire de mock, stub nu verifică comportamentul — el doar furnizează date. Mai multe detalii în articolul lui Martin Fowler despre test doubles.
Principalele
Stub — este un obiect-substituent care înlocuiește dependența reală în test și returnează valori prestabilite la apeluri specifice. Termenul a fost introdus în clasificarea lui Gerard Meszaros (2007) în cartea „xUnit Test Patterns". Stub aparține categoriei test doubles — obiecte care înlocuiesc componentele reale în timpul testării. Scopul principal al stub-ului este de a furniza blocului testat date previzibile, eliminând incertitudinea sistemelor externe.
Principiul de funcționare — testul configurează stub-ul înainte de execuție: „când metoda getUsers() va fi apelată, returnează această listă de utilizatori". Stub nu conține logică de afaceri, nu verifică succesiunea apelurilor și nu înregistrează istoricul accesărilor. El pur și simplu stă în locul componentei reale și furnizează ceea ce i s-a spus. În contextul testării Android, aceasta înseamnă că clientul OkHttp nu face o cerere reală la server, ci primește răspunsul de la MockWebServer configurat ca stub.
Când să folosiți — stub-urile sunt optime pentru testarea stratului UI (ViewModel, Presenter) și a logicii de afaceri (UseCase, Interactor), unde trebuie verificată reacția la date specifice: listă goală, serverul a returnat eroarea 500, token expirat. Orice caz în care testul necesită o stare de intrare specifică este o sarcină pentru stub. Pentru fiecare scenariu de test se creează propria configurație a stub-ului, ceea ce face testele lizibile și previzibile.
Gerard Meszaros (2007) în cartea „xUnit Test Patterns" a identificat cinci tipuri de test doubles: dummy, stub, spy, mock, fake. Fiecare tip își rezolvă propria sarcină. Dummy — este transmis, dar nu este utilizat. Stub — returnează date. Spy — înregistrează apelurile. Mock — verifică comportamentul. Fake — conține logică simplificată. Înțelegerea acestei clasificări ajută dezvoltatorul să aleagă instrumentul potrivit pentru fiecare scenariu de test.
Cererile de rețea — cel mai frecvent scenariu de utilizare a stub-urilor. Aplicația face apeluri HTTP către API, iar în test trebuie verificată reacția la diferite răspunsuri: JSON reușit, eroare 401 (neautorizat), timeout, matrice goală. MockWebServer (OkHttp) pe Android și URLProtocol (iOS) acționează ca stub-uri, returnând răspunsuri HTTP predefinite fără conexiune reală la server. Aceasta accelerează testele de la secunde la milisecunde.
Baza de date — Room (Android) și CoreData (iOS) au variante in-memory, dar configurarea lor necesită totuși timp. Stub în locul repository-ului returnează liste pregătite în avans de Entity, fără a atinge baza de date. Acest lucru este deosebit de eficient pentru testarea ViewModel, unde trebuie verificată sortarea, filtrarea sau transformarea datelor. Testul se execută în milisecunde, indiferent de volumul de date.
Servicii de sistem — LocationManager, SensorManager, SharedPreferences necesită dispozitiv real sau emulator. Stub pentru LocationProvider returnează coordonate specificate, pentru SensorManager — valori fixe ale accelerometrului. Pe iOS, analogul este CLLocationManager cu implementare de test a delegatului. Fără stub-uri, astfel de teste necesită un dispozitiv fizic cu condiții specifice.
Sistemul de fișiere și cache — încărcarea imaginilor, stocarea în cache a răspunsurilor, lucrul cu fișierele de configurare — toate aceste operații depind de starea discului. Stub pentru FileManager sau ImageCache returnează succes/eroare fără a citi fișiere reale. Aceasta elimină căderile false ale testelor din cauza nepotrivirii căilor sau permisiunilor pe diferite mașini ale dezvoltatorilor.
Separarea responsabilităților — trei tipuri de test doubles rezolvă sarcini diferite. Stub: „dă-mi date". Mock: „verifică dacă am fost apelat". Fake: „funcționez ca unul real, doar mai simplu". Diferența este critică pentru lizibilitatea testelor: dacă testul folosește mock acolo unde este necesar stub, el este supraîncărcat cu apeluri verify care nu au legătură cu scenariul testat.
| Caracteristică | Stub | Mock | Fake |
|---|---|---|---|
| Scop | Furniza date | Verifica interacțiunea | Implementare simplificată |
| Logică | Nu | Nu | Da (dar simplificată) |
| Verificare | Nu | Da (verify) | Indirectă (prin stare) |
| Flexibilitate | Scăzută — răspunsuri fixe | Medie | Ridicată — logica se adaptează |
| Viteză | Maximă | Ridicată | Medie |
| Exemplu | MockWebServer returnează JSON | Mockito.verify(repository).save() | InMemoryRepository cu HashMap |
Regula practică — dacă testul verifică ce date a primit componentul testat — folosiți stub. Dacă testul verifică dacă componentul a apelat metoda dependenței cu argumentele corecte — folosiți mock. Dacă doriți pur și simplu să înlocuiți baza de date cu un tabel hash — acesta este fake. Amestecarea tipurilor într-un singur test îl face fragil: la schimbarea implementării va trebui rescris atât stub-ul, cât și logica verify.
Stub cu verify — o greșeală frecventă când dezvoltatorul configurează stub-ul și apoi adaugă verify(stub).method(). Stub prin definiție nu trebuie verificat — pentru verificare există mock. Dacă trebuie să verificați că metoda a fost apelată cu argumente specifice, folosiți Mockito.mock() în loc de Mockito.stub(). Această separare menține intenția testului clară pentru alți dezvoltatori.
MockWebServer — biblioteca OkHttp pentru crearea stub-urilor HTTP pe Android și JVM. Aceasta pornește un server HTTP local pe un port specificat, care interceptează cererile clientului OkHttp și returnează răspunsuri predefinite. Configurarea durează trei linii: creați serverul, adăugați răspunsul în coadă (enqueue), porniți. Testul poate adăuga secvențial mai multe răspunsuri în coadă pentru scenarii cu paginare sau reîncercări.
class UserRepositoryTest {
private val server = MockWebServer()
fun setup() {
server.start(8080)
val client = OkHttpClient.Builder()
.readTimeout(1, TimeUnit.SECONDS)
.build()
}
fun test_user_list_success() {
val json = "[{\"id\":1,\"name\":\"Alice\"}]"
server.enqueue(MockResponse()
.setBody(json)
.setResponseCode(200)
)
val result = repository.getUsers()
assertEquals(1, result.size)
}
fun teardown() {
server.shutdown()
}
}
MockK — alternativă la Mockito pentru Kotlin cu suport first-class pentru corutine, funcții de extensie și clase sealed. Stub-urile în MockK se creează prin coEvery (pentru funcții suspend) și every (pentru funcții obișnuite). Spre deosebire de MockWebServer, MockK înlocuiește metode individuale de dependență, nu întregul strat HTTP. Acest lucru este convenabil pentru testele unitare ale UseCase sau Interactor, unde dependențele sunt abstracții ale repository-urilor.
interface UserRepository {
suspend fun getUsers(): List<User>
}
class GetUsersUseCaseTest {
private val repo = mockk<UserRepository>()
private val useCase = GetUsersUseCase(repo)
fun test_empty_list() = runTest {
coEvery { repo.getUsers() } returns emptyList()
val result = useCase.invoke()
assertTrue(result.isEmpty())
coVerify(exactly = 1) { repo.getUsers() }
}
}
Best practice — pentru testele de integrare folosiți MockWebServer (interceptează HTTP real), pentru testele unitare — MockK (înlocuiește interfețe). Nu înlocuiți ceea ce nu testați: dacă testul verifică Repository, nu înlocuiți clientul OkHttp în interiorul său — folosiți MockWebServer real la nivelul HTTP. Această regulă menține testele relevante și reduce fragilitatea la refactorizare.
Protocoalele Swift ca stub-uri — în abordarea nativă iOS, stub-ul se implementează prin substituirea unei structuri de test care respectă protocolul dependenței. În locul NetworkService real, testul primește StubNetworkService care returnează date fixe. Swift este un limbaj cu tipizare statică, deci stub-ul trebuie să respecte același protocol ca și serviciul real. Compilatorul garantează că stub-ul implementează toate metodele cerute.
protocol NetworkServiceProtocol {
func fetchUsers() async throws -> [User]
}
struct StubNetworkService: NetworkServiceProtocol {
let result: Result<[User], Error>
func fetchUsers() async throws -> [User] {
try result.get()
}
}
final class UsersViewModelTests: XCTestCase {
func test_success_state() async {
let stub = StubNetworkService(
result: .success([User(name: "Alice")])
)
let vm = UsersViewModel(service: stub)
await vm.load()
XCTAssertEqual(vm.users.count, 1)
}
}
OCMock pentru Objective-C — bibliotecă pentru crearea stub-urilor și mock-urilor în proiecte iOS moștenite. OCMock suportă metode stub cu argumente și valori de returnare. Proiectele moderne în Swift preferă abordarea bazată pe protocoale cu stub-uri manuale — aceasta oferă control asupra fiecărei metode și nu necesită dependențe externe. OCMock rămâne o opțiune pentru proiectele unde protocolizarea tuturor dependențelor este economică nejustificată.
URLProtocol pentru stub-uri HTTP — mecanismul de sistem iOS pentru interceptarea cererilor de rețea printr-o subclasă URLProtocol. Testul înregistrează un URLProtocol personalizat care interceptează URLSession și returnează răspunsuri stub. Avantaj față de stub-urile manuale: nu este nevoie să schimbați arhitectura aplicației — URLSession rămâne real, dar datele sunt înlocuite la nivel de protocol. Dezavantaj: mai dificil de depanat decât un serviciu stub explicit.
Întrebări frecvente
Stub returnează date predefinite și nu verifică faptul apelului. Mock suplimentar verifică că metoda a fost apelată cu argumentele corecte (verify). Stub răspunde la întrebarea „ce să returnez", Mock — la întrebarea „a fost apelat". Folosiți stub pentru verificarea stării, mock — pentru verificarea interacțiunii.
Fake este necesar când testul necesită o implementare funcțională (chiar dacă simplificată) — de exemplu, o bază de date in-memory în loc de Room. Stub este potrivit pentru scenarii singulare cu date predefinite. Dacă repetați același stub în 10 teste — cel mai probabil aveți nevoie de Fake. Fake reduce duplicarea deoarece logica trăiește într-o singură clasă.
Pe Android — MockK pentru obiecte Kotlin (object) suportă mockkObject(), inclusiv metode statice ale claselor Java prin mockkStatic(). Pe iOS — metodele statice Swift nu se stub-uiesc direct; folosiți protocoale și DI pentru a înlocui apelul static cu o metodă de instanță a protocolului. Stub-urile statice sunt datorii tehnice și trebuie evitate în codul nou.
Folosiți MockWebServer (OkHttp) — funcționează ca un server HTTP local care pune răspunsurile în coadă (enqueue). Pentru Retrofit este suficient să schimbați URL-ul de bază la localhost:8080. Pentru Ktor folosiți MockEngine — mecanismul încorporat pentru înlocuirea HttpStatement. Ambele abordări funcționează fără internet real și oferă control complet asupra codului de stare, corpului și antetelor răspunsului.
Spy îmbracă obiectul real și înregistrează apelurile, în timp ce Stub înlocuiește complet obiectul cu răspunsuri fixe. Spy permite utilizarea parțială a implementării reale (celelalte metode funcționează ca înainte), iar stub — nu. Dacă trebuie să verificați că metoda a fost apelată, dar o parte din logică trebuie să se execute — folosiți spy, nu stub.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și