Leter du etter en måte å gjøre tester på en enklere måte? Da har vi noe for deg! Sjekk følgende artikkel og lær hvordan du kan gjøre det mulig.
Moderne applikasjonsutvikling er basert på én enkel regel:
Bruk sammensetning
Vi setter sammen klasser, funksjoner og tjenester til større deler av programvaren. Det siste elementet er grunnlaget for mikrotjenester og sekskantet arkitektur. Vi ønsker å bruke eksisterende løsninger, integrere dem med programvaren vår og gå rett på marked.
Ønsker du å håndtere kontoregistrering og lagre brukerdata? Du kan velge en av OAuth-tjenestene. Kanskje applikasjonen din tilbyr en form for abonnement eller betaling? Det finnes mange tjenester som kan hjelpe deg med å håndtere dette. Trenger du analyser på nettstedet ditt, men forstår ikke GDPR? Ta gjerne en av de ferdige løsningene.
Noe som gjør utvikling så enkelt fra et forretningsmessig synspunkt, kan gi deg hodepine - når du skal skrive en enkel test.
De fantastiske dyrene: Køer, databaser og hvordan du tester dem
Enhetstesting er ganske enkelt. Hvis du bare følger reglene, vil testmiljøet ditt og kode er sunne. Hvilke regler er det?
Lett å skrive - en enhetstest skal være enkel å skrive fordi du skriver mange av dem. Mindre innsats betyr at det skrives flere tester.
Lesbar - testkoden skal være lett å lese. Testen er en historie. Den beskriver hvordan programvaren oppfører seg, og kan brukes som en snarvei til dokumentasjon. En god enhetstest hjelper deg med å fikse feil uten å feilsøke koden.
Pålitelig - testen skal bare feile hvis det er en feil i systemet som testes. Er det åpenbart? Ikke alltid. Noen ganger består tester hvis du kjører dem én og én, men feiler når du kjører dem som et sett. De består på din maskin, men feiler på CI (Fungerer på min maskin). En god enhetstest har bare én årsak til feil.
Rask - testene skal være raske. Forberedelse til kjøring, start og selve testutførelsen bør gå svært raskt. Ellers vil du skrive dem, men ikke kjøre dem. Sakte tester betyr tapt fokus. Du venter og ser på fremdriftslinjen.
Uavhengig - Til slutt skal testen være uavhengig. Denne regelen stammer fra de foregående. Bare virkelig uavhengige tester kan bli en enhet. De forstyrrer ikke hverandre, kan kjøres i hvilken som helst rekkefølge, og potensielle feil avhenger ikke av resultatene fra andre tester. Uavhengige betyr også at de ikke er avhengige av eksterne ressurser som databaser, meldingstjenester eller filsystem. Hvis du trenger å kommunisere med eksterne ressurser, kan du bruke mocks, stubber eller dummies.
Alt blir komplisert når vi ønsker å skrive noen integrasjonstester. Det er ikke så ille hvis vi ønsker å teste noen få tjenester sammen. Men når vi skal teste tjenester som bruker eksterne ressurser, som databaser eller meldingstjenester, er vi ute på dypt vann.
For å kjøre testen må du installere...
For mange år siden, da vi ønsket å lage integrasjonstester og bruke f.eks. databaser, hadde vi to alternativer:
Vi kan installere en database lokalt. Sett opp et skjema og koble til fra testen vår;
Vi kan koble oss til en eksisterende instans "et sted i verdensrommet".
Begge hadde fordeler, begge hadde ulemper. Men begge introduserer flere nivåer av kompleksitet. Noen ganger var det teknisk kompleksitet som følge av egenskapene til visse verktøy, f.eks. installasjon og administrasjon av Oracle DB på din lokale host. Noen ganger var det en ulempe i prosessen, f.eks. at du må bli enig med test team om JMS-bruk hver gang du vil kjøre tester.
Containere til unnsetning
I løpet av de siste ti årene har ideen om containerisering blitt anerkjent i bransjen. Derfor var det naturlig å velge containere som løsning på integrasjonstestproblemet vårt. Dette er en enkel og ren løsning. Du kjører bare prosessbyggingen, så fungerer alt! Kan du ikke tro det? Ta en titt på denne enkle konfigurasjonen av en maven-build:
com.dkanejs.maven.plugins
docker-compose-maven-plugin
4.0.0
up
test-kompilere
opp
${prosjekt.basedir}/docker-compose.yml.
true
down
post-integrasjonstest
ned
${project.basedir}/docker-compose.yml true
true
Og docker-compose.yml filen ser også ganske fin ut!
Eksemplet ovenfor er veldig enkelt. Bare én postgres-database, pgAdmin og det er alt. Når du kjører
bash
$ mvn clean verify
så starter maven-plugin-modulen containerne og slår dem av etter testene. Problemene oppstår når prosjektet vokser, og Compose-filen vår også vokser. Hver gang må du starte alle containerne, og de vil være i live gjennom hele byggingen. Du kan gjøre situasjonen litt bedre ved å endre konfigurasjonen for kjøring av plugin-modulen, men det er ikke nok. I verste fall bruker containerne opp systemressursene dine før testene starter!
Og dette er ikke det eneste problemet. Du kan ikke kjøre en eneste integrasjonstest fra IDE-en din. Før det må du starte containerne for hånd. Dessuten vil neste maven-kjøring rive ned disse containerne (ta en titt på ned utførelse).
Så denne løsningen er som et stort lasteskip. Hvis alt fungerer bra, er alt i orden. Enhver uventet eller uvanlig oppførsel fører oss til en slags katastrofe.
Testcontainere - kjør containere fra tester
Men hva om vi kunne kjøre containerne våre fra tester? Denne ideen ser god ut, og den er allerede i ferd med å bli implementert. TestcontainereSiden vi snakker om dette prosjektet, her er en løsning på problemene våre. Ikke ideelt, men ingen er perfekt.
Dette er en Java biblioteket, som støtter JUnit- og Spock-tester, og som gir lette og enkle måter å kjøre Docker-containeren på. La oss ta en titt på det og skrive litt kode!
Forutsetninger og konfigurasjon
Før vi begynner, må vi sjekke konfigurasjonen vår. Testbeholdere behov:
Docker i versjon v17.09,
Java minimum versjon 1.8,
Tilgang til nettverk, spesielt til docker.hub.
Mer om kravene til spesifikke operativsystemer og CI finner du her i dokumentasjon.
Nå er det på tide å legge til noen linjer til pom.xml.
org.testcontainers
testcontainers-bom $
${testcontainers.versjon}
pom
import
org.postgresql
postgresql runtime</s/s
runtime
org.testcontainers
postgresql test
test
org.testcontainers
junit-jupiter test</scope
test
Jeg bruker Testbeholdere versjon 1.17.3men bruk gjerne den nyeste.
Tester med Postgres-container
Det første trinnet er å klargjøre en containerforekomst. Du kan gjøre det direkte i testen, men det ser bedre ut med en uavhengig klasse.
public class Postgres13TC extends PostgreSQLContainer {
private static final Postgres13TC TC = new Postgres13TC();
private Postgres13TC() {
super("postgres:13.2");
}
public static Postgres13TC getInstance() {
return TC;
}
@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
public void stop() {
// gjør ingenting. Dette er en delt instans. La JVM håndtere denne operasjonen.
}
}
I begynnelsen av testene oppretter vi en forekomst av Postgres13TC. Denne klassen kan håndtere informasjon om containeren vår. Det viktigste her er databasens tilkoblingsstrenger og legitimasjon. Nå er det på tide å skrive en veldig enkel test.
Jeg bruker JUnit 5 her. Annotasjon @Testcontainere er en del av utvidelsene som kontrollerer containere i testmiljøet. De finner alle felt med @Container annotasjon og henholdsvis start- og stoppcontainere.
Tester med Spring Boot
Som jeg nevnte tidligere, bruker jeg Spring Boot i prosjektet. I dette tilfellet må vi skrive litt mer kode. Det første trinnet er å opprette en ekstra konfigurasjonsklasse.
Denne klassen overstyrer de eksisterende egenskapene med verdier fra testbeholder. De tre første egenskapene er standard Spring-egenskaper. De neste fem er ekstra, tilpassede egenskaper som kan brukes til å konfigurere andre ressurser og utvidelser som f.eks. liquibase:
Nå er det på tide å definere en enkel integrasjonstest.
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureTestDatabase(replace = NONE)
@ContextConfiguration(initialisatorer = ContainerInit.class)
@Testcontainere
class DummyRepositoryTest {
@Autowired
private DummyRepository;
@Test
void shouldReturnDummy() {
var byId = dummyRepository.getById(10L);
var expected = new Dummy();
expected.setId(10L);
assertThat(byId).completes().emitsCount(1).emits(expected);
}
}
Vi har noen ekstra kommentarer her.
@SpringBootTest(webEnvironment = RANDOM_PORT) - markerer testen som en Spring Boot-test og starter Spring-konteksten.
@AutoConfigureTestDatabase(replace = NONE) - disse merknadene sier at vårens testutvidelse ikke skal erstatte postgres-databasekonfigurasjonen med H2 i minnekonfigurasjonen.
@ContextConfiguration(initialisatorer = ContainerInit.class) - en ekstra vårkontekst konfigurasjon der vi setter opp egenskaper fra Testbeholdere.
@Testcontainere - som tidligere nevnt, kontrollerer denne annotasjonen beholderens livssyklus.
I dette eksempelet bruker jeg reaktive repositorier, men det fungerer på samme måte med vanlige JDBC- og JPA-repositorier.
Nå kan vi kjøre denne testen. Hvis det er første gang, må motoren hente bilder fra docker.hub. Det kan ta et øyeblikk. Etter det vil vi se at to containere har kjørt. Den ene er postgres og den andre er Testcontainers-kontrolleren. Den andre containeren administrerer containere som kjører, og selv om JVM uventet stopper, slår den av containerne og rydder opp i miljøet.
La oss oppsummere
Testbeholdere er svært brukervennlige verktøy som hjelper oss med å lage integrasjonstester som bruker Docker-containere. Det gir oss mer fleksibilitet og øker utviklingshastigheten. Riktig oppsett av testkonfigurasjon reduserer tiden det tar for nye utviklere å gå om bord. De trenger ikke å sette opp alle avhengigheter, men kan bare kjøre de skrevne testene med utvalgte konfigurasjonsfiler.