Automatiserade integrationstester – del 1/3
När man skall skriva automatiserade integrations- eller regressionstester ställs man delvis inför lite andra utmaningar än vad som gäller för ”vanliga” enhetstester. Detta är den första delen av tre i en artikelserie om automatiserade integrationstester. Denna första del ger en kort beskrivning av syftet med integrationstester och de utmaningar som ofta uppstår vid testning. Där visar jag också hur man kan parallellisera tester i JUnit för att minska exekveringstiden.
Del två visar på användningen av parametriserade tester och hur man på ett bra sätt kan använda extern testdata (testfixtures).
Den sista delen visar hur man kan koppla ihop sina testsviter med open source-verktyget TestLink för att få fram trevliga rapporter.
Introduktion till integrationstester
Om enhetstester primärt tillhör utvecklarens domän, så hör integrations- och regressiontester traditionellt mera hemma hos Q&A, dvs i ett mindre projekt den eller de testare som är knutna till teamet. I ett agilt team är dock gränserna oftast flytande mellan utveckling och test och en strävan att automatisera integrationstester gör att en utvecklare inte ”kommer undan” ansvaret att skriva dessa tester.
Enhetstester testar som bekant funktionaliteten hos en väl avgränsad enhet i din kod, vanligtvis en klass. Testerna körs i ett testkontext, ofta direkt inifrån din utvecklingsmiljö eller i samband med att enheten byggs och utan att koden som skall testas behöver vara installerad i en produktionsliknande miljö. Här kan utmaning ofta vara att skriva om eller refaktorisera redan skriven kod så att externa beroenden kan ”mockas” bort på ett begripligt sätt. Du kan läsa mer om mockning i den här artikeln.
Integrationstester å andra sidan testar de viktigaste användningsfallen i ditt system, oftast hela vägen från ”ax till limpa”, dvs ända från det att en extern begäran når systemet tills att det avsedda resultatet är uppnått och förutsätter ett system som är uppe och snurrar i en produktionsliknande miljö.
En av grundförutsättningarna enligt mitt sätt att se på integrationstester är att tröskeln att komma igång skall vara låg, något som underlättas om du kan utveckla och köra dina tester utan att behöva lämna den utvecklingsmiljö du normalt jobbar i. Detta innebär i just mitt fall tester baserade på JUnit, utvecklade i ett språk som kan köras på JVM:n och körbara inifrån Eclipse
Vilka är då några av utmaningarna?
- Integrationstester tar normalt längre tid att exekvera än enhetstester. Detta är inte ett problem om antalet tester år få, men det kan vara ett stort problem om du har tusentals integrationstester som skall köras i samband med att en ny version av ditt system skall testas.
Inom exempelvis telekomvärlden varifrån jag hämtar mina senaste erfarenheter är sista anhalten i ett användningsfall ofta en mobiltelefon (verklig eller simulerad). En mobiltelefon kan vara en tämligen opålitlig best med svarstider som är svåra att förutsäga (om den ens är på) vilket får till följd att exekveringstiderna för vissa tester kan bli väldigt långa.
-
Realistisk testdata kan ofta behöva definieras utanför själva testkoden, exempelvis i ett excelark som verksamhetssidan är ansvarig för. Det är inte heller ovanligt att samma test skall köras flera gånger med olika testdata.
-
Många använder någon form av externt test management system, exempelvis TestLink, för att definiera integrationstestfall och testplaner. Då är det trevligt och inte minst tidsbesparande om integrationstesterna automatiskt uppdaterar rätt testplan med resultatet av exekveringen vartefter de körs.
Nu äntligen dags för lite kod!
Parallella tester
Ett sätt att hantera att integrationstester normalt tar längre tid att köra och att du därigenom kan få vänta länge på att alla tester blir klara är att köra flera tester samtidigt. För att förstå hur vi skall åstadkomma parallell exekvering av våra tester så krävs först lite insikt i JUnit4‘s exekveringsmiljö.
I JUnit sköts själva exekveringen av en testklass av en testrunner, en klass som direkt eller indirekt ärver org.junit.runner.Runner
. Normalt sköts detta bakom kulisserna men synliggörs exempelvis om du har behov av att gruppera dina testklasser i en testsvit.
@RunWith(Suite.class)
@Suite.SuiteClasses({MyTest.class,MyOtherTest.class})
public class MyTestSuite {}
I exemplet ovan har vi genom att annotera vår testsvit med annotering @RunWith
sagt åt JUnit att testrunnerklassen org.junit.runners.Suite
kommer att användas för att starta upp exekveringen av de ingående testelementen.
Med testelement avses de enheter som en testrunner ansvarar för att exekvera, om det är testsvit som skall köras så är testelementen alltså testklasser och/eller andra testsviter. För en enskild testklass är det själva testmetoderna som utgör testelementen.
Notera dock att varje enskild testklass kommer att få sin egen testrunner och alltså inte kommer att exekveras av org.junit.runners.Suite
.
Nedanstående fiktiva test kommer alltså att exekveras av min egen MyTestRunner
-klass i stället för den som JUnit skulle använda som default.
@RunWith(MyTestRunner.class)
public class MyTestClass { ... }
Möjligheten att skriva egna testrunners är första byggstenen som skall se till att du kan köra dina tidskrävande integrationstester parallellt.
En egen testrunner måste som tidigare sagts direkt eller indirekt ärva org.junit.runner.Runner
men är i pratiken oftast en subklass till org.junit.runner.ParentRunner
eller någon av dess konkreta subklasser.
Hur använder du nu ovanstående kunskap för att kunna köra dina testfall parallellt?
Från och med JUnit4 använder ParentRunner
en RunnerScheduler
för exekveringen av sina testelement. Genom att att implementera en egen testrunner med en implementation av RunnerScheduler
som använder en trådpool för att exekvera varje enskilt testelement i en egen tråd kan du få en parallell exekvering av dina tester.
Koden kan se ut som följer :
package se.cygni.integrationtests.concurrent;
import org.junit.runners.model.RunnerScheduler;
import java.util.LinkedList;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentTestRunnerScheduler implements RunnerScheduler {
private ExecutorCompletionService completionService;
private LinkedList>tasks;
private ExecutorService executorService;
public ConcurrentTestRunnerScheduler(final Class<?> klass) {
executorService = Executors.newFixedThreadPool(klass.getAnnotation(Concurrent.class).threads());
completionService = new ExecutorCompletionService(executorService);
tasks = new LinkedList > ();
}
@Override
public void schedule(Runnable childStatement) {
tasks.offer(completionService.submit(childStatement, null));
}
@Override
public void finished() {
try {
while (!tasks.isEmpty())
tasks.remove(completionService.take());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
while (!tasks.isEmpty())
tasks.poll().cancel(true);
executorService.shutdownNow();
}
}
}
Den används sedan på följande sätt i egen testrunnerklass:
package se.cygni.integrationtests.concurrent;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;
public class ConcurrentJUnitRunner extends BlockJUnit4ClassRunner {
public ConcurrentJUnitRunner(final Class<?> klass) throws InitializationError {
super(klass);
if (klass.isAnnotationPresent(Concurrent.class)) {
if (runTestsInParallell()) {
setScheduler(getRunnerScheduler(klass));
} else {
System.out.println(String.format("Test class %s is annotated for concurrent run of tests but this test run has disabled concurrent tests.", klass.getSimpleName()));
}
}
}
private static boolean runTestsInParallell() {
return Configuration.runTestsInParallell();
}
private static RunnerScheduler getRunnerScheduler(final Class<?> klass) {
return new ConcurrentTestRunnerScheduler(klass);
}
}
Jag låter min testrunnerklass ärva BlockJUnit4ClassRunner
som är default testrunner för testklasser i JUnit4. Om du vill använda Spring för att konfigurera/köra dina tester skall BlockJUnit4ClassRunner
bytas ut mot SpringJUnit4ClassRunner
ovan (och på övriga ställen i artikeln).
Metoden runTestsInParallell
är en utility-metod som via anrop till en fiktiv konfigurationklass kan stänga av all parallellitet. Denna möjlighet har visat sig användbar vid bl.a. felsökning för att fastställa att eventuella fallerande tester inte beror på att de körs parallellt med andra tester.
Klassen Concurrent
är en annotering som anger att testklassen (eller testsviten) skall köra sina testelement asynkront. Som koden ovan anger så måste denna annotering finnas för att tester skall köras parallellt, detta även om ConcurrentJUnitRunner
används som testrunner.
package se.cygni.integrationtests.concurrent;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Concurrent {
int threads() default 5;
}
Om vi nu sammanfattar det hela så kommer testfallen i följande testklass att exekveras parallellt i 10 samtidiga trådar.
@RunWith(ConcurrentJUnitRunner.class)
@Concurrent(threads=10)
public class MyTestClass { ... }
Inspiration och stora delar av koden är hämtade från detta blogginlägg.
I nästa del kodar vi vidare med parametriserade tester – dvs låta samma test köras flera gånger med olika testdata.