Testarea unitară este o metodă de verificare a software-ului prin care se testează corectitudinea modulelor individuale sau a funcţiilor de cod în izolare faţă de restul sistemului. Potrivit lui Martin Fowler, 2026, testele unitare sunt fundamentul CI/CD şi al refactorizării, oferind feedback rapid despre funcţionarea codului. Testarea modulară ajută la detectarea erorilor în fazele incipiente ale dezvoltării, reducând costul remedierii acestora de zeci de ori.
Principalele aspecte
Testarea unitară este procesul de verificare a unităţilor individuale (unit) ale codului sursă — funcţii, metode, clase — în izolare de restul programului. Fiecare test execută un scenariu specific de utilizare a modulului şi verifică dacă rezultatul corespunde celui aşteptat. Testele unitare se scriu în acelaşi limbaj de programare ca şi codul principal şi se execută automat în mediul de dezvoltare sau în pipeline-ul CI/CD. Spre deosebire de testele de integrare, testele unitare nu interacţionează cu baze de date reale, sistemul de fişiere sau servicii de reţea.
Scopul principal este feedback-ul rapid despre corectitudinea codului după modificări. Dacă un dezvoltator refactorizează o metodă, setul de teste unitare confirmă că comportamentul nu s-a stricat. Potrivit Google Testing Blog (2025), proiectele cu acoperire a testelor unitare peste 60% au de 2,5 ori mai puţine incidente de producţie. Avantaje suplimentare: documentarea codului (testele arată cum să foloseşti API-ul), simplificarea refactorizării (poţi schimba implementarea păstrând comportamentul) şi diagnosticarea rapidă a regresiunilor.
Nu orice test automat este un test unitar. Criterii: se testează un singur modul (clasă sau funcţie), dependenţele externe sunt înlocuite cu mockuri sau stuburi, testul se execută în milisecunde, nu necesită pornirea unui server sau a unei baze de date. Un test care accesează o bază de date reală este un test de integrare. Un test care deschide un browser este un test E2E. Înţelegerea graniţelor dintre tipurile de teste este importantă pentru distribuirea corectă a eforturilor în piramida testării.
Testele unitare de calitate respectă principiile FIRST formulate de Robert C. Martin. Fiecare test trebuie să fie Fast (rapid — milisecunde), Isolated (izolat — nu depinde de alte teste), Repeatable (repetabil — acelaşi rezultat pe orice maşină), Self-validating (autoverificabil — rezultatul este „passed” sau „failed”, fără verificare manuală) şi Timely (oportun — scris înainte sau simultan cu codul). Încălcarea oricărui principiu reduce valoarea testului.
Şablonul standard pentru scrierea testelor unitare. Arrange — pregătirea datelor şi a dependenţelor: crearea obiectelor, configurarea mockurilor, setarea parametrilor de intrare. Act — executarea acţiunii testate: apelarea metodei sau funcţiei. Assert — verificarea rezultatului: compararea valorii reale cu cea aşteptată. Împărţirea în trei blocuri face testul lizibil şi uşor de înţeles. Dacă blocul Assert necesită logică complexă, testul probabil verifică prea multe lucruri deodată.
// Exemplu de test unitar după şablonul AAA în Kotlin cu JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — creăm obiectul testat
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — executăm acţiunea
val result = calculator.add(2, 3)
// ASSERT — verificăm rezultatul
Assertions.assertEquals(5, result)
}
}
Numele testului trebuie să descrie ce se verifică şi care este rezultatul aşteptat. Format: [methodName]_[scenario]_[expectedResult]. Exemplu: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. Un nume bun de test înlocuieşte comentariul şi la eşec indică imediat ce funcţionalitate este încălcată. Evitaţi nume precum test1, checkSomething sau verify — nu transmit informaţii şi îngreunează diagnosticarea.
Pentru izolarea modulului testat de dependenţele externe se folosesc dubluri de test (test doubles). Tipurile principale: mockuri (mocks) — verifică dacă o anumită metodă a fost apelată cu parametrii corecţi; stuburi (stubs) — returnează valori prestabilite la apelarea metodei; fake-uri (fakes) — implementare simplificată a unui component real (de exemplu, InMemoryUserRepository în loc de UserRepository care lucrează cu baza de date). Alegerea depinde de ce trebuie verificat: starea (stub) sau interacţiunea (mock).
| Dublură | Ce verifică | Exemplu |
|---|---|---|
| Mock | Apelarea metodei cu parametrii corecţi | userRepository.save(user) a fost apelat exact 1 dată |
| Stub | Valoarea returnată | repository.findById(1) returnează User(id=1, name=„Test”) |
| Fake | Logica prin implementare simplificată | InMemoryMapUserRepository cu HashMap în loc de bază de date |
| Spion | Mocking parţial al unui obiect real | spy(repo).when(findById).thenReturn(user) |
Mockito este cel mai popular framework de mock în Java şi Kotlin. Permite crearea de mockuri prin mock(), configurarea valorilor returnate prin when().thenReturn() şi verificarea apelurilor prin verify(). Versiunile moderne de Mockito (5.x) suportă mockuri statice (mockStatic) şi sintaxă simplificată prin BDDMockito (given-willReturn). O regulă importantă: nu mocka ceea ce nu este al tău — nu crea mockuri pentru obiecte-valoare şi biblioteci standard.
// Exemplu de test unitar cu Mockito în Kotlin
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (Test-Driven Development) — metodologia în care testul se scrie înainte de implementarea codului. Ciclul „Red-Green-Refactor”: scrie un test care eşuează (Red), scrie codul minim pentru a trece testul (Green), îmbunătăţeşte codul fără a schimba comportamentul (Refactor). TDD garantează că tot codul este acoperit de teste (acoperire = 100% pentru funcţionalitatea scrisă) şi că codul este testabil — dacă codul este greu de testat, înseamnă că arhitectura necesită îmbunătăţiri.
Potrivit cercetărilor IBM (2006-2026, studiu longitudinal), echipele care folosesc TDD au cu 40-80% mai puţine defecte în producţie comparativ cu echipele care scriu teste după cod. TDD îmbunătăţeşte de asemenea arhitectura: dezvoltatorul trebuie să se gândească la design-ul API înainte de implementare, ceea ce duce la cuplare slabă (loose coupling) şi coeziune ridicată (high cohesion). Un efect suplimentar — documentare prin cod viu: testele servesc ca specificaţie a comportamentului modulului, care este mereu actualizată.
TDD nu este întotdeauna optim. Componentele UI sunt greu de testat în izolare — pentru ele sunt mai eficiente testele snapshot sau testele de regresie vizuală (Percy, Chromatic). Prototiparea şi cercetarea (spike solutions) nu necesită teste. Codul legacy fără teste este greu de acoperit prin TDD — aici sunt necesare mai întâi characterization tests (teste care fixează comportamentul curent înainte de refactorizare). În aceste cazuri, TDD nu este complet abandonat, ci adaptat — se scriu teste pentru funcţionalitatea modificată, nu pentru tot codul legacy.
Dezvoltarea mobilă are specificul ei: logica de business este adesea amestecată cu codul UI (Activity, ViewController, ViewModel), ceea ce complică testarea unitară. Cea mai bună practică — view-uri subţiri, ViewModel-uri groase: mutaţi toată logica din componentele UI în clase separate (UseCase, Repository, ViewModel) care pot fi testate uşor fără emulator. Pentru Android şi iOS există frameworkuri native de testare unitară care rulează pe JVM/Native fără a porni dispozitivul.
Testele unitare Android se execută pe JVM local fără emulator, ceea ce asigură viteză de execuţie — un test tipic durează sub 100ms. JUnit 5 este runner-ul principal. Pentru testele ViewModel folosiţi kotlinx-coroutines-test pentru testarea corutinelor şi Turbine pentru testarea StateFlow. Robolectric permite testarea componentelor dependente de Android (Context, Resources) fără emulator, încărcând clase shadow. Pentru testele Compose folosiţi Compose UI Test — dar acestea sunt deja teste UI, nu unitare.
Testele unitare iOS se scriu în Swift cu XCTest (încorporat în Xcode). Quick + Nimble — frameworkuri BDD pentru teste mai lizibile (describe/context/it). Pentru mocking folosiţi Cuckoo (generare de mockuri) sau SwiftyMocky. Swift suportă protocoale şi injectarea dependenţelor, ceea ce facilitează înlocuirea dependenţelor. Aspect cheie: testele unitare iOS rulează pe simulatorul macOS, nu pe un dispozitiv real. Testele care necesită funcţii hardware (cameră, Bluetooth) sunt teste de integrare.
Testele unitare Flutter folosesc pachetul flutter_test şi rulează pe Dart VM fără emulator. Pentru mocking — pachetul mockito cu generator de cod (build_runner). Testele de widgeturi (în acelaşi pachet) testează widgeturi individuale, dar necesită randare şi rulează mai lent — folosiţi-le doar pentru verificarea logicii UI. Logica Dart pură (modele, repository-uri, blocuri) se testează ca teste Dart obişnuite fără a importa flutter_test.
// Exemplu de test unitar pe Flutter cu mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
Testarea unitară eficientă necesită disciplină. Regula principală: testaţi comportamentul, nu implementarea. Testul nu trebuie să ştie cum este implementat modulul intern (ce metode private sunt apelate, în ce ordine). Dacă testul este legat de implementare, se strică la fiecare refactorizare şi şi pierde valoarea. Testul verifică un contract: la intrarea X trebuie să avem ieşirea Y. Excepţie — testele pentru algoritmi cu performanţă critică, unde ordinea apelurilor este importantă.
100% acoperire este un scop de neatins şi inutil. Potrivit Google Testing Blog (2025), nivelul optim de acoperire pentru testele unitare este de 70-80% din liniile de cod. 100% acoperire se atinge adesea prin testarea getterelor, setterelor şi constructorilor, ceea ce nu aduce valoare. Concentraţi-vă pe logica de business critică: calcule complexe, validare, gestionarea erorilor, cazuri marginale. Folosiţi JaCoCo (Java), Coverage.py (Python), Istanbul (JS) pentru măsurare şi setaţi un prag în CI — eşecul build-ului la acoperire sub 60%.
Testele unitare sunt prima etapă a oricărui pipeline CI/CD. Se execută la fiecare push în repository, înainte de build şi deploy. Timpul mediu de execuţie a testelor unitare într-un proiect nu trebuie să depăşească 5 minute — dacă este mai lung, testele încetează să fie „rapide” şi dezvoltatorii încetează să le ruleze local. Împărţiţi testele în rapide (unitare) şi lente (de integrare) şi rulaţi-le în etape diferite ale pipeline-ului. Folosiţi execuţia paralelă şi fail-fast pentru accelerare.
Întrebări frecvente
Testul unitar verifică un singur modul în izolare, înlocuind dependenţele externe cu mockuri. Testul de integrare verifică interacţiunea dintre mai multe componente reale (bază de date, API, sistem de fişiere). Testele unitare se execută în milisecunde, cele de integrare în secunde. În piramida testării, testele unitare ocupă 70%.
Alegerea depinde de platformă: JUnit 5 pentru Java/Kotlin, XCTest pentru iOS/Swift, pytest pentru Python, Jest/Vitest pentru JavaScript/TypeScript, flutter_test pentru Flutter. Pentru mocking folosiţi Mockito (Java), Cuckoo (iOS), unittest.mock (Python) sau vitest.mock (JS). Toate frameworkurile moderne suportă teste parametrizate, aserţiuni încorporate şi executare paralelă.
Fast — testul se execută în milisecunde. Isolated — nu depinde de alte teste şi sisteme externe. Repeatable — dă acelaşi rezultat pe orice maşină. Self-validating — verifică rezultatul automat. Timely — scris înainte sau sincron cu codul. Încălcarea oricărui principiu reduce eficienţa testării.
Da, obligatoriu. ViewModel conţine logica de business — gestionarea evenimentelor, transformarea datelor, gestionarea stării. Pe Android folosiţi kotlinx-coroutines-test pentru corutine şi Turbine pentru testarea StateFlow. Pe iOS testaţi Combine Publishers sau async/await în ViewModel. Testele ViewModel sunt teste unitare pure care rulează pe JVM/macOS fără emulator.
Cererile de reţea în testele unitare nu se execută — sunt înlocuite cu mockuri ale clientului HTTP. Pe Android folosiţi MockWebServer (OkHttp) — acesta porneşte un server HTTP local, ceea ce este preferabil mockurilor deoarece reproduce interacţiunea reală de reţea. MockWebServer oferă izolare fără a pierde realismul. Pentru iOS — OHHTTPStubs sau URLProtocol pentru interceptarea şi înlocuirea răspunsurilor.
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