CodeGym /Java Blog /Willekeurig /Alles over unit testing: technieken, concepten, praktijk
John Squirrels
Niveau 41
San Francisco

Alles over unit testing: technieken, concepten, praktijk

Gepubliceerd in de groep Willekeurig
Tegenwoordig zul je geen applicatie vinden die niet is gedrapeerd met tests, dus dit onderwerp zal relevanter dan ooit zijn voor beginnende ontwikkelaars: je kunt niet slagen zonder tests. Laten we eens kijken welke soorten testen in principe worden gebruikt, en dan zullen we in detail alles bestuderen wat er te weten valt over het testen van eenheden. Alles over unit testing: technieken, concepten, praktijk - 1

Soorten testen

Wat is een toets? Volgens Wikipedia: "Softwaretesten omvat de uitvoering van een softwarecomponent of systeemcomponent om een ​​of meer interessante eigenschappen te evalueren." Met andere woorden, het is een controle van de juistheid van ons systeem in bepaalde situaties. Laten we eens kijken welke soorten testen er in het algemeen zijn:
  • Unit testing — Tests waarvan het doel is om elke module van het systeem afzonderlijk te controleren. Deze tests moeten van toepassing zijn op de kleinste atomaire onderdelen van het systeem, bijvoorbeeld modules.
  • Systeemtesten — Testen op hoog niveau om de werking van een groter deel van de applicatie of het systeem als geheel te controleren.
  • Regressietesten — Testen die worden gebruikt om te controleren of nieuwe functies of bugfixes de bestaande functionaliteit van de applicatie beïnvloeden of oude bugs introduceren.
  • Functioneel testen — Controleren of een onderdeel van de applicatie voldoet aan de eisen die gesteld worden in de specificaties, user stories, etc.

    Soorten functionele testen:

    • White-box testen — Controleren of een deel van de applicatie voldoet aan de vereisten terwijl de interne implementatie van het systeem bekend is;
    • Black-box testen — Controleren of een deel van de applicatie voldoet aan de vereisten zonder de interne implementatie van het systeem te kennen.

  • Prestatietests - Tests die zijn geschreven om te bepalen hoe het systeem of een deel van het systeem presteert onder een bepaalde belasting.
  • Belastingstest — Tests die zijn ontworpen om de stabiliteit van het systeem onder standaardbelastingen te controleren en om de maximale belasting te vinden waarbij de applicatie nog steeds correct werkt.
  • Stresstesten - Testen die zijn ontworpen om de prestaties van de toepassing te controleren onder niet-standaardbelastingen en om de maximale belasting te bepalen voordat het systeem uitvalt.
  • Beveiligingstests — Tests die worden gebruikt om de beveiliging van het systeem te controleren (van hackers, virussen, ongeoorloofde toegang tot vertrouwelijke gegevens en andere heerlijke aanvallen).
  • Lokalisatietests — Tests van de lokalisatie van de applicatie.
  • Bruikbaarheidstesten - Testen gericht op het controleren van bruikbaarheid, begrijpelijkheid, aantrekkelijkheid en leerbaarheid.
Dit klinkt allemaal goed, maar hoe werkt het in de praktijk? Eenvoudig! We gebruiken de testpiramide van Mike Cohn: Alles over unit testing: technieken, concepten, praktijk - 2Dit is een vereenvoudigde versie van de piramide: hij is nu opgedeeld in nog kleinere delen. Maar vandaag zullen we niet te geavanceerd worden. We zullen de eenvoudigste versie overwegen.
  1. Eenheid — Deze sectie verwijst naar eenheidstests, die worden toegepast in verschillende lagen van de applicatie. Ze testen de kleinste deelbare eenheid van applicatielogica. Bijvoorbeeld klassen, maar meestal methoden. Deze tests proberen meestal zoveel mogelijk te isoleren wat wordt getest van externe logica. Dat wil zeggen, ze proberen de illusie te wekken dat de rest van de applicatie werkt zoals verwacht.

    Er zouden altijd veel van deze tests moeten zijn (meer dan enig ander type), omdat ze kleine stukjes testen en erg licht van gewicht zijn en niet veel bronnen verbruiken (wat betekent RAM en tijd).

  2. Integratie — Deze sectie verwijst naar integratietesten. Deze test controleert grotere delen van het systeem. Dat wil zeggen, het combineert verschillende stukjes logica (meerdere methoden of klassen), of het controleert de juistheid van de interactie met een externe component. Deze tests zijn meestal kleiner dan unit-tests omdat ze zwaarder zijn.

    Een voorbeeld van een integratietest zou kunnen zijn om verbinding te maken met een database en de juistheid van de werking van methoden om ermee te werken te controleren.

  3. UI — Deze sectie verwijst naar tests die de werking van de gebruikersinterface controleren. Ze betrekken de logica op alle niveaus van de applicatie, daarom worden ze ook wel end-to-end tests genoemd. In de regel zijn dat er veel minder, omdat ze het meest omslachtig zijn en de meest noodzakelijke (gebruikte) paden moeten controleren.

    In de afbeelding hierboven zien we dat de verschillende delen van de driehoek variëren in grootte: er zijn ongeveer dezelfde verhoudingen in het aantal verschillende soorten tests in het echte werk.

    Vandaag gaan we dieper in op de meest voorkomende tests, unit tests, aangezien alle zichzelf respecterende Java-ontwikkelaars ze op basisniveau zouden moeten kunnen gebruiken.

Kernbegrippen bij het testen van eenheden

Testdekking (codedekking) is een van de belangrijkste maatstaven voor hoe goed een applicatie wordt getest. Dit is het percentage van de code dat wordt gedekt door de tests (0-100%). In de praktijk streven velen dit percentage als doel na. Dat is iets waar ik het niet mee eens ben, omdat het betekent dat tests worden toegepast waar ze niet nodig zijn. Stel dat we standaard CRUD-bewerkingen (creëren/ophalen/bijwerken/verwijderen) in onze service hebben zonder aanvullende logica. Deze methoden zijn louter tussenpersonen die werk delegeren aan de laag die met de repository werkt. In deze situatie hebben we niets te testen, behalve misschien of de gegeven methode een DAO-methode aanroept, maar dat is een grap. Er worden meestal aanvullende tools gebruikt om de testdekking te beoordelen: JaCoCo, Cobertura, Clover, Emma, ​​enz. Voor een meer gedetailleerde studie van dit onderwerp, TDD staat voor test-driven development. Bij deze benadering schrijft u, voordat u iets anders doet, een test die specifieke code controleert. Dit blijkt black-box testen: we weten wat de input is en we weten wat de output zou moeten zijn. Dit maakt het mogelijk om codeduplicatie te voorkomen. Testgestuurde ontwikkeling begint met het ontwerpen en ontwikkelen van tests voor elk stukje functionaliteit in uw applicatie. Bij de TDD-benadering maken we eerst een test die het gedrag van de code definieert en test. Het belangrijkste doel van TDD is om uw code begrijpelijker, eenvoudiger en foutloos te maken. Alles over unit testing: technieken, concepten, praktijk - 3De aanpak bestaat uit het volgende:
  • We schrijven onze test.
  • We voeren de test uit. Het is niet verwonderlijk dat het mislukt, omdat we de vereiste logica nog niet hebben geïmplementeerd.
  • Voeg de code toe die ervoor zorgt dat de test slaagt (we voeren de test opnieuw uit).
  • We herstructureren de code.
TDD is gebaseerd op unittests, aangezien dit de kleinste bouwstenen zijn in de testautomatiseringspiramide. Met unit tests kunnen we de bedrijfslogica van elke klasse testen. BDD staat voor gedragsgestuurde ontwikkeling. Deze aanpak is gebaseerd op TDD. Meer specifiek gebruikt het eenvoudige taalvoorbeelden die het gedrag van het systeem uitleggen voor iedereen die betrokken is bij de ontwikkeling. Op deze term gaan we niet in, aangezien het vooral testers en business analisten treft. Een testcase is een scenario dat de stappen, specifieke voorwaarden en parameters beschrijft die nodig zijn om de te testen code te controleren. Een testopstelling is code die de testomgeving instelt om de status te hebben die nodig is om de te testen methode succesvol te laten werken. Het is een vooraf gedefinieerde set objecten en hun gedrag onder gespecificeerde voorwaarden.

Stadia van testen

Een test bestaat uit drie fasen:
  • Specificeer testgegevens (fixtures).
  • Oefen de te testen code (noem de geteste methode).
  • Controleer de resultaten en vergelijk met de verwachte resultaten.
Alles over unit testing: technieken, concepten, praktijk - 4Om testmodulariteit te garanderen, moet u zich isoleren van andere lagen van de applicatie. Dit kan worden gedaan met behulp van stubs, mocks en spionnen. Mocks zijn objecten die kunnen worden aangepast (bijvoorbeeld op maat gemaakt voor elke test). Ze laten ons specificeren wat we verwachten van methodeaanroepen, dwz de verwachte antwoorden. We gebruiken nepobjecten om te verifiëren dat we krijgen wat we verwachten. Stubs bieden een hardgecodeerd antwoord op oproepen tijdens het testen. Ze kunnen ook informatie over de oproep opslaan (bijvoorbeeld parameters of het aantal oproepen). Deze worden soms spionnen genoemd. Soms verwarren mensen de termen stub en mock: het verschil is dat een stub niets controleert - het simuleert alleen een bepaalde toestand. Een mock is een object dat verwachtingen heeft. Bijvoorbeeld dat een bepaalde methode een bepaald aantal keren moet worden aangeroepen. Met andere woorden,

Omgevingen testen

Dus, nu to the point. Er zijn verschillende testomgevingen (frameworks) beschikbaar voor Java. De meest populaire hiervan zijn JUnit en TestNG. Voor onze review hier gebruiken we: Alles over unit testing: technieken, concepten, praktijk - 5Een JUnit-test is een methode in een klasse die alleen wordt gebruikt voor testen. De klasse wordt meestal dezelfde naam gegeven als de klasse die wordt getest, met "Test" aan het einde toegevoegd. Bijvoorbeeld AutoService -> AutoServiceTest. Het Maven-bouwsysteem neemt dergelijke klassen automatisch op in de testscope. In feite wordt deze klasse een testklasse genoemd. Laten we kort de basisannotaties bespreken:

  • @Test geeft aan dat de methode een test is (in feite is een methode gemarkeerd met deze annotatie een eenheidstest).
  • @Before betekent een methode die vóór elke test wordt uitgevoerd. Om bijvoorbeeld een klasse te vullen met testgegevens, invoergegevens te lezen, enz.
  • @After wordt gebruikt om een ​​methode te markeren die na elke test wordt aangeroepen (bijv. om gegevens te wissen of standaardwaarden te herstellen).
  • @BeforeClass wordt boven een methode geplaatst, analoog aan @Before. Maar zo'n methode wordt maar één keer aangeroepen vóór alle tests voor de gegeven klasse en moet daarom statisch zijn. Het wordt gebruikt om meer resource-intensieve bewerkingen uit te voeren, zoals het opstarten van een testdatabase.
  • @AfterClass is het tegenovergestelde van @BeforeClass: het wordt één keer uitgevoerd voor de gegeven klasse, maar pas na alle tests. Het wordt bijvoorbeeld gebruikt om persistente bronnen te wissen of de verbinding met een database te verbreken.
  • @Ignore geeft aan dat een methode is uitgeschakeld en zal worden genegeerd tijdens de algehele testrun. Dit wordt in verschillende situaties gebruikt, bijvoorbeeld als de basismethode is gewijzigd en de test nog niet is herwerkt om de wijzigingen op te vangen. In dergelijke gevallen is het ook wenselijk om een ​​beschrijving toe te voegen, bijv. @Ignore("Een beschrijving").
  • @Test(expected = Exception.class) wordt gebruikt voor negatieve tests. Dit zijn tests die verifiëren hoe de methode zich gedraagt ​​in het geval van een fout, dat wil zeggen dat de test verwacht dat de methode een soort uitzondering genereert. Een dergelijke methode wordt aangegeven door de @Test-annotatie, maar met een indicatie van welke fout moet worden opgevangen.
  • @Test(timeout = 100) controleert of de methode wordt uitgevoerd in niet meer dan 100 milliseconden.
  • @Mock wordt boven een veld gebruikt om een ​​mock-object toe te wijzen (dit is geen JUnit-annotatie, maar komt van Mockito). Indien nodig stellen we het gedrag van de mock voor een specifieke situatie direct in de testmethode in.
  • @RunWith(MockitoJUnitRunner.class) wordt boven een klasse geplaatst. Deze annotatie vertelt JUnit om de tests in de klas aan te roepen. Er zijn verschillende hardlopers, waaronder deze: MockitoJUnitRunner, JUnitPlatform en SpringRunner. In JUnit 5 is de @RunWith-annotatie vervangen door de krachtigere @ExtendWith-annotatie.
Laten we eens kijken naar enkele methoden die worden gebruikt om resultaten te vergelijken:

  • assertEquals(Object expects, Object actuals) — controleert of de doorgegeven objecten gelijk zijn.
  • assertTrue(booleaanse vlag) — controleert of de doorgegeven waarde waar is.
  • assertFalse(booleaanse vlag) — controleert of de doorgegeven waarde onwaar is.
  • assertNull(Object object) — controleert of het doorgegeven object null is.
  • assertSame(Object firstObject, Object secondObject) — controleert of de doorgegeven waarden naar hetzelfde object verwijzen.
  • beweerDat(T t, Matcher matcher) — Controleert of t voldoet aan de voorwaarde gespecificeerd in matcher.
AssertJ biedt ook een handige vergelijkingsmethode: assertThat(firstObject).isEqualTo(secondObject) . Hier heb ik de basismethoden genoemd - de andere zijn variaties op het bovenstaande.

Testen in de praktijk

Laten we nu eens kijken naar het bovenstaande materiaal in een specifiek voorbeeld. We zullen de updatemethode van een service testen. We houden geen rekening met de DAO-laag, omdat we de standaard gebruiken. Laten we een starter toevoegen voor de tests:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.2.2.RELEASE</version>
   <scope>test</scope>
</dependency>
En hier hebben we de serviceklasse:

@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());
   }
}
Regel 8 — haal het bijgewerkte object uit de database. Regels 9-14 - maak een object via de bouwer. Als het inkomende object een veld heeft, stelt u dit in. Zo niet, dan laten we wat er in de database staat. Kijk nu naar onze 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);
   }
Lijn 1 — onze Runner. Regel 4 — we isoleren de service van de DAO-laag door een mock te vervangen. Regel 11 - we stellen een testentiteit in (degene die we als proefkonijn zullen gebruiken) voor de klas. Regel 22 - we stellen het serviceobject in, wat we zullen testen.

@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());
}
Hier zien we dat de test drie duidelijke indelingen heeft: Regels 3-9 — specificatie van armaturen. Regel 11 - het uitvoeren van de geteste code. Regels 13-17 - de resultaten controleren. Meer in detail: regels 3-4 — stel het gedrag in voor de DAO-mock. Regel 5 — stel de instantie in die we bovenop onze standaard zullen updaten. Regel 11 - gebruik de methode en neem de resulterende instantie. Regel 13 - controleer of het niet null is. Regel 14 — vergelijk de ID van het resultaat en de gegeven methodeargumenten. Regel 15 - controleer of de naam is bijgewerkt. Regel 16 - zie het CPU-resultaat. Regel 17 — we hebben dit veld niet gespecificeerd in de instantie, dus het moet hetzelfde blijven. We controleren die voorwaarde hier. Laten we het uitvoeren:Alles over unit testing: technieken, concepten, praktijk - 6De toets is groen! We kunnen opgelucht ademhalen :) Kortom, testen verbetert de kwaliteit van de code en maakt het ontwikkelproces flexibeler en betrouwbaarder. Stelt u zich eens voor hoeveel moeite het kost om software met honderden klassenbestanden opnieuw te ontwerpen. Als we voor al deze klassen unittests hebben geschreven, kunnen we met vertrouwen refactoren. En nog belangrijker, het helpt ons gemakkelijk bugs te vinden tijdens de ontwikkeling. Jongens en meiden, meer heb ik niet vandaag. Geef me een like en laat een reactie achter :)
Opmerkingen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION