Ben je op zoek naar een manier om eenvoudiger testen te maken? We hebben je! Bekijk het volgende artikel en leer hoe je dit mogelijk maakt.
Moderne applicatieontwikkeling is gebaseerd op één eenvoudige regel:
Gebruik samenstelling
We voegen klassen, functies en services samen tot grotere stukken software. Dat laatste element is de basis van microservices en zeshoekige architectuur. We willen graag bestaande oplossingen gebruiken, deze integreren met onze software en rechtstreeks naar de markt.
Wil je accountregistratie afhandelen en gebruikersgegevens opslaan? Dan kun je een van de OAuth-services kiezen. Misschien biedt je applicatie een soort abonnement of betaling aan? Er zijn veel diensten die je hierbij kunnen helpen. Heb je analytics nodig op je website, maar begrijp je GDPR niet? Neem gerust een van de kant-en-klare oplossingen.
Iets dat ontwikkeling vanuit zakelijk oogpunt zo eenvoudig maakt, kan je hoofdpijn bezorgen - het moment waarop je een eenvoudige test moet schrijven.
De Fantastic Beasts: Wachtrijen, databases en hoe ze te testen
Unit testen is vrij eenvoudig. Als je alleen de regels volgt, dan zullen je testomgeving en code gezond zijn. Welke regels zijn dat?
Gemakkelijk te schrijven - Een unit test moet gemakkelijk te schrijven zijn omdat je er veel van schrijft. Minder moeite betekent dat er meer tests worden geschreven.
Leesbaar - De testcode moet gemakkelijk te lezen zijn. De test is een verhaal. Het beschrijft het gedrag van software en kan worden gebruikt als een snelkoppeling voor documentatie. Een goede unit test helpt je om bugs op te lossen zonder de code te debuggen.
Betrouwbaar - de test mag alleen mislukken als er een fout zit in het systeem dat getest wordt. Duidelijk? Niet altijd. Soms slagen tests als je ze één voor één uitvoert, maar mislukken ze als je ze als een set uitvoert. Ze slagen op jouw machine, maar falen op CI (Werkt op mijn machine). Een goede unit test heeft slechts één reden voor mislukking.
Snel - testen moeten snel zijn. De voorbereiding, de start en de testuitvoering zelf moeten heel snel zijn. Anders schrijf je ze wel, maar voer je ze niet uit. Trage tests betekenen verlies van focus. Je wacht en kijkt naar de voortgangsbalk.
Onafhankelijk - Ten slotte moet de test onafhankelijk zijn. Deze regel komt voort uit de vorige. Alleen echt onafhankelijke tests kunnen een eenheid worden. Ze interfereren niet met elkaar, kunnen in elke volgorde worden uitgevoerd en potentiële fouten zijn niet afhankelijk van de resultaten van andere tests. Onafhankelijk betekent ook geen afhankelijkheid van externe bronnen zoals databases, berichtendiensten of bestandssystemen. Als je moet communiceren met externen, kun je mocks, stubs of dummy's gebruiken.
Alles wordt ingewikkeld als we enkele integratietests willen schrijven. Het is niet erg als we een paar services samen willen testen. Maar als we services moeten testen die externe bronnen gebruiken, zoals databases of berichtendiensten, dan vragen we om problemen.
Om de test uit te voeren, moet je...
Vele jaren geleden, toen we enkele integratietests wilden maken en bijvoorbeeld databases wilden gebruiken, hadden we twee opties:
We kunnen lokaal een database installeren. Stel een schema in en maak verbinding vanuit onze test;
We kunnen verbinding maken met een bestaande instantie "ergens in de ruimte".
Beide hadden voor- en nadelen. Maar beide introduceren extra niveaus van complexiteit. Soms was het technische complexiteit die voortkwam uit de eigenschappen van bepaalde tools, bijvoorbeeld de installatie en het beheer van Oracle DB op je localhost. Soms was het een ongemak in het proces, bijv. je moet het eens zijn met de test team over JMS-gebruik... elke keer dat je tests wilt uitvoeren.
Containers als redding
In de afgelopen 10 jaar heeft het idee van containerisatie steeds meer erkenning gekregen in de industrie. Het is dus een logische keuze om containers te kiezen als oplossing voor ons integratietestprobleem. Dit is een eenvoudige, schone oplossing. Je hoeft alleen maar je proces build uit te voeren en alles werkt! Kun je het niet geloven? Kijk dan eens naar deze eenvoudige configuratie van een maven build:
com.dkanejs.maven.plugins/groupId>
docker-compose-maven-plugin
4.0.0
op
test-compileren
op
${project.basedir}/docker-compose.yml
true
down
post-integratie-test
down
${project.basedir}/docker-compose.yml
true
En de docker-compose.yml Het bestand ziet er ook mooi uit!
Het bovenstaande voorbeeld is heel eenvoudig. Gewoon één postgres database, pgAdmin en dat is alles. Wanneer je
bash
$ mvn clean verifiëren
dan start de maven-plugin de containers en schakelt ze na de tests weer uit. Er ontstaan problemen als het project groeit en ons composeerbestand ook. Elke keer moet je alle containers starten en ze zullen de hele build in leven blijven. Je kunt de situatie een beetje verbeteren door de uitvoeringsconfiguratie van de plugin te veranderen, maar dat is niet genoeg. In het ergste geval putten je containers systeembronnen uit voordat de tests starten!
En dit is niet het enige probleem. Je kunt geen enkele integratietest uitvoeren vanuit je IDE. Daarvoor moet je de containers met de hand starten. Bovendien zal de volgende maven run die containers afbreken (kijk eens naar naar beneden uitvoering).
Deze oplossing is dus als een groot vrachtschip. Als alles goed werkt, dan is het ok. Elk onverwacht of ongewoon gedrag leidt ons naar een soort ramp.
Maar wat als we onze containers konden uitvoeren vanuit tests? Dit idee ziet er goed uit en wordt al geïmplementeerd. TestcontainersOmdat we het over dit project hebben, is hier een oplossing voor onze problemen. Niet ideaal, maar niemand is perfect.
Dit is een Java bibliotheek, die JUnit- en Spock-tests ondersteunt en lichtgewicht en eenvoudige manieren biedt om de Docker-container uit te voeren. Laten we er eens naar kijken en wat code schrijven!
Vereisten en configuratie
Voordat we beginnen, moeten we onze configuratie controleren. Testvaten nodig hebben:
Docker in versie v17.09,
Java minimaal versie 1.8,
Toegang tot het netwerk, vooral tot docker.hub.
Meer over de vereisten voor specifieke OS en CI kun je vinden in documentatie.
Nu is het tijd om wat regels toe te voegen aan pom.xml.
org.testcontainers/groupId>
testcontainers-bom
${testcontaines.version}
pom
import
org.postgresql
postgresql
runtime
org.testcontainers/groupId>
postgresql
test
org.testcontainers/groupId>
junit-jupiter
test
Ik gebruik Testvaten versie 1.17.3maar voel je vrij om de nieuwste te gebruiken.
Tests met Postgres-container
De eerste stap is het voorbereiden van onze instantie van een container. Je kunt dat direct in de test doen, maar een onafhankelijke klasse ziet er beter uit.
openbare klasse Postgres13TC uitbreidt PostgreSQLContainer {
private static final Postgres13TC TC = nieuwe Postgres13TC();
private Postgres13TC() {
super("postgres:13.2");
}
public static Postgres13TC getInstance() {
TC terug;
}
@Override
public void start() {
super.start();
System.setProperty("DB_URL", TC.getJdbcUrl());
System.setProperty("DB_USERNAME", TC.getUsername());
System.setProperty("DB_PASSWORD", TC.getPassword());
}
@Override
openbare void stop() {
// niets doen. Dit is een gedeelde instantie. Laat JVM deze operatie afhandelen.
}
}
Aan het begin van de tests maken we een instantie van Postgres13TC. Deze klasse kan informatie over onze container verwerken. Het belangrijkste hier zijn de database connectie strings en credentials. Nu is het tijd om een heel eenvoudige test te schrijven.
Ik gebruik hier JUnit 5. Annotatie @Testcontainers is een onderdeel van de extensies die containers in de testomgeving controleren. Ze vinden alle velden met @Container annotatie en respectievelijk start- en stopcontainers.
Testen met Spring Boot
Zoals ik al eerder zei, gebruik ik Spring Boot in het project. In dit geval moeten we iets meer code schrijven. De eerste stap is het maken van een extra configuratieklasse.
Deze klasse overschrijft de bestaande eigenschappen met waarden uit de testcontainer. De eerste drie eigenschappen zijn standaard Spring eigenschappen. De volgende vijf zijn aanvullende, aangepaste eigenschappen die kunnen worden gebruikt om andere bronnen en extensies zoals liquibase te configureren, bijv:
Nu is het tijd om een eenvoudige integratietest te definiëren.
@SpringBootTest(webomgeving = RANDOM_PORT)
@AutoConfigureTestDatabase(vervangen = GEEN)
@ContextConfiguratie(initializers = ContainerInit.class)
@Testcontainers
klasse DummyRepositoryTest {
@Autowired
private DummyRepository;
@Test
id shouldReturnDummy() {
var byId = dummyRepository.getById(10L);
var expected = nieuwe Dummy();
expected.setId(10L);
assertThat(byId).completes().emitsCount(1).emits(expected);
}
}
We hebben hier wat extra annotaties.
@SpringBootTest(webomgeving = RANDOM_PORT) - markeert de test als een Spring Boot test en start de Spring context.
@AutoConfigureTestDatabase(vervangen = GEEN) - deze annotaties zeggen dat de springtestuitbreiding de postgres databaseconfiguratie niet mag vervangen door H2 in de geheugenconfiguratie.
@ContextConfiguratie(initializers = ContainerInit.class) - een extra veercontext configuratie waar we eigenschappen instellen van Testvaten.
@Testcontainers - Zoals eerder vermeld, regelt deze annotatie de levenscyclus van de container.
In dit voorbeeld gebruik ik reactive repositories, maar het werkt hetzelfde met gewone JDBC en JPA repositories.
Nu kunnen we deze test uitvoeren. Als het de eerste keer is, moet de engine images ophalen van docker.hub. Dat kan even duren. Daarna zullen we zien dat er twee containers draaien. De ene is postgres en de andere is Testcontainers controller. Die tweede container beheert de draaiende containers en zelfs als JVM onverwacht stopt, schakelt het de containers uit en ruimt het de omgeving op.
Laten we samenvatten
Testvaten zijn zeer eenvoudig te gebruiken tools die ons helpen om integratietests te maken die Docker-containers gebruiken. Dat geeft ons meer flexibiliteit en verhoogt de ontwikkelingssnelheid. Het goed instellen van testconfiguratie vermindert de tijd die nodig is voor nieuwe ontwikkelaars. Ze hoeven niet alle afhankelijkheden in te stellen, maar alleen de geschreven tests uit te voeren met geselecteerde configuratiebestanden.