CodeGym /Java blogg /Slumpmässig /Allt om enhetstestning: tekniker, koncept, praktik
John Squirrels
Nivå
San Francisco

Allt om enhetstestning: tekniker, koncept, praktik

Publicerad i gruppen
Idag kommer du inte att hitta en applikation som inte är täckt med tester, så det här ämnet kommer att vara mer relevant än någonsin för nybörjare: du kan inte lyckas utan tester. Låt oss överväga vilka typer av testning som används i princip, och sedan kommer vi att studera i detalj allt som finns att veta om enhetstestning. Allt om enhetstestning: tekniker, koncept, praktik - 1

Typer av testning

Vad är ett test? Enligt Wikipedia: "Programvarutestning involverar exekvering av en mjukvarukomponent eller systemkomponent för att utvärdera en eller flera egenskaper av intresse." Det är med andra ord en kontroll av att vårt system är korrekt i vissa situationer. Tja, låt oss se vilka typer av tester det finns i allmänhet:
  • Enhetstestning — Tester vars syfte är att kontrollera varje modul i systemet separat. Dessa tester bör gälla de minsta atomära delarna av systemet, t.ex. moduler.
  • Systemtestning — Testning på hög nivå för att kontrollera funktionen hos en större del av applikationen eller systemet som helhet.
  • Regressionstestning — Testning som används för att kontrollera om nya funktioner eller buggfixar påverkar applikationens befintliga funktionalitet eller introducerar gamla buggar.
  • Funktionstestning — Kontrollera om en del av applikationen uppfyller kraven som anges i specifikationer, användarberättelser, etc.

    Typer av funktionstestning:

    • White-box-testning — Kontrollera om en del av applikationen uppfyller kraven samtidigt som du känner till systemets interna implementering;
    • Black-box-testning — Kontrollera om en del av applikationen uppfyller kraven utan att känna till systemets interna implementering.

  • Prestandatestning — Tester som är skrivna för att fastställa hur systemet eller en del av systemet presterar under en viss belastning.
  • Belastningstestning — Tester utformade för att kontrollera systemets stabilitet under standardbelastningar och för att hitta den maximala belastningen vid vilken applikationen fortfarande fungerar korrekt.
  • Stresstestning — Testning utformad för att kontrollera applikationens prestanda under icke-standardiserade belastningar och för att fastställa den maximala belastningen före systemfel.
  • Säkerhetstestning — Tester som används för att kontrollera systemets säkerhet (från hackare, virus, obehörig åtkomst till konfidentiell data och andra förtjusande attacker).
  • Lokaliseringstestning — Tester av applikationens lokalisering.
  • Användbarhetstestning — Test som syftar till att kontrollera användbarhet, förståelighet, attraktivitet och inlärbarhet.
Det här låter bra, men hur fungerar det i praktiken? Enkel! Vi använder Mike Cohns testpyramid: Allt om enhetstestning: tekniker, koncept, praktik - 2Detta är en förenklad version av pyramiden: den är nu uppdelad i ännu mindre delar. Men idag blir vi inte alltför sofistikerade. Vi kommer att överväga den enklaste versionen.
  1. Enhet — Det här avsnittet hänvisar till enhetstester, som tillämpas i olika skikt av applikationen. De testar den minsta delbara enheten för tillämpningslogik. Till exempel klasser, men oftast metoder. Dessa tester försöker vanligtvis så mycket som möjligt att isolera det som testas från någon extern logik. Det vill säga, de försöker skapa en illusion av att resten av programmet körs som förväntat.

    Det bör alltid finnas många av dessa tester (fler än någon annan typ), eftersom de testar små bitar och är mycket lätta, inte förbrukar mycket resurser (vilket betyder RAM och tid).

  2. Integration — Det här avsnittet hänvisar till integrationstestning. Denna testning kontrollerar större delar av systemet. Det vill säga att den antingen kombinerar flera delar av logik (flera metoder eller klasser), eller så kontrollerar den korrektheten av interaktion med en extern komponent. Dessa tester är vanligtvis mindre än enhetstester eftersom de är tyngre.

    Ett exempel på ett integrationstest kan vara att ansluta till en databas och kontrollera att metoderna för att arbeta med den fungerar korrekt.

  3. UI — Det här avsnittet hänvisar till tester som kontrollerar användargränssnittets funktion. De involverar logiken på alla nivåer av applikationen, varför de också kallas för end-to-end-tester. Som regel är det mycket färre av dem, eftersom de är de mest besvärliga och måste kontrollera de mest nödvändiga (begagnade) vägarna.

    På bilden ovan ser vi att triangelns olika delar varierar i storlek: ungefär samma proportioner finns i antalet olika sorters tester i verkligt arbete.

    Idag ska vi titta närmare på de vanligaste testerna, enhetstester, eftersom alla Java-utvecklare med självrespekt borde kunna använda dem på en grundläggande nivå.

Nyckelbegrepp inom enhetstestning

Testtäckning (kodtäckning) är ett av huvudmåtten på hur väl en applikation är testad. Detta är den procentandel av koden som omfattas av testerna (0-100%). I praktiken strävar många efter denna procentsats som sitt mål. Det är något jag inte håller med om, eftersom det innebär att tester börjar tillämpas där de inte behövs. Anta till exempel att vi har standard CRUD-operationer (skapa/skaffa/uppdatera/ta bort) i vår tjänst utan ytterligare logik. Dessa metoder är rena mellanhänder som delegerar arbete till lagret som arbetar med förvaret. I det här läget har vi inget att testa, förutom kanske om den givna metoden kallar en DAO-metod, men det är ett skämt. Ytterligare verktyg används vanligtvis för att bedöma testtäckning: JaCoCo, Cobertura, Clover, Emma, ​​etc. För en mer detaljerad studie av detta ämne, TDD står för testdriven utveckling. I detta tillvägagångssätt, innan du gör något annat, skriver du ett test som kommer att kontrollera specifik kod. Detta visar sig vara black-box-testning: vi vet att ingången är och vi vet vad utmatningen ska vara. Detta gör det möjligt att undvika kodduplicering. Testdriven utveckling börjar med att designa och utveckla tester för varje funktionalitet i din applikation. I TDD-metoden skapar vi först ett test som definierar och testar kodens beteende. Huvudmålet med TDD är att göra din kod mer förståelig, enklare och felfri. Allt om enhetstestning: tekniker, koncept, praktik - 3Tillvägagångssättet består av följande:
  • Vi skriver vårt test.
  • Vi kör testet. Föga överraskande misslyckas det, eftersom vi ännu inte har implementerat den nödvändiga logiken.
  • Lägg till koden som gör att testet går igenom (vi kör testet igen).
  • Vi refaktorerar koden.
TDD bygger på enhetstester, eftersom de är de minsta byggstenarna i testautomationspyramiden. Med enhetstester kan vi testa affärslogiken för vilken klass som helst. BDD står för beteendedriven utveckling. Detta tillvägagångssätt är baserat på TDD. Mer specifikt använder den klarspråksexempel som förklarar systembeteende för alla som är involverade i utveckling. Vi kommer inte att fördjupa oss i denna term, eftersom den främst berör testare och affärsanalytiker. Ett testfall är ett scenario som beskriver stegen, specifika villkor och parametrar som krävs för att kontrollera koden som testas. En testfixtur är kod som ställer in testmiljön så att den har det tillstånd som krävs för att metoden som testas ska fungera framgångsrikt. Det är en fördefinierad uppsättning objekt och deras beteende under specificerade förhållanden.

Stadier av testning

Ett test består av tre steg:
  • Ange testdata (fixturer).
  • Utöva koden under test (kalla den testade metoden).
  • Verifiera resultaten och jämför med de förväntade resultaten.
Allt om enhetstestning: tekniker, koncept, praktik - 4För att säkerställa testmodularitet måste du isolera från andra lager i applikationen. Detta kan göras med hjälp av stubbar, hånar och spioner. Mockar är objekt som kan anpassas (till exempel skräddarsydda för varje test). De låter oss specificera vad vi förväntar oss av metodanrop, dvs de förväntade svaren. Vi använder skenobjekt för att verifiera att vi får vad vi förväntar oss. Stubbar ger ett hårdkodat svar på samtal under testning. De kan också lagra information om samtalet (till exempel parametrar eller antalet samtal). Dessa kallas ibland spioner. Ibland blandar människor samman termerna stubb och hån: skillnaden är att en stubb inte kontrollerar någonting - den simulerar bara ett givet tillstånd. En hån är ett föremål som har förväntningar. Till exempel att en given metod måste anropas ett visst antal gånger. Med andra ord,

Testmiljöer

Så nu till saken. Det finns flera testmiljöer (frameworks) tillgängliga för Java. De mest populära av dessa är JUnit och TestNG. För vår recension här använder vi: Allt om enhetstestning: tekniker, koncept, praktik - 5Ett JUnit-test är en metod i en klass som endast används för testning. Klassen heter vanligtvis samma som klassen den testar, med "Test" tillagt i slutet. Till exempel, CarService -> CarServiceTest. Maven-byggsystemet inkluderar automatiskt sådana klasser i testomfånget. Faktum är att denna klass kallas en testklass. Låt oss kort gå igenom de grundläggande anteckningarna:

  • @Test indikerar att metoden är ett test (i grund och botten är en metod markerad med denna anteckning ett enhetstest).
  • @Before anger en metod som kommer att exekveras före varje test. Till exempel för att fylla en klass med testdata, läsa indata, etc.
  • @After används för att markera en metod som kommer att anropas efter varje test (t.ex. för att rensa ut data eller återställa standardvärden).
  • @BeforeClass placeras ovanför en metod, analog med @Before. Men en sådan metod anropas bara en gång före alla tester för den givna klassen och måste därför vara statisk. Den används för att utföra mer resurskrävande operationer, som att snurra upp en testdatabas.
  • @AfterClass är motsatsen till @BeforeClass: den körs en gång för den givna klassen, men bara efter alla tester. Den används till exempel för att rensa beständiga resurser eller koppla från en databas.
  • @Ignorera anger att en metod är inaktiverad och kommer att ignoreras under den övergripande testkörningen. Detta används i olika situationer, till exempel om basmetoden har ändrats och testet ännu inte har omarbetats för att ta hänsyn till ändringarna. I sådana fall är det också önskvärt att lägga till en beskrivning, dvs @Ignore("Någon beskrivning").
  • @Test(expected = Exception.class) används för negativa tester. Detta är tester som verifierar hur metoden beter sig vid ett fel, det vill säga testet förväntar sig att metoden ger något slags undantag. En sådan metod indikeras av @Test-anteckningen, men med en indikation på vilket fel som ska fångas.
  • @Test(timeout = 100) kontrollerar att metoden exekveras på inte mer än 100 millisekunder.
  • @Mock används ovanför ett fält för att tilldela ett skenobjekt (detta är inte JUnit-anteckning, utan kommer istället från Mockito). Efter behov ställer vi in ​​mockens beteende för en specifik situation direkt i testmetoden.
  • @RunWith(MockitoJUnitRunner.class) placeras ovanför en klass. Den här anteckningen talar om för JUnit att anropa testerna i klassen. Det finns olika löpare, inklusive dessa: MockitoJUnitRunner, JUnitPlatform och SpringRunner. I JUnit 5 har @RunWith-kommentaren ersatts med den mer kraftfulla @ExtendWith-kommentaren.
Låt oss ta en titt på några metoder som används för att jämföra resultat:

  • assertEquals(Object expects, Object actuals) — kontrollerar om de skickade objekten är lika.
  • assertTrue(boolesk flagga) — kontrollerar om det godkända värdet är sant.
  • assertFalse(boolesk flagga) — kontrollerar om det godkända värdet är falskt.
  • assertNull(Objektobjekt) — kontrollerar om det skickade objektet är null.
  • assertSame(Object firstObject, Object secondObject) — kontrollerar om de godkända värdena refererar till samma objekt.
  • assertThat(T t, Matcher matchare) — Kontrollerar om t uppfyller det villkor som anges i matcher.
AssertJ tillhandahåller också en användbar jämförelsemetod: assertThat(firstObject).isEqualTo(secondObject) . Här har jag nämnt de grundläggande metoderna - de andra är varianter av ovanstående.

Testning i praktiken

Låt oss nu titta på ovanstående material i ett specifikt exempel. Vi kommer att testa en tjänsts uppdateringsmetod. Vi kommer inte att överväga DAO-lagret, eftersom vi använder standard. Låt oss lägga till en förrätt för testerna:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.2.2.RELEASE</version>
   <scope>test</scope>
</dependency>
Och här 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());
   }
}
Rad 8 — dra det uppdaterade objektet från databasen. Rad 9-14 — skapa ett objekt genom byggaren. Om det inkommande objektet har ett fält, ställ in det. Om inte lämnar vi det som finns i databasen. Titta nu på vårt 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 — vår löpare. Rad 4 — vi isolerar tjänsten från DAO-lagret genom att ersätta en mock. Rad 11 — vi ställer in en testenhet (den som vi kommer att använda som marsvin) för klassen. Rad 22 — vi ställer in serviceobjektet, vilket är vad vi kommer att 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());
}
Här ser vi att testet har tre tydliga indelningar: Raderna 3-9 — specificerar fixturer. Rad 11 — exekvera koden som testas. Rad 13-17 — kontrollerar resultaten. Mer detaljerat: Raderna 3-4 — ställ in beteendet för DAO-mock. Rad 5 — ställ in instansen som vi ska uppdatera utöver vår standard. Rad 11 — använd metoden och ta den resulterande instansen. Rad 13 — kontrollera att den inte är null. Rad 14 — jämför ID för resultatet och de givna metodargumenten. Rad 15 — kontrollera om namnet har uppdaterats. Rad 16 — se CPU-resultatet. Rad 17 — vi angav inte detta fält i instansen, så det bör förbli detsamma. Vi kontrollerar det tillståndet här. Låt oss köra det:Allt om enhetstestning: tekniker, koncept, praktik - 6Testet är grönt! Vi kan andas ut :) Sammanfattningsvis förbättrar testning kodens kvalitet och gör utvecklingsprocessen mer flexibel och pålitlig. Föreställ dig hur mycket ansträngning det tar att omdesigna programvara som involverar hundratals klassfiler. När vi har enhetstester skrivna för alla dessa klasser kan vi refaktorera med tillförsikt. Och viktigast av allt, det hjälper oss att enkelt hitta buggar under utvecklingen. Killar och tjejer, det är allt jag har idag. Gilla och lämna en kommentar :)
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION