CodeGym /Java blog /Tilfældig /Alt om enhedstestning: teknikker, koncepter, praksis
John Squirrels
Niveau
San Francisco

Alt om enhedstestning: teknikker, koncepter, praksis

Udgivet i gruppen
I dag finder du ikke en applikation, der ikke er dækket af test, så dette emne vil være mere relevant end nogensinde for nybegyndere: du kan ikke lykkes uden test. Lad os overveje, hvilke typer test der bruges i princippet, og så vil vi i detaljer studere alt, hvad der er at vide om enhedstest. Alt om enhedstestning: teknikker, koncepter, praksis - 1

Typer af test

Hvad er en test? Ifølge Wikipedia: "Softwaretest involverer udførelse af en softwarekomponent eller systemkomponent for at evaluere en eller flere egenskaber af interesse." Det er med andre ord en kontrol af rigtigheden af ​​vores system i visse situationer. Nå, lad os se, hvilke typer test der er generelt:
  • Unit testing — Tests, hvis formål er at kontrollere hvert modul i systemet separat. Disse test bør gælde for de mindste atomare dele af systemet, f.eks. moduler.
  • Systemtest — Test på højt niveau for at kontrollere driften af ​​en større del af applikationen eller systemet som helhed.
  • Regressionstest — Test, der bruges til at kontrollere, om nye funktioner eller fejlrettelser påvirker applikationens eksisterende funktionalitet eller introducerer gamle fejl.
  • Funktionstest — Kontrol af, om en del af applikationen opfylder kravene i specifikationerne, brugerhistorier osv.

    Typer af funktionstest:

    • White-box-test — Kontrol af, om en del af applikationen opfylder kravene, samtidig med at du kender systemets interne implementering;
    • Black-box-test — Kontrol af, om en del af applikationen opfylder kravene uden at kende systemets interne implementering.

  • Ydelsestest — Tests, der er skrevet for at bestemme, hvordan systemet eller en del af systemet fungerer under en bestemt belastning.
  • Belastningstest — Tests designet til at kontrollere systemets stabilitet under standardbelastninger og finde den maksimale belastning, hvor applikationen stadig fungerer korrekt.
  • Stresstest — Test designet til at kontrollere applikationens ydeevne under ikke-standardbelastninger og til at bestemme den maksimale belastning før systemfejl.
  • Sikkerhedstest — Tests, der bruges til at kontrollere systemets sikkerhed (fra hackere, vira, uautoriseret adgang til fortrolige data og andre dejlige angreb).
  • Lokaliseringstest — Test af lokalisering af applikationen.
  • Usability test - Test, der har til formål at kontrollere brugervenlighed, forståelighed, tiltrækningskraft og indlæringsevne.
Det lyder alt sammen godt, men hvordan fungerer det i praksis? Enkel! Vi bruger Mike Cohns testpyramide: Alt om enhedstestning: teknikker, koncepter, praksis - 2Dette er en forenklet version af pyramiden: den er nu opdelt i endnu mindre dele. Men i dag bliver vi ikke for sofistikerede. Vi vil overveje den enkleste version.
  1. Enhed — Dette afsnit henviser til enhedstest, som anvendes i forskellige lag af applikationen. De tester den mindste delbare enhed af applikationslogik. For eksempel klasser, men oftest metoder. Disse tests forsøger normalt så meget som muligt at isolere det testede fra enhver ekstern logik. Det vil sige, at de forsøger at skabe den illusion, at resten af ​​applikationen kører som forventet.

    Der bør altid være mange af disse tests (flere end nogen anden type), da de tester små stykker og er meget lette, og de bruger ikke mange ressourcer (hvilket betyder RAM og tid).

  2. Integration — Dette afsnit henviser til integrationstest. Denne test kontrollerer større dele af systemet. Det vil sige, at det enten kombinerer flere stykker logik (flere metoder eller klasser), eller det kontrollerer korrektheden af ​​interaktion med en ekstern komponent. Disse tests er normalt mindre end enhedstests, fordi de er tungere.

    Et eksempel på en integrationstest kunne være at oprette forbindelse til en database og kontrollere rigtigheden af ​​driften af ​​metoder til at arbejde med den.

  3. UI — Dette afsnit henviser til test, der kontrollerer betjeningen af ​​brugergrænsefladen. De involverer logikken på alle niveauer af applikationen, hvorfor de også kaldes end-to-end tests. Som regel er der langt færre af dem, fordi de er de mest besværlige og skal tjekke de mest nødvendige (brugte) stier.

    På billedet ovenfor ser vi, at de forskellige dele af trekanten varierer i størrelse: Der findes omtrent de samme proportioner i antallet af forskellige slags test i virkeligt arbejde.

    I dag skal vi se nærmere på de mest almindelige tests, enhedstests, da alle Java-udviklere med respekt for sig selv burde kunne bruge dem på et grundlæggende niveau.

Nøglebegreber i enhedstestning

Testdækning (kodedækning) er et af hovedmålene for, hvor godt en applikation er testet. Dette er den procentdel af koden, der er omfattet af testene (0-100%). I praksis forfølger mange denne procentdel som deres mål. Det er jeg uenig i, da det betyder, at test begynder at blive anvendt, hvor de ikke er nødvendige. Antag for eksempel, at vi har standard CRUD-operationer (create/get/update/delete) i vores tjeneste uden yderligere logik. Disse metoder er rene mellemled, der uddelegerer arbejde til laget, der arbejder med depotet. I denne situation har vi ikke noget at teste, undtagen måske om den givne metode kalder en DAO-metode, men det er en joke. Yderligere værktøjer bruges normalt til at vurdere testdækning: JaCoCo, Cobertura, Clover, Emma osv. For en mere detaljeret undersøgelse af dette emne, TDD står for testdrevet udvikling. I denne tilgang, før du gør noget andet, skriver du en test, der vil kontrollere specifik kode. Dette viser sig at være black-box-test: vi ved, at inputtet er, og vi ved, hvad outputtet skal være. Dette gør det muligt at undgå kodeduplikering. Testdrevet udvikling starter med at designe og udvikle tests for hver funktionalitet i din applikation. I TDD-tilgangen laver vi først en test, der definerer og tester kodens adfærd. Hovedmålet med TDD er at gøre din kode mere forståelig, enklere og fejlfri. Alt om enhedstestning: teknikker, koncepter, praksis - 3Tilgangen består af følgende:
  • Vi skriver vores test.
  • Vi kører testen. Ikke overraskende mislykkes det, da vi endnu ikke har implementeret den nødvendige logik.
  • Tilføj koden, der får testen til at bestå (vi kører testen igen).
  • Vi refaktoriserer koden.
TDD er baseret på enhedstests, da de er de mindste byggesten i testautomatiseringspyramiden. Med enhedstests kan vi teste forretningslogikken for enhver klasse. BDD står for adfærdsdrevet udvikling. Denne tilgang er baseret på TDD. Mere specifikt bruger den almindelige eksempler, der forklarer systemadfærd for alle, der er involveret i udvikling. Vi vil ikke dykke ned i dette udtryk, da det primært påvirker testere og forretningsanalytikere. En testcase er et scenarie, der beskriver de trin, specifikke forhold og parametre, der kræves for at kontrollere koden under test. Et testarmatur er kode, der sætter testmiljøet op til at have den tilstand, der er nødvendig for, at metoden, der testes, kan køre med succes. Det er et foruddefineret sæt af objekter og deres adfærd under specificerede forhold.

Stadier af test

En test består af tre faser:
  • Angiv testdata (inventar).
  • Træn koden under test (kald den testede metode).
  • Bekræft resultaterne og sammenlign med de forventede resultater.
Alt om enhedstestning: teknikker, koncepter, praksis - 4For at sikre testmodularitet skal du isolere fra andre lag af applikationen. Dette kan gøres ved hjælp af stubbe, håner og spioner. Spot er objekter, der kan tilpasses (for eksempel skræddersyet til hver test). De lader os specificere, hvad vi forventer af metodekald, altså de forventede svar. Vi bruger falske objekter til at bekræfte, at vi får, hvad vi forventer. Stubs giver et hårdkodet svar på opkald under test. De kan også gemme oplysninger om opkaldet (f.eks. parametre eller antallet af opkald). Disse omtales nogle gange som spioner. Nogle gange forveksler folk udtrykkene stub og hån: Forskellen er, at en stub ikke tjekker noget - den simulerer kun en given tilstand. En hån er en genstand, der har forventninger. For eksempel at en given metode skal kaldes et vist antal gange. Med andre ord,

Test miljøer

Så nu til sagen. Der er flere testmiljøer (frameworks) tilgængelige for Java. De mest populære af disse er JUnit og TestNG. Til vores anmeldelse her bruger vi: Alt om enhedstestning: teknikker, koncepter, praksis - 5En JUnit test er en metode i en klasse, der kun bruges til test. Klassen hedder normalt det samme som den klasse, den tester, med "Test" tilføjet til slutningen. For eksempel CarService -> CarServiceTest. Maven-byggesystemet inkluderer automatisk sådanne klasser i testomfanget. Faktisk kaldes denne klasse en testklasse. Lad os kort gennemgå de grundlæggende annoteringer:

  • @Test angiver, at metoden er en test (dybest set er en metode markeret med denne annotation en enhedstest).
  • @Before angiver en metode, der vil blive udført før hver test. For eksempel at udfylde en klasse med testdata, læse inputdata osv.
  • @After bruges til at markere en metode, der vil blive kaldt efter hver test (f.eks. for at slette data eller gendanne standardværdier).
  • @BeforeClass er placeret over en metode, analog med @Before. Men sådan en metode kaldes kun én gang før alle tests for den givne klasse og skal derfor være statisk. Det bruges til at udføre mere ressourcekrævende operationer, såsom at oprette en testdatabase.
  • @AfterClass er det modsatte af @BeforeClass: det udføres én gang for den givne klasse, men kun efter alle tests. Det bruges for eksempel til at rydde vedvarende ressourcer eller afbryde forbindelsen til en database.
  • @Ignorer angiver, at en metode er deaktiveret og vil blive ignoreret under den samlede testkørsel. Dette bruges i forskellige situationer, for eksempel hvis basismetoden er blevet ændret, og testen endnu ikke er blevet omarbejdet for at imødekomme ændringerne. I sådanne tilfælde er det også ønskeligt at tilføje en beskrivelse, dvs. @Ignore("Nogle beskrivelse").
  • @Test(expected = Exception.class) bruges til negative tests. Det er test, der verificerer, hvordan metoden opfører sig i tilfælde af en fejl, det vil sige, at testen forventer, at metoden kaster en form for undtagelse. En sådan metode er angivet med @Test-annotationen, men med en indikation af, hvilken fejl der skal fanges.
  • @Test(timeout = 100) kontrollerer, at metoden udføres på ikke mere end 100 millisekunder.
  • @Mock bruges over et felt til at tildele et mock-objekt (dette er ikke JUnit-annotering, men kommer i stedet fra Mockito). Efter behov sætter vi spottens adfærd til en specifik situation direkte i testmetoden.
  • @RunWith(MockitoJUnitRunner.class) er placeret over en klasse. Denne annotation fortæller JUnit at påberåbe sig testene i klassen. Der er forskellige løbere, herunder disse: MockitoJUnitRunner, JUnitPlatform og SpringRunner. I JUnit 5 er @RunWith-annotationen blevet erstattet med den mere kraftfulde @ExtendWith-annotering.
Lad os tage et kig på nogle metoder, der bruges til at sammenligne resultater:

  • assertEquals(Object expects, Object actuals) — kontrollerer, om de beståede objekter er ens.
  • assertTrue(boolesk flag) — kontrollerer, om den beståede værdi er sand.
  • assertFalse(boolesk flag) — kontrollerer, om den beståede værdi er falsk.
  • assertNull(Objektobjekt) — kontrollerer, om det beståede objekt er null.
  • assertSame(Object firstObject, Object secondObject) — kontrollerer, om de beståede værdier refererer til det samme objekt.
  • assertThat(T t, Matcher matcher) — Kontrollerer, om t opfylder betingelsen specificeret i matcher.
AssertJ giver også en nyttig sammenligningsmetode: assertThat(firstObject).isEqualTo(secondObject) . Her har jeg nævnt de grundlæggende metoder - de andre er variationer af ovenstående.

Test i praksis

Lad os nu se på ovenstående materiale i et specifikt eksempel. Vi tester en tjenestes opdateringsmetode. Vi vil ikke overveje DAO-laget, da vi bruger standarden. Lad os tilføje en starter til testene:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.2.2.RELEASE</version>
   <scope>test</scope>
</dependency>
Og her har vi serviceklassen:

@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());
   }
}
Linje 8 — træk det opdaterede objekt fra databasen. Linje 9-14 — opret et objekt gennem builderen. Hvis det indkommende objekt har et felt, skal du indstille det. Hvis ikke, efterlader vi det, der er i databasen. Se nu vores test:

@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);
   }
Linje 1 - vores Runner. Linje 4 — vi isolerer tjenesten fra DAO-laget ved at erstatte en mock. Linje 11 — vi indstiller en test-entitet (den, som vi vil bruge som et marsvin) for klassen. Linje 22 — vi indstiller serviceobjektet, hvilket er det, vi vil teste.

@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());
}
Her ser vi, at testen har tre klare opdelinger: Linje 3-9 — specificering af inventar. Linje 11 — udførelse af koden under test. Linje 13-17 — kontrol af resultaterne. Mere detaljeret: Linje 3-4 — indstil adfærden for DAO-mock. Linje 5 — indstil den instans, som vi vil opdatere oven på vores standard. Linje 11 — brug metoden og tag den resulterende instans. Linje 13 — tjek, at den ikke er nul. Linje 14 — sammenlign id'et for resultatet og de givne metodeargumenter. Linje 15 — tjek om navnet blev opdateret. Linje 16 — se CPU-resultatet. Linje 17 — vi specificerede ikke dette felt i forekomsten, så det skulle forblive det samme. Vi tjekker den tilstand her. Lad os køre det:Alt om enhedstestning: teknikker, koncepter, praksis - 6Testen er grøn! Vi kan ånde lettet op :) Sammenfattende forbedrer test kvaliteten af ​​koden og gør udviklingsprocessen mere fleksibel og pålidelig. Forestil dig, hvor meget indsats det kræver at redesigne software, der involverer hundredvis af klassefiler. Når vi har skrevet enhedstests for alle disse klasser, kan vi refaktorisere med tillid. Og vigtigst af alt, det hjælper os med nemt at finde fejl under udvikling. Drenge og piger, det er alt, hvad jeg har i dag. Giv mig et like og smid en kommentar :)
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION