Letar du efter ett sätt att göra tester på ett enklare sätt? Vi har hittat dig! Läs följande artikel och lär dig hur du gör det möjligt.
Modern applikationsutveckling bygger på en enkel regel:
Använd sammansättning
Vi komponerar klasser, funktioner och tjänster till större delar av programvaran. Det sista elementet är grunden för mikrotjänster och hexagonal arkitektur. Vi skulle vilja använda befintliga lösningar, integrera dem med vår programvara och gå direkt till marknad.
Vill du hantera kontoregistrering och lagra användardata? Du kan välja en av OAuth-tjänsterna. Kanske erbjuder din applikation någon form av prenumeration eller betalning? Det finns många tjänster som kan hjälpa dig att hantera detta. Behöver du analyser på din webbplats, men förstår inte GDPR? Känn dig fri och ta en av de färdiga lösningarna.
Något som gör utvecklingen så enkel ur affärssynpunkt kan ge dig huvudvärk - i det ögonblick du behöver skriva ett enkelt test.
De fantastiska djuren: Köer, databaser och hur man testar dem
Enhetstestning är ganska enkelt. Om du bara följer reglerna, så kommer din testmiljö och kod är hälsosamma. Vilka regler är det?
Lätt att skriva - ett enhetstest ska vara lätt att skriva eftersom du skriver många av dem. Mindre ansträngning innebär att fler tester skrivs.
Läsbar - testkoden ska vara lättläst. Testet är en berättelse. Det beskriver programvarans beteende och kan användas som en genväg till dokumentation. Ett bra enhetstest hjälper dig att åtgärda buggar utan att felsöka koden.
Pålitlig - testet ska bara misslyckas om det finns en bugg i det system som testas. Självklart? Inte alltid. Ibland godkänns tester om du kör dem en och en, men misslyckas när du kör dem som en uppsättning. De godkänns på din maskin, men misslyckas på CI (Fungerar på min maskin). Ett bra enhetstest har bara en anledning till att misslyckas.
Snabb - Testerna ska vara snabba. Förberedelser för att köra, starta och själva testkörningen bör vara mycket snabb. Annars kommer du att skriva dem, men inte köra dem. Långsamma tester innebär förlorat fokus. Du väntar och tittar på förloppsindikatorn.
Oberoende - Slutligen ska testet vara oberoende. Denna regel härrör från de föregående. Endast verkligt oberoende tester kan bli en enhet. De stör inte varandra, kan köras i vilken ordning som helst och eventuella fel beror inte på resultaten från andra tester. Oberoende innebär också att det inte finns något beroende av externa resurser som databaser, meddelandetjänster eller filsystem. Om du behöver kommunicera med externa resurser kan du använda mocks, stubbar eller dummies.
Allt blir komplicerat när vi vill skriva några integrationstester. Det är inte så illa om vi vill testa några tjänster tillsammans. Men när vi behöver testa tjänster som använder externa resurser som databaser eller meddelandetjänster, då är vi ute på hal is.
För att köra testet måste du installera...
För många år sedan, när vi ville göra några integrationstester och använda t.ex. databaser, hade vi två alternativ:
Vi kan installera en databas lokalt. Ställ in ett schema och anslut från vårt test;
Vi kan koppla upp oss mot en befintlig instans "någonstans i rymden".
Båda hade fördelar, båda hade nackdelar. Men båda introducerar ytterligare nivåer av komplexitet. Ibland var det teknisk komplexitet som uppstod på grund av egenskaperna hos vissa verktyg, t.ex. installation och hantering av Oracle DB på ditt lokala webbhotell. Ibland var det en olägenhet i processen, t.ex. att du måste hålla med om testet Team om JMS-användning ... varje gång du vill köra tester.
Containrar till undsättning
Under de senaste 10 åren har idén om containerisering fått ett erkännande i branschen. Så ett naturligt beslut är att välja containrar som en lösning på vårt integrationstestproblem. Det här är en enkel och ren lösning. Du kör bara din process build och allt fungerar! Kan du inte tro det? Ta en titt på den här enkla konfigurationen av en maven-build:
com.dkanejs.maven.plugins
docker-compose-maven-plugin
4.0.0
upp
test-kompilera
upp
</goals
${projekt.basedir}/docker-compose.yml
troligt
ned
test efter integration
nedgång
${project.basedir}/docker-compose.yml
troget
</plugin
</plugins
Och docker-compose.yml filen ser också ganska bra ut!
Exemplet ovan är mycket enkelt. Bara en postgres-databas, pgAdmin och det är allt. När du kör
bash
$ mvn ren verifiera
sedan startar maven-plugin containrarna och stänger av dem efter testerna. Problemen börjar när projektet växer och vår compose-fil också växer. Varje gång måste du starta alla behållare, och de kommer att vara vid liv genom hela byggandet. Du kan göra situationen lite bättre genom att ändra plugin-körningskonfigurationen, men det räcker inte. I värsta fall tömmer dina containrar systemresurserna innan testerna startar!
Och detta är inte det enda problemet. Du kan inte köra ett enda integrationstest från din IDE. Innan dess måste du starta behållarna för hand. Dessutom kommer nästa maven-körning att riva ner dessa behållare (ta en titt på ner avrättning).
Så den här lösningen är som ett stort lastfartyg. Om allt fungerar bra, då är det ok. Alla oväntade eller ovanliga beteenden leder oss till någon form av katastrof.
Testcontainrar - kör containrar från tester
Men tänk om vi kunde köra våra containrar från tester? Den här idén ser bra ut, och den håller redan på att implementeras. TestbehållareEftersom vi talar om det här projektet, här är en lösning på våra problem. Den är inte idealisk, men ingen är perfekt.
Detta är en Java biblioteket, som stöder JUnit- och Spock-tester, vilket ger lätta och enkla sätt att köra Docker-containern. Låt oss ta en titt på det och skriva lite kod!
Förutsättningar och konfiguration
Innan vi börjar måste vi kontrollera vår konfiguration. Testbehållare behov:
Docker i version v17.09,
Java minst version 1.8,
Tillgång till nätverk, särskilt till docker.hub.
Mer information om kraven för specifika operativsystem och CI finns i i dokumentation.
Nu är det dags att lägga till några rader till pom.xml.
org.testcontainers
testcontainers-bom
${testcontainers.version}
pom
importera
.
Beroendehantering
org.postgresql
postgresql
körtid Beroende
org.testcontainers
postgresql
test
org.testcontainers
junit-jupiter
test
</dependency
.
Jag använder Testbehållare version 1.17.3men använd gärna den nyaste.
Tester med Postgres-behållare
Det första steget är att förbereda vår instans av en container. Du kan göra det direkt i testet, men en oberoende klass ser bättre ut.
public class Postgres13TC utökar PostgreSQLContainer {
privat statisk slutlig Postgres13TC TC = new Postgres13TC();
privat Postgres13TC() {
super("postgres:13.2");
}
public static Postgres13TC getInstance() {
return TC;
}
@Överstyrning
offentligt ogiltigt start() {
super.start();
System.setProperty("DB_URL", TC.getJdbcUrl());
System.setProperty("DB_USERNAME", TC.getUsername());
System.setProperty("DB_PASSWORD", TC.getPassword());
}
@Överträdelse
public void stop() {
// gör ingenting. Detta är en delad instans. Låt JVM hantera denna operation.
}
}
I början av testerna kommer vi att skapa en instans av Postgres13TC. Den här klassen kan hantera information om vår container. Det viktigaste här är databasens anslutningssträngar och referenser. Nu är det dags att skriva ett mycket enkelt test.
Jag använder JUnit 5 här. Annotation @Testbehållare är en del av de tillägg som styr containrar i testmiljön. De hittar alla fält med @Container annotation och start- respektive stoppbehållare.
Tester med Spring Boot
Som jag nämnde tidigare använder jag Spring Boot i projektet. I det här fallet behöver vi skriva lite mer kod. Det första steget är att skapa en ytterligare konfigurationsklass.
Denna klass åsidosätter de befintliga egenskaperna med värden från testbehållare. De tre första egenskaperna är standard Spring-egenskaper. De följande fem är ytterligare, anpassade egenskaper som kan användas för att konfigurera andra resurser och tillägg som liquibase, t.ex:
Nu är det dags att definiera ett enkelt integrationstest.
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureTestDatabase(replace = NONE)
@ContextConfiguration(initialiserare = ContainerInit.class)
@Testcontainers
klass DummyRepositoryTest {
@Autowired
privat DummyRepository;
@Test
void shouldReturnDummy() {
var byId = dummyRepository.getById(10L);
var expected = ny Dummy();
expected.setId(10L);
assertThat(byId).completes().emitsCount(1).emits(expected);
}
}
Vi har några extra anteckningar här.
@SpringBootTest(webEnvironment = RANDOM_PORT) - markerar testet som ett Spring Boot-test och startar Spring-kontexten.
@AutoConfigureTestDatabase(replace = NONE) - dessa anteckningar säger att vårtesttillägget inte ska ersätta postgres-databasens konfiguration med H2 i minneskonfigurationen.
@ContextConfiguration(initialiserare = ContainerInit.class) - ytterligare ett vårsammanhang konfiguration där vi sätter upp egenskaper från Testbehållare.
@Testbehållare - som tidigare nämnts styr denna annotation behållarens livscykel.
I det här exemplet använder jag reaktiva repositories, men det fungerar på samma sätt med vanliga JDBC- och JPA-repositories.
Nu kan vi köra det här testet. Om det är första gången behöver motorn hämta bilder från docker.hub. Det kan ta en stund. Efter det kommer vi att se att två containrar har körts. Den ena är postgres och den andra är Testcontainers controller. Den andra containern hanterar containrar som körs och även om JVM oväntat stannar, stänger den av containrarna och rensar upp miljön.
Låt oss sammanfatta
Testbehållare är mycket lättanvända verktyg som hjälper oss att skapa integrationstester som använder Docker-containrar. Det ger oss mer flexibilitet och ökar utvecklingshastigheten. Korrekt inställning av testkonfigurationen minskar den tid som krävs för att introducera nya utvecklare. De behöver inte konfigurera alla beroenden, utan bara köra de skrivna testerna med utvalda konfigurationsfiler.