CodeGym /Blog Java /Aleatoriu /Totul despre testarea unitară: tehnici, concepte, practic...
John Squirrels
Nivel
San Francisco

Totul despre testarea unitară: tehnici, concepte, practică

Publicat în grup
Astăzi nu vei găsi o aplicație care să nu fie acoperită cu teste, așa că acest subiect va fi mai relevant ca niciodată pentru dezvoltatorii începători: nu poți reuși fără teste. Să luăm în considerare ce tipuri de testare sunt utilizate în principiu și apoi vom studia în detaliu tot ce trebuie să știm despre testarea unitară. Totul despre testarea unitară: tehnici, concepte, practică - 1

Tipuri de testare

Ce este un test? Potrivit Wikipedia: „Testarea de software implică executarea unei componente software sau a unei componente de sistem pentru a evalua una sau mai multe proprietăți de interes”. Cu alte cuvinte, este o verificare a corectitudinii sistemului nostru în anumite situații. Ei bine, să vedem ce tipuri de testare există în general:
  • Testare unitară — Teste al căror scop este verificarea fiecărui modul al sistemului separat. Aceste teste ar trebui să se aplice celor mai mici părți atomice ale sistemului, de exemplu module.
  • Testarea sistemului — Testare la nivel înalt pentru a verifica funcționarea unei părți mai mari a aplicației sau a sistemului în ansamblu.
  • Testare de regresie — Testare utilizată pentru a verifica dacă funcțiile noi sau remedierea erorilor afectează funcționalitatea existentă a aplicației sau introduce erori vechi.
  • Testare funcțională — Verificarea dacă o parte a aplicației îndeplinește cerințele menționate în specificații, poveștile utilizatorilor etc.

    Tipuri de testare funcțională:

    • Testare cutie albă — Verificarea dacă o parte a aplicației îndeplinește cerințele în timp ce se cunoaște implementarea internă a sistemului;
    • Testare cutie neagră — Verificarea dacă o parte a aplicației îndeplinește cerințele fără a cunoaște implementarea internă a sistemului.

  • Testare de performanță — Teste care sunt scrise pentru a determina modul în care sistemul sau o parte a sistemului funcționează la o anumită sarcină.
  • Testare de sarcină — Teste concepute pentru a verifica stabilitatea sistemului la sarcini standard și pentru a găsi sarcina maximă la care aplicația încă funcționează corect.
  • Testare la stres — Testare concepută pentru a verifica performanța aplicației la sarcini nestandard și pentru a determina sarcina maximă înainte de defectarea sistemului.
  • Testare de securitate — Teste utilizate pentru a verifica securitatea sistemului (de la hackeri, viruși, acces neautorizat la date confidențiale și alte atacuri încântătoare).
  • Testare de localizare — Teste de localizare a aplicației.
  • Testare de utilizare — Testare care vizează verificarea gradului de utilizare, înțelegere, atractivitate și învățare.
Toate acestea sună bine, dar cum funcționează în practică? Simplu! Folosim piramida de testare a lui Mike Cohn: Totul despre testarea unitară: tehnici, concepte, practică - 2Aceasta este o versiune simplificată a piramidei: acum este împărțită în părți și mai mici. Dar astăzi nu vom deveni prea sofisticați. Vom lua în considerare cea mai simplă versiune.
  1. Unit — Această secțiune se referă la testele unitare, care sunt aplicate în diferite straturi ale aplicației. Ei testează cea mai mică unitate divizibilă a logicii aplicației. De exemplu, clase, dar cel mai adesea metode. Aceste teste încearcă, de obicei, pe cât posibil să izoleze ceea ce este testat de orice logică externă. Adică, încearcă să creeze iluzia că restul aplicației rulează conform așteptărilor.

    Ar trebui să existe întotdeauna o mulțime de aceste teste (mai mult decât orice alt tip), deoarece testează bucăți mici și sunt foarte ușoare, fără a consuma multe resurse (adică RAM și timp).

  2. Integrare — Această secțiune se referă la testarea integrării. Această testare verifică părți mai mari ale sistemului. Adică, fie combină mai multe bucăți de logică (mai multe metode sau clase), fie verifică corectitudinea interacțiunii cu o componentă externă. Aceste teste sunt de obicei mai mici decât testele unitare, deoarece sunt mai grele.

    Un exemplu de test de integrare ar putea fi conectarea la o bază de date și verificarea corectitudinii funcționării metodelor de lucru cu aceasta.

  3. UI — Această secțiune se referă la teste care verifică funcționarea interfeței cu utilizatorul. Ele implică logica la toate nivelurile aplicației, motiv pentru care sunt numite și teste end-to-end. De regulă, sunt mult mai puține, deoarece sunt cele mai greoaie și trebuie să verifice căile cele mai necesare (folosite).

    În imaginea de mai sus, vedem că diferitele părți ale triunghiului variază în dimensiune: aproximativ aceleași proporții există în numărul de tipuri diferite de teste în munca reală.

    Astăzi vom arunca o privire mai atentă la cele mai comune teste, teste unitare, deoarece toți dezvoltatorii Java care se respectă ar trebui să le poată folosi la un nivel de bază.

Concepte cheie în testarea unitară

Acoperirea testului (acoperirea codului) este una dintre principalele măsuri ale cât de bine este testată o aplicație. Acesta este procentul din cod care este acoperit de teste (0-100%). În practică, mulți urmăresc acest procent ca obiectiv. Este ceva cu care nu sunt de acord, deoarece înseamnă că testele încep să fie aplicate acolo unde nu sunt necesare. De exemplu, să presupunem că avem operațiuni standard CRUD (creare/obține/actualizare/ștergere) în serviciul nostru fără logică suplimentară. Aceste metode sunt pur intermediari care deleg lucrul stratului care lucrează cu depozitul. În această situație, nu avem nimic de testat, cu excepția, poate, dacă metoda dată apelează la o metodă DAO, dar asta e o glumă. Instrumente suplimentare sunt de obicei folosite pentru a evalua acoperirea testului: JaCoCo, Cobertura, Clover, Emma etc. Pentru un studiu mai detaliat al acestui subiect, TDD înseamnă dezvoltare bazată pe teste. În această abordare, înainte de a face orice altceva, scrieți un test care va verifica codul specific. Aceasta se dovedește a fi testarea cutie neagră: știm că este intrarea și știm care ar trebui să fie rezultatul. Acest lucru face posibilă evitarea dublării codului. Dezvoltarea bazată pe teste începe cu proiectarea și dezvoltarea de teste pentru fiecare bit de funcționalitate din aplicația dvs. În abordarea TDD, creăm mai întâi un test care definește și testează comportamentul codului. Scopul principal al TDD este de a face codul mai ușor de înțeles, mai simplu și fără erori. Totul despre testarea unitară: tehnici, concepte, practică - 3Abordarea constă în următoarele:
  • Ne scriem testul.
  • Facem testul. Deloc surprinzător, eșuează, deoarece nu am implementat încă logica necesară.
  • Adăugați codul care face ca testul să treacă (rulăm din nou testul).
  • Refactorizăm codul.
TDD se bazează pe teste unitare, deoarece acestea sunt cele mai mici blocuri de construcție din piramida de automatizare a testelor. Cu teste unitare, putem testa logica de afaceri a oricărei clase. BDD înseamnă dezvoltare bazată pe comportament. Această abordare se bazează pe TDD. Mai precis, folosește exemple de limbaj simplu care explică comportamentul sistemului pentru toți cei implicați în dezvoltare. Nu vom aprofunda acest termen, deoarece afectează în principal testerii și analiștii de afaceri. Un caz de testare este un scenariu care descrie pașii, condițiile specifice și parametrii necesari pentru verificarea codului testat. Un dispozitiv de testare este un cod care setează mediul de testare pentru a avea starea necesară pentru ca metoda testată să ruleze cu succes. Este un set predefinit de obiecte și comportamentul lor în condiții specificate.

Etapele testării

Un test constă din trei etape:
  • Specificați datele de testare (instalații).
  • Exersați codul testat (apelați metoda testată).
  • Verificați rezultatele și comparați cu rezultatele așteptate.
Totul despre testarea unitară: tehnici, concepte, practică - 4Pentru a asigura modularitatea testului, trebuie să vă izolați de alte straturi ale aplicației. Acest lucru se poate face folosind cioturi, batjocuri și spioni. Mock-urile sunt obiecte care pot fi personalizate (de exemplu, personalizate pentru fiecare test). Ele ne permit să specificăm ce așteptăm de la apelurile de metodă, adică răspunsurile așteptate. Folosim obiecte simulate pentru a verifica dacă obținem ceea ce ne așteptăm. Stub-urile oferă un răspuns codificat la apeluri în timpul testării. De asemenea, pot stoca informații despre apel (de exemplu, parametrii sau numărul de apeluri). Aceștia sunt uneori denumiți ca spioni. Uneori, oamenii confundă termenii stub și mock: diferența este că un stub nu verifică nimic - doar simulează o anumită stare. Un batjocor este un obiect care are așteptări. De exemplu, că o anumită metodă trebuie apelată de un anumit număr de ori. Cu alte cuvinte,

Medii de testare

Deci, acum la obiect. Există mai multe medii de testare (cadre) disponibile pentru Java. Cele mai populare dintre acestea sunt JUnit și TestNG. Pentru revizuirea noastră aici, folosim: Totul despre testarea unitară: tehnici, concepte, practică - 5Un test JUnit este o metodă dintr-o clasă care este folosită numai pentru testare. Clasa este de obicei numită la fel ca și clasa pe care o testează, cu „Test” atașat la sfârșit. De exemplu, CarService -> CarServiceTest. Sistemul de compilare Maven include automat astfel de clase în domeniul testului. De fapt, această clasă se numește clasă de testare. Să trecem pe scurt peste adnotările de bază:

  • @Test indică faptul că metoda este un test (în principiu, o metodă marcată cu această adnotare este un test unitar).
  • @Before semnifică o metodă care va fi executată înainte de fiecare test. De exemplu, pentru a popula o clasă cu date de testare, pentru a citi datele de intrare etc.
  • @After este folosit pentru a marca o metodă care va fi apelată după fiecare test (de exemplu, pentru a șterge datele sau a restabili valorile implicite).
  • @BeforeClass este plasat deasupra unei metode, analog cu @Before. Dar o astfel de metodă este apelată o singură dată înainte de toate testele pentru clasa dată și, prin urmare, trebuie să fie statică. Este folosit pentru a efectua operațiuni care necesită mai multe resurse, cum ar fi derularea unei baze de date de testare.
  • @AfterClass este opusul @BeforeClass: este executat o dată pentru clasa dată, dar numai după toate testele. Este folosit, de exemplu, pentru a șterge resurse persistente sau a deconecta de la o bază de date.
  • @Ignore indică faptul că o metodă este dezactivată și va fi ignorată în timpul testului general. Acesta este utilizat în diverse situații, de exemplu, dacă metoda de bază a fost schimbată și testul nu a fost încă reluat pentru a se adapta la modificări. În astfel de cazuri, este, de asemenea, de dorit să adăugați o descriere, adică @Ignore(„Unele descriere”).
  • @Test(expected = Exception.class) este folosit pentru testele negative. Acestea sunt teste care verifică modul în care se comportă metoda în cazul unei erori, adică testul se așteaptă ca metoda să arunce un fel de excepție. O astfel de metodă este indicată de adnotarea @Test, dar cu o indicație a erorii care trebuie detectată.
  • @Test(timeout = 100) verifică dacă metoda este executată în cel mult 100 de milisecunde.
  • @Mock este folosit deasupra unui câmp pentru a atribui un obiect simulat (aceasta nu este o adnotare JUnit, ci vine de la Mockito). După cum este necesar, setăm comportamentul simulacului pentru o situație specifică direct în metoda de testare.
  • @RunWith(MockitoJUnitRunner.class) este plasat deasupra unei clase. Această adnotare îi spune lui JUnit să invoce testele din clasă. Există diferiți alergători, inclusiv aceștia: MockitoJUnitRunner, JUnitPlatform și SpringRunner. În JUnit 5, adnotarea @RunWith a fost înlocuită cu adnotarea mai puternică @ExtendWith.
Să aruncăm o privire la câteva metode folosite pentru a compara rezultatele:

  • assertEquals(Object expects, Object actuals) — verifică dacă obiectele transmise sunt egale.
  • assertTrue(steagul boolean) — verifică dacă valoarea transmisă este adevărată.
  • assertFalse(steagul boolean) — verifică dacă valoarea transmisă este falsă.
  • assertNull(Object object) — verifică dacă obiectul transmis este nul.
  • assertSame(Object firstObject, Object secondObject) — verifică dacă valorile transmise se referă la același obiect.
  • afirmă că (T t, Matcher potrivire) — Verifică dacă t îndeplinește condiția specificată în potrivire.
AssertJ oferă, de asemenea, o metodă de comparare utilă: assertThat(firstObject).isEqualTo(secondObject) . Aici am menționat metodele de bază - celelalte sunt variații ale celor de mai sus.

Testarea în practică

Acum să ne uităm la materialul de mai sus într-un exemplu specific. Vom testa metoda de actualizare a unui serviciu. Nu vom lua în considerare stratul DAO, deoarece folosim cel implicit. Să adăugăm un starter pentru teste:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.2.2.RELEASE</version>
   <scope>test</scope>
</dependency>
Și aici avem clasa de servicii:

@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
   private final RobotDAO robotDAO;

   @Override
   public Robot update(Long id, Robot robot) {
       Robot found = robotDAO.findById(id);
       return robotDAO.update(Robot.builder()
               .id(id)
               .name(robot.getName() != null ? robot.getName() : found.getName())
               .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
               .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
               .build());
   }
}
Linia 8 — trageți obiectul actualizat din baza de date. Liniile 9-14 — creați un obiect prin constructor. Dacă obiectul primit are un câmp, setați-l. Dacă nu, vom lăsa ceea ce este în baza de date. Acum uită-te la testul nostru:

@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
   @Mock
   private RobotDAO robotDAO;

   private RobotServiceImpl robotService;

   private static Robot testRobot;

   @BeforeClass
   public static void prepareTestData() {
       testRobot = Robot
               .builder()
               .id(123L)
               .name("testRobotMolly")
               .cpu("Intel Core i7-9700K")
               .producer("China")
               .build();
   }

   @Before
   public void init() {
       robotService = new RobotServiceImpl(robotDAO);
   }
Linia 1 - alergătorul nostru. Linia 4 - izolăm serviciul de stratul DAO prin înlocuirea unui simulacro. Linia 11 — setăm o entitate de testare (cea pe care o vom folosi ca cobai) pentru clasă. Linia 22 — setăm obiectul de serviciu, care este ceea ce vom testa.

@Test
public void updateTest() {
   when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
   when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
   Robot robotForUpdate = Robot
           .builder()
           .name("Vally")
           .cpu("AMD Ryzen 7 2700X")
           .build();

   Robot resultRobot = robotService.update(123L, robotForUpdate);

   assertNotNull(resultRobot);
   assertSame(resultRobot.getId(),testRobot.getId());
   assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
   assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
   assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
Aici vedem că testul are trei diviziuni clare: Liniile 3-9 — specificarea dispozitivelor de fixare. Linia 11 — executarea codului testat. Rândurile 13-17 — verificarea rezultatelor. Mai detaliat: liniile 3-4 — setați comportamentul pentru simularea DAO. Linia 5 — setați instanța pe care o vom actualiza peste standardul nostru. Linia 11 - utilizați metoda și luați instanța rezultată. Linia 13 — verificați dacă nu este nulă. Linia 14 — comparați ID-ul rezultatului și argumentele metodei date. Linia 15 — verificați dacă numele a fost actualizat. Linia 16 - vezi rezultatul CPU. Linia 17 — nu am specificat acest câmp în instanță, așa că ar trebui să rămână același. Verificăm această stare aici. Hai să-l rulăm:Totul despre testarea unitară: tehnici, concepte, practică - 6Testul este verde! Putem răsufla ușurați :) Pe scurt, testarea îmbunătățește calitatea codului și face procesul de dezvoltare mai flexibil și mai fiabil. Imaginează-ți cât de mult efort este nevoie pentru a reproiecta software-ul care implică sute de fișiere de clasă. Când avem teste unitare scrise pentru toate aceste clase, putem refactoriza cu încredere. Și cel mai important, ne ajută să găsim cu ușurință erori în timpul dezvoltării. Băieți și fete, asta e tot ce am astăzi. Da-mi un like si lasa un comentariu :)
Comentarii
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION