Hvis en integrationstest fejler, skal det helst være fordi at vi enten har lavet en kodefejl, eller at vi er ved at opgradere versionen af den komponent vi tester. Hvis testen fejler på grund af sideeffekter i den kontekst vi lever f.eks. brugere opretter data, systemer går ned eller at vi mister kontakten på grund af netværksfejl er det et problem – måske er vi endda i gang med et helt andet projekt og i kampens hede fristes vi til helt at deaktivere disse tests blot for at komme videre.
I følgende indlæg vil jeg prøve at komme med en skala for graden af isolation af tests, samt med et par konkrete eksempler på komponenter der hjælper til isoleret afvikling.
Det er ikke fordi at vi ikke er interesserede i fejl i integrationspunkterne når konteksten ændrer sig – det er blot en anden del af vores tests. Så enkelte dele af vores testsuite har forskelligt fokus – og skulle helst kun indikere fejl når der reelt er fejl i fokusområdet.
For at kunne skabe tests der kun har ét fokusområde er vi nød til at skabe isolation – skrive testen så det kun er det primære testemne der kan forårsage et negativt svar.
Det er en svær opgave. Vi har ikke altid mulighed for at skabe fuldt isolerede tests. Årsagerne er forskellige; mængden af basisdata simpelthen for stor, for stor indflydelse på databasen fra eksterne systemer eller også er det slet og ret ikke tidsmæssigt forsvarligt.
Vi skal også være pragmatiske – det er langt vigtigere at få skrevet tests, end at alle tests kører i fuld isolation. Men måske kan vi opnå mere bevidsthed hvis vi havde en faktor for hvor isoleret hvert enkelt projekt er – hvor tæt er vi på at have skabt en fuld isoleret test? Graden af isolation kunne f.eks. deles ind i følgende niveauer:
Niveau 1
Der testes mod en komponent der bliver oprettet on-the-fly inkl. konfiguration, skemaer og basisdata.
Niveau 2
Der integrationstestes mod en allerede aktiv komponent men konfiguration, skemaer og basisdata bliver oprettet on-the-fly.
Niveau 3
Der integrationstestes mod en allerede aktiv komponent og konfiguration, skemaer og basisdata eksisterer på forhånd.
Hvordan opnår vi komponentisolation?
Nogle vælger at scripte hele miljøer og dermed give mulighed for at skabe dem on-the-fly, en god løsning hvis opsætningen er kompleks og/eller der er tale om mange systemer. Vagrant er en af disse løsninger, hvor man lader et program holde øje med alle de ændringer man skaber, og bagefter kan checke resultatet ind i et versionsstyringsystem.
Det er ikke alle byggesystemer understøtter denne model. Et alternativ er at skabe komponenter der fra starten kan afvikles ad hoc fra udviklingsmiljøet og undgå at efterlade permanente ændringer.
Nogle komponenter er allerede forberedt på dette – arbejder man fx. med RavenDB på .NET platformen, så tilbyder den en klasse der giver en ren in-memory database. (Oren Eini aka Ayende taler om RavenDB på dette års GOTO.)
Med andre systemer er udfordringen lidt større – her er et par eksempler på mine løsninger:
Redis
Redis er en distribueret cache der også skriver snapshots af rammen til disk. Den er skrevet i C og derfor ikke nem at instansere fra .NET. Den består til gengæld blot af en enkelt eksekverbar fil, der nemt kan afvikles som en separat proces. Trin et er så at lave en klasse der nemt starter den op og lukker den ned igen når man er færdig. For at undgå at skulle bekymre sig om at skulle have den eksekverbare fil liggende et bestemt sted på den lokale disk, er selve exe-filen blevet tilføjet til projektet som en embedded stream – ved opstart kopieres stream’en til temp-mappen i Windows og afvikles herfra. Ved afslutning dræbes processen og filen fjernes igen. Da den ved opstart bliver konfigureret til at køre på en tilfældig port er der ikke nogen problemer med at køre flere samtidige instanser. De er helt isolerede og man behøver ikke at tænke over hvilken rækkefølge man afvikler ens tests med.
Dermed kan en integrationstest realiseres således:
using (var redis = new Redis()) // her kan Redis tilgåes på redis.Endpoint med en almindelig klient }
Resultatet er en separat komponent der blot kan tilføjes til ens testprojekt og kan ses her.
Elasticsearch
Elasticsearch er skrevet i Java, så hvis det er der man arbejder så har man ud af boksen nogle gode muligheder for isolerede integrationstests. På .NET platformen er udfordringen lidt større. Jeg vil gerne kunne køre mine tests på helt separate byggeplatforme som fx. på SaaS-platformen AppVeyor og her kan man ikke forvente at have et Java-runtime-miljø.
Derfor gik opgaven ud på at pakke både hele Java-runtime (JRE) og Elasticsearch sammen i en enkelt .NET assembly. JRE’en alene fylder ca. 140mb og Elasticsearch ca. 33mb. Almindelig zip af filerne løste dette umiddelbare problem og den samlede størrelse af det endelige bibliotek endte omkring 90mb. Problemet med dette var dog opstartstiden – zip er åbenbart ikke det hurtigste format og selve udpakningen tog alt for lang tid.
LZ4 er en ny interessant komprimeringsalgoritme der er så hurtig at den ofte er på niveau med almindelig tilgang til RAM. Den har en variant der hedder HC (high compression) der tager lidt længere tid om at pakke men er meget hurtig at udpakke – perfekt til denne situation. Det viste sig så at zip faktisk er ret hurtig når den afvikles helt uden komprimering og kun benyttes som containerformat – dermed var løsningen på plads. Først zippes hele JRE’en til en enkelt fil og dernæst køres denne fil igennem LZ4 HC.
Resultatet er at JRE og Elasticsearch kan skrives til en folder på 2-3 sekunder på min standard computer. Ganske fint, taget i betragtning at Elasticsearch der ud over i sig selv tager 6-7 sekunder om at starte op. Under selve opstarten polles Elasticsearch indtil den melder klar, således vil den altid være klar til at tage imod forespørgsler fra integrationstests:
using (var elasticsearch = new Elasticsearch()) { // her kan Elasticsearch tilgåes på elasticsearch.Url med en almindelig klient }
Resultatet af dette er ligeledes en separat komponent – og kan ses her.
Med isolerede integrationstests og ved at bruge lidt energi på at skabe testbare komponenter, kan vi opnå en situation hvor vi kun skal bekymre os om forretningslogik og test af denne. Det giver desuden nogle gode værktøjer når nye versioner skal prøves af – da det blot handler om at opgradere en pakke.
Jeg er overbevist om at tiden man bruger på at skabe en isoleret kontekst kommer igen i tifold når den rigtige forretningskode skal skrives – i fremtiden vil dette have stor indflydelse på mine komponentvalg så mine projekter kan opnå niveau 1 i integrationstests.
Hvilket niveau er dit softwareprojekt på?
[…] Bedre softwaretests med fuld isolation […]
[…] Bedre softwaretests med fuld isolation […]