CodeGym /Java-blogg /Tilfeldig /Alt om enhetstesting: teknikker, konsepter, praksis
John Squirrels
Nivå
San Francisco

Alt om enhetstesting: teknikker, konsepter, praksis

Publisert i gruppen
I dag vil du ikke finne en applikasjon som ikke er dekket med tester, så dette emnet vil være mer relevant enn noen gang for nybegynnere: du kan ikke lykkes uten tester. La oss vurdere hvilke typer testing som brukes i prinsippet, og deretter vil vi studere i detalj alt som er å vite om enhetstesting. Alt om enhetstesting: teknikker, konsepter, praksis - 1

Typer testing

Hva er en test? I følge Wikipedia: "Programvaretesting involverer utførelse av en programvarekomponent eller systemkomponent for å evaluere en eller flere egenskaper av interesse." Det er med andre ord en kontroll av riktigheten av systemet vårt i visse situasjoner. Vel, la oss se hvilke typer testing det er generelt:
  • Enhetstesting — Tester hvis formål er å kontrollere hver modul i systemet separat. Disse testene bør gjelde for de minste atomdelene av systemet, f.eks. moduler.
  • Systemtesting — Testing på høyt nivå for å sjekke driften av en større del av applikasjonen eller systemet som helhet.
  • Regresjonstesting — Testing som brukes til å sjekke om nye funksjoner eller feilrettinger påvirker applikasjonens eksisterende funksjonalitet eller introduserer gamle feil.
  • Funksjonstesting — Sjekke om en del av applikasjonen tilfredsstiller kravene angitt i spesifikasjonene, brukerhistorier osv.

    Typer funksjonstesting:

    • White-box-testing — Sjekke om en del av applikasjonen tilfredsstiller kravene mens du kjenner systemets interne implementering;
    • Black-box-testing — Sjekke om en del av applikasjonen tilfredsstiller kravene uten å kjenne til systemets interne implementering.

  • Ytelsestesting — Tester som er skrevet for å bestemme hvordan systemet eller en del av systemet yter under en viss belastning.
  • Lasttesting — Tester designet for å sjekke systemets stabilitet under standardbelastninger og for å finne den maksimale belastningen der applikasjonen fortsatt fungerer som den skal.
  • Stresstesting — Testing designet for å sjekke applikasjonens ytelse under ikke-standard belastning og for å bestemme maksimal belastning før systemfeil.
  • Sikkerhetstesting – Tester som brukes til å sjekke systemets sikkerhet (fra hackere, virus, uautorisert tilgang til konfidensielle data og andre herlige angrep).
  • Lokaliseringstesting — Tester av lokaliseringen av applikasjonen.
  • Brukervennlighetstesting – Testing rettet mot å sjekke brukervennlighet, forståelighet, attraktivitet og lærebarhet.
Alt dette høres bra ut, men hvordan fungerer det i praksis? Enkel! Vi bruker Mike Cohns testpyramide: Alt om enhetstesting: teknikker, konsepter, praksis - 2Dette er en forenklet versjon av pyramiden: den er nå delt inn i enda mindre deler. Men i dag blir vi ikke for sofistikerte. Vi vil vurdere den enkleste versjonen.
  1. Enhet — Denne delen refererer til enhetstester, som brukes i forskjellige lag av applikasjonen. De tester den minste delbare enheten for applikasjonslogikk. For eksempel klasser, men oftest metoder. Disse testene prøver vanligvis så mye som mulig å isolere det som testes fra enhver ekstern logikk. Det vil si at de prøver å skape en illusjon om at resten av applikasjonen kjører som forventet.

    Det bør alltid være mange av disse testene (flere enn noen annen type), siden de tester små biter og er veldig lette, og bruker ikke mye ressurser (som betyr RAM og tid).

  2. Integrasjon — Denne delen refererer til integrasjonstesting. Denne testingen sjekker større deler av systemet. Det vil si at den enten kombinerer flere deler av logikk (flere metoder eller klasser), eller den kontrollerer korrektheten av interaksjon med en ekstern komponent. Disse testene er vanligvis mindre enn enhetstester fordi de er tyngre.

    Et eksempel på en integrasjonstest kan være å koble til en database og kontrollere riktigheten av driften av metoder for å jobbe med den.

  3. UI — Denne delen refererer til tester som kontrollerer funksjonen til brukergrensesnittet. De involverer logikken på alle nivåer av applikasjonen, og det er derfor de også kalles ende-til-ende-tester. Som regel er det langt færre av dem, fordi de er de mest tungvinte og må sjekke de mest nødvendige (brukte) stiene.

    På bildet ovenfor ser vi at de forskjellige delene av trekanten varierer i størrelse: omtrent like proporsjoner eksisterer i antall forskjellige typer tester i virkelig arbeid.

    I dag skal vi se nærmere på de vanligste testene, enhetstester, siden alle Java-utviklere med respekt for seg selv skal kunne bruke dem på et grunnleggende nivå.

Sentrale begreper i enhetstesting

Testdekning (kodedekning) er et av hovedmålene for hvor godt en applikasjon er testet. Dette er prosentandelen av koden som dekkes av testene (0-100%). I praksis er det mange som forfølger denne prosentandelen som mål. Det er noe jeg er uenig i, siden det betyr at tester begynner å bli brukt der de ikke er nødvendige. Anta for eksempel at vi har standard CRUD-operasjoner (opprett/hent/oppdater/slett) i tjenesten vår uten ekstra logikk. Disse metodene er rene mellomledd som delegerer arbeid til laget som arbeider med depotet. I denne situasjonen har vi ingenting å teste, bortsett fra kanskje om den gitte metoden kaller en DAO-metode, men det er en spøk. Ytterligere verktøy brukes vanligvis for å vurdere testdekning: JaCoCo, Cobertura, Clover, Emma, ​​etc. For en mer detaljert studie av dette emnet, TDD står for testdrevet utvikling. I denne tilnærmingen, før du gjør noe annet, skriver du en test som vil sjekke spesifikk kode. Dette viser seg å være black-box-testing: vi vet at inngangen er og vi vet hva utgangen skal være. Dette gjør det mulig å unngå kodeduplisering. Testdrevet utvikling starter med å designe og utvikle tester for hver funksjonalitet i applikasjonen din. I TDD-tilnærmingen lager vi først en test som definerer og tester kodens oppførsel. Hovedmålet med TDD er å gjøre koden din mer forståelig, enklere og feilfri. Alt om enhetstesting: teknikker, konsepter, praksis - 3Tilnærmingen består av følgende:
  • Vi skriver testen vår.
  • Vi kjører testen. Ikke overraskende mislykkes det, siden vi ennå ikke har implementert den nødvendige logikken.
  • Legg til koden som får testen til å bestå (vi kjører testen på nytt).
  • Vi refaktoriserer koden.
TDD er basert på enhetstester, siden de er de minste byggesteinene i testautomatiseringspyramiden. Med enhetstester kan vi teste forretningslogikken til enhver klasse. BDD står for atferdsdrevet utvikling. Denne tilnærmingen er basert på TDD. Mer spesifikt bruker den klare språkeksempler som forklarer systematferd for alle som er involvert i utvikling. Vi skal ikke fordype oss i dette begrepet, siden det hovedsakelig rammer testere og forretningsanalytikere. Et testtilfelle er et scenario som beskriver trinnene, spesifikke forholdene og parameterne som kreves for å sjekke koden som testes. En testarmatur er kode som setter opp testmiljøet til å ha den tilstanden som er nødvendig for at metoden som testes skal kjøre vellykket. Det er et forhåndsdefinert sett med objekter og deres oppførsel under spesifiserte forhold.

Stadier av testing

En test består av tre stadier:
  • Spesifiser testdata (inventar).
  • Tren koden under test (kall den testede metoden).
  • Verifiser resultatene og sammenlign med de forventede resultatene.
Alt om enhetstesting: teknikker, konsepter, praksis - 4For å sikre testmodularitet, må du isolere fra andre lag i applikasjonen. Dette kan gjøres ved hjelp av stubber, spotter og spioner. Spot er objekter som kan tilpasses (for eksempel skreddersydd for hver test). De lar oss spesifisere hva vi forventer av metodekall, dvs. forventede svar. Vi bruker falske objekter for å bekrefte at vi får det vi forventer. Stubber gir et hardkodet svar på anrop under testing. De kan også lagre informasjon om samtalen (for eksempel parametere eller antall samtaler). Disse blir noen ganger referert til som spioner. Noen ganger forveksler folk begrepene stubbe og hån: forskjellen er at en stubbe ikke sjekker noe - den simulerer bare en gitt tilstand. En hån er en gjenstand som har forventninger. For eksempel at en gitt metode må kalles et visst antall ganger. Med andre ord,

Testmiljøer

Så, nå til poenget. Det er flere testmiljøer (rammeverk) tilgjengelig for Java. De mest populære av disse er JUnit og TestNG. For vår anmeldelse her bruker vi: Alt om enhetstesting: teknikker, konsepter, praksis - 5En JUnit-test er en metode i en klasse som kun brukes til testing. Klassen heter vanligvis det samme som klassen den tester, med "Test" vedlagt på slutten. For eksempel, CarService -> CarServiceTest. Maven-byggesystemet inkluderer automatisk slike klasser i testomfanget. Faktisk kalles denne klassen en testklasse. La oss kort gå gjennom de grunnleggende kommentarene:

  • @Test indikerer at metoden er en test (i utgangspunktet er en metode merket med denne merknaden en enhetstest).
  • @Before betyr en metode som vil bli utført før hver test. For eksempel for å fylle en klasse med testdata, lese inndata, etc.
  • @After brukes til å markere en metode som vil bli kalt etter hver test (f.eks. for å slette data eller gjenopprette standardverdier).
  • @BeforeClass er plassert over en metode, analogt med @Before. Men en slik metode kalles bare én gang før alle tester for den gitte klassen og må derfor være statisk. Den brukes til å utføre mer ressurskrevende operasjoner, for eksempel å spinne opp en testdatabase.
  • @AfterClass er det motsatte av @BeforeClass: det utføres én gang for den gitte klassen, men bare etter alle tester. Den brukes for eksempel til å fjerne vedvarende ressurser eller koble fra en database.
  • @Ignorer angir at en metode er deaktivert og vil bli ignorert under den totale testkjøringen. Dette brukes i ulike situasjoner, for eksempel hvis basismetoden er endret og testen ennå ikke er omarbeidet for å imøtekomme endringene. I slike tilfeller er det også ønskelig å legge til en beskrivelse, dvs. @Ignore("Noen beskrivelse").
  • @Test(expected = Exception.class) brukes for negative tester. Dette er tester som bekrefter hvordan metoden oppfører seg i tilfelle feil, det vil si at testen forventer at metoden kaster et slags unntak. En slik metode indikeres av @Test-kommentaren, men med en indikasjon på hvilken feil som skal fanges opp.
  • @Test(timeout = 100) sjekker at metoden utføres på ikke mer enn 100 millisekunder.
  • @Mock brukes over et felt for å tilordne et falskt objekt (dette er ikke JUnit-kommentar, men kommer i stedet fra Mockito). Etter behov setter vi spottens oppførsel for en spesifikk situasjon direkte i testmetoden.
  • @RunWith(MockitoJUnitRunner.class) er plassert over en klasse. Denne merknaden ber JUnit om å starte testene i klassen. Det er forskjellige løpere, inkludert disse: MockitoJUnitRunner, JUnitPlatform og SpringRunner. I JUnit 5 er @RunWith-kommentaren erstattet med den kraftigere @ExtendWith-kommentaren.
La oss ta en titt på noen metoder som brukes for å sammenligne resultater:

  • assertEquals(Object expects, Object actuals) — sjekker om de beståtte objektene er like.
  • assertTrue(boolesk flagg) – sjekker om den beståtte verdien er sann.
  • assertFalse(boolesk flagg) – sjekker om den beståtte verdien er falsk.
  • assertNull(Objektobjekt) – sjekker om det beståtte objektet er null.
  • assertSame(Object firstObject, Object secondObject) — sjekker om de beståtte verdiene refererer til det samme objektet.
  • assertThat(T t, Matcher matcher) — Sjekker om t tilfredsstiller betingelsen spesifisert i matcher.
AssertJ gir også en nyttig sammenligningsmetode: assertThat(firstObject).isEqualTo(secondObject) . Her har jeg nevnt de grunnleggende metodene - de andre er varianter av ovennevnte.

Testing i praksis

La oss nå se på materialet ovenfor i et spesifikt eksempel. Vi vil teste en tjenestes oppdateringsmetode. Vi vil ikke vurdere DAO-laget, siden vi bruker standarden. La oss legge til en startpakke for 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 — trekk det oppdaterte objektet fra databasen. Linje 9-14 — lag et objekt gjennom byggherren. Hvis det innkommende objektet har et felt, sett det. Hvis ikke, lar vi det som er i databasen. Se nå på testen vår:

@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 - vår løper. Linje 4 - vi isolerer tjenesten fra DAO-laget ved å erstatte en mock. Linje 11 — vi setter en testenhet (den vi skal bruke som marsvin) for klassen. Linje 22 — vi setter tjenesteobjektet, som er det vi skal 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 inndelinger: Linje 3-9 — spesifisere inventar. Linje 11 — utfører koden som testes. Linje 13-17 — sjekker resultatene. Mer detaljert: Linje 3-4 — angi atferden for DAO-mock. Linje 5 - angi forekomsten som vi skal oppdatere på toppen av standarden vår. Linje 11 - bruk metoden og ta den resulterende forekomsten. Linje 13 – sjekk at den ikke er null. Linje 14 — sammenlign ID-en til resultatet og de gitte metodeargumentene. Linje 15 — sjekk om navnet ble oppdatert. Linje 16 - se CPU-resultatet. Linje 17 — vi spesifiserte ikke dette feltet i forekomsten, så det bør forbli det samme. Vi sjekker den tilstanden her. La oss kjøre det:Alt om enhetstesting: teknikker, konsepter, praksis - 6Testen er grønn! Vi kan puste lettet ut :) Oppsummert forbedrer testing kvaliteten på koden og gjør utviklingsprosessen mer fleksibel og pålitelig. Tenk deg hvor mye innsats det tar å redesigne programvare som involverer hundrevis av klassefiler. Når vi har skrevet enhetstester for alle disse klassene, kan vi refaktorisere med selvtillit. Og viktigst av alt, det hjelper oss enkelt å finne feil under utvikling. Gutter og jenter, det er alt jeg har i dag. Gi meg et like og legg igjen en kommentar :)
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION