Testdesignteknik: Andre gode teknikker

Specifikationsbaseret testdesignteknik

Med denne testdesignteknik laver du dine test cases ud fra forretningsscenarier, der fx kan være beskrevet i form at user stories eller use cases. Happy day scenariet bliver typisk dækket helt af sig selv (som det også blev i ovenstående unit tests), men husk at tjekke om der findes andre forretningsscenarier i din kravspecifikation. De fleste kan du nemlig med fordel lave test cases (og unit tests) ud fra.

Negativ test

Formålet med denne testdesignteknik er at fremprovokere fejl i applikationen. Som udvikler kan du bruge teknikken til at designe test cases, hvor du fx får din mockede API til smide en exception. Så kan du i din test verificere, at din metode håndterer exceptions efter hensigten.

Fejlgætning

Med denne testdesignteknik laver du dine test cases ud fra din intuition og erfaring med lignende programmer og teknologier.
  Hvis du ved, at der typisk opstår fejl i en bestemt slags komponent, så brug din erfaring til at lave unit test(s), der vil fange den type fejl. Og hvis din mavefornemmelse siger dig, at implementeringen af en bestemt funktionalitet meget vil kunne resultere i en fejl, så brug din intuition til at lave unit test(s), der vil fange den type fejl.
  Lad være med at overdrive brugen af denne teknik, fordi den virker som den nemmeste og måske stemmer overens med, hvordan du indtil nu har designet dine unit tests. Den er et godt supplement og bliver endnu bedre i takt med at din test erfaring vokser, men den er ikke hensigtsmæssig som den eneste testdesignteknik. Et vigtigt formål med vores unit tests er jo bl.a. at dokumentere, at applikationen kan håndtere de forskellige scenarier, som vi med rimelig kan forvente, at den skal håndtere.

Andre teknikker

Der findes selvfølgelig andre testdesignteknikker. Du er godt hjulpet på vej med de testdesignteknikker, som jeg har nævnt i denne guide, men hvis testdesign har fanget din interesse, synes jeg så absolut, at du skal kigge mere på de andre testdesignteknikker, der er derude.
  Jeg kan blandt andet anbefale Pairwise Testing, hvis du skal teste kombinationer af parametre, der alle kan have forskellige værdier. Du kan finde mange forskellige slags værktøjer til Pairwise Testing, der alle hjælper dig med at generere det mindst mulige antal test cases, som du skal bruge til at dække de mulige kombinationer. Pairwise Testing eller All-pairs testing er god til at finde fejl i kombinationer og bruges ofte i kritiske systemer, hvor en god test dækning er et krav, og mængden af nødvendige test cases virker uoverskuelig.

The end

Mange tak fordi du læste med! Du er altid velkommen til at komme med et spørgsmål eller en bemærkning i kommentar feltet, og så vil jeg svare hurtigst muligt.

Del 14: Testdesignteknik: Andre gode teknikker

Testdesignteknik: Grænseværdianalyse

Potentielle fejl kan ofte findes ved grænserne af vores partitioner. Minimum- og maksimumværdierne for en partition er gyldige grænseværdier. De ugyldige grænseværdier vil ligge lige uden for partitionen.
  Fejlene opstår i kode som (IF A <= B THEN DO X ELSE DO Y), hvor man ikke er helt skarp på, hvordan man bruger <= eller lignende sammenligninger.
  Personligt morede jeg mig gevaldigt, da jeg første gang lærte, at der findes en decideret testdesignteknik, hvis formål udelukkende er at fange de fejl, som man som udvikler nemt kan lave i if statements og loops. Professionelle testere bruger grænseværdianalysen under test af brugergrænseflader for at fange vores fejl i if statements og loops, men jeg synes nu, at vi skal lette deres arbejde og tage den i brug allerede i vores unit tests.

When it all comes together…


Ex. Send vurderinger af eleverne i en klasse til undervisningsministeriet
Lad os vende tilbage til vores tidligere eksempel og forestille os en kravspecifikation, hvor læreren efter vurderingen af hver enkelt elev i en klasse, skal sende klassens vurderinger til undervisningsministeriets web API. Da der forekommer rigtigt mange kald til denne API, har man i udviklingsteamet besluttet, at alle kald til API’en skal kombineres med en retry logik, der kalder API’en op til 3 gange for svar, før man registrerer et fejlet kald.

Trin 1

Jeg starter igen med at oprette min klasse og min metode:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class EvaluationService
{
    private ILogService _logService;
    private IEducationMinistryApiHandler _educationMinistryApi;

    public EvaluationService(ILogService logService, IEducationMinistryApiHandler educationMinistryApi)
    {
        this._logService = logService;
        this._educationMinistryApi = educationMinistryApi;
    }

    public void SendClassEvaluation(ClassEvaluation classEvaluation)
    {

    }
}

Trin 2

Grænseværdier er nemmest at finde, når du tager udgangspunkt i partitioner, så lad os bruge ækvivalenspartitionering teknikken til at inddele vores krav i partitioner. For en retry logik med op til 3 kald får vi:

Gyldig
Ugyldig
1
2
3
4

Jf. ækvivalenspartitionering kunne vi tage én gyldig værdi og én ugyldig for at opnå en fornuftig testdækning. Da vi nu ved, at potentielle fejl ofte findes ved grænserne af vores partitioner, så lad os tage vores mindste og største gyldige værdi sammen med den mindste ugyldige værdi.


Gyldig
Ugyldig
1
2
3
4

Altså får jeg nedenstående unit tests:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
[TestClass]
public class Tests_EvaluationService
{
    [TestMethod]
    public void Tests_SendClassEvaluation_1_API_Call()
    {
        //Arrange
        ClassEvaluation classEvaluation = new ClassEvaluation
        {
            Class = "7b",
            StudentEvaluations = new List<string> { "Passed", "Passed", "Failed", "Passed" }
        };
            
        var mockLog = new Mock<ILogService>();

        var mockApi = new Mock<IEducationMinistryApiHandler>();
        HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.Accepted) { Content = new StringContent("") };
        mockApi.Setup(api => api.PostClassEvaluation(It.IsAny<ClassEvaluation>())).Returns(Task.FromResult<HttpResponseMessage>(responseMessage));

        int expectedNoOfApiCalls = 1;

        //Act
        EvaluationService evaluationService = new EvaluationService(mockLog.Object, mockApi.Object);
        evaluationService.SendClassEvaluation(classEvaluation);

        //Assert
        mockApi.Verify(api => api.PostClassEvaluation(classEvaluation), Times.Exactly(expectedNoOfApiCalls),
            "The API wasn't called the expected number of times.");
        mockApi.VerifyNoOtherCalls();

        mockLog.VerifyNoOtherCalls();
    }

    [TestMethod]
    public void Tests_SendClassEvaluation_3_API_Calls()
    {
        //Arrange
        ClassEvaluation classEvaluation = new ClassEvaluation
        {
            Class = "7b",
            StudentEvaluations = new List<string> { "Passed", "Passed", "Failed", "Passed" }
        };

        var mockLog = new Mock<ILogService>();

        var mockApi = new Mock<IEducationMinistryApiHandler>();

        HttpResponseMessage responseRequestTimeout1 = new HttpResponseMessage(HttpStatusCode.RequestTimeout) { Content = new StringContent("") };
        HttpResponseMessage responseRequestTimeout2 = new HttpResponseMessage(HttpStatusCode.RequestTimeout) { Content = new StringContent("") };
        HttpResponseMessage responseAccepted = new HttpResponseMessage(HttpStatusCode.Accepted) { Content = new StringContent("") };

        mockApi.SetupSequence(api => api.PostClassEvaluation(It.IsAny<ClassEvaluation>()))
            .Returns(Task.FromResult<HttpResponseMessage>(responseRequestTimeout1))
            .Returns(Task.FromResult<HttpResponseMessage>(responseRequestTimeout2))
            .Returns(Task.FromResult<HttpResponseMessage>(responseAccepted));

        int expectedNoOfApiCalls = 3;

        //Act
        EvaluationService evaluationService = new EvaluationService(mockLog.Object, mockApi.Object);
        evaluationService.SendClassEvaluation(classEvaluation);

        //Assert
        mockApi.Verify(api => api.PostClassEvaluation(classEvaluation), Times.Exactly(expectedNoOfApiCalls),
            "The API wasn't called the expected number of times.");
        mockApi.VerifyNoOtherCalls();

        mockLog.VerifyNoOtherCalls();
    }

    [TestMethod]
    public void Tests_SendClassEvaluation_4_API_Calls()
    {
        //Arrange
        ClassEvaluation classEvaluation = new ClassEvaluation
        {
            Class = "7b",
            StudentEvaluations = new List<string> { "Passed", "Passed", "Failed", "Passed" }
        };

        var mockLog = new Mock<ILogService>();

        var mockApi = new Mock<IEducationMinistryApiHandler>();

        HttpResponseMessage responseRequestTimeout1 = new HttpResponseMessage(HttpStatusCode.RequestTimeout) { Content = new StringContent("") };
        HttpResponseMessage responseRequestTimeout2 = new HttpResponseMessage(HttpStatusCode.RequestTimeout) { Content = new StringContent("") };
        HttpResponseMessage responseRequestTimeout3 = new HttpResponseMessage(HttpStatusCode.RequestTimeout) { Content = new StringContent("") };
        HttpResponseMessage responseRequestTimeout4 = new HttpResponseMessage(HttpStatusCode.RequestTimeout) { Content = new StringContent("") };

        mockApi.SetupSequence(api => api.PostClassEvaluation(It.IsAny<ClassEvaluation>()))
            .Returns(Task.FromResult<HttpResponseMessage>(responseRequestTimeout1))
            .Returns(Task.FromResult<HttpResponseMessage>(responseRequestTimeout2))
            .Returns(Task.FromResult<HttpResponseMessage>(responseRequestTimeout3))
            .Returns(Task.FromResult<HttpResponseMessage>(responseRequestTimeout4));

        int expectedNoOfApiCalls = 3;
        string expectedLogMessage = "Could not send class evaluation because of request timeout.";

        //Act
        EvaluationService evaluationService = new EvaluationService(mockLog.Object, mockApi.Object);
        evaluationService.SendClassEvaluation(classEvaluation);

        //Assert
        mockApi.Verify(api 8u=> api.PostClassEvaluation(classEvaluation), Times.Exactly(expectedNoOfApiCalls),
            "The API wasn't called the expected number of times.");
        mockApi.VerifyNoOtherCalls();

        mockLog.Verify(log => log.Log(expectedLogMessage), "The expected message wasn't logged.");
        mockLog.VerifyNoOtherCalls();
    }
}

Bemærk at jeg ved at bruge Moq kan bestemme hvilke HTTP status koder, som min metode skal håndtere i testen, og at jeg i stedet for Assert() kan bruge Verify() til at sikre, at min API handler kaldes med den forventede værdi og det forventede antal gange.

Trin 3

Da jeg kørte min unit test fejlede de som forventet. Herefter ville jeg kunne fortsatte med at implementere min metode SendClassEvaluation, indtil mine unit tests bestod og derefter lave refactoring af min kode.

Alt kode i denne serie af indlæg kan findes på min Github profil:

Fortsættes...

Del 13: Testdesignteknik: Grænseværdianalyse

Testdesignteknik: Ækvivalenspartitionering

Prøv at sige det hurtigt 5 gange! Det fornemme ord henviser til en logisk måde, hvorpå du kan inddele værdier i grupper. De fleste bruger den allerede uden at tænke over det. Når det så er sagt, så vil du opleve, at det bliver lettere at udvælge relevante input værdier, når du bevidst tager denne teknik i brug.
  Ækvivalenspartitionering handler som sagt om at opdele input i grupper (partitioner). Hver gruppe forventes at have samme opførsel og blive behandlet på samme måde i applikationen. Grupperne kan findes for:

  • Output værdier (indenfor/udenfor kreditgrænse)
  • Valide/invalide værdier (bestillingsmængder min/max)
  • Tidsrelaterede værdier (før, under og efter en opgave)
  • Sæt af muligheder (hårfarve, øjenfarve)

Når du designer test med denne teknik, anvender du mindst én værdi til at repræsentere hver partition. I teorien er én værdi fra hver partition nok, men der kan være tilfælde, hvor du af forretnings logiske grunde gerne vil anvende flere, eller når du arbejder med grænseværdianalyse, som vi kommer til senere.

When it all comes together...


Ex. Vurdering af elev ud fra karakter
Lad os tage et simpelt eksempel og sige, at du har en metode, der skal returnere, hvorvidt eleven er bestået eller ej ud fra hans eller hendes karakter. Vores forretningslogik siger, at karaktererne -3, 00 og 02 betyder at eleven er dumpet. 4, 7, 10 og 12 betyder, at eleven har bestået.
  Kravspecifikationen siger, at brugerne skal kunne angive en karakter og få en besked tilbage om, hvorvidt eleven er bestået eller ej. Ydermere blev man på det sidste udviklingsmøde enige om at logge alle karakterer med ugyldige værdier.

Trin 1

Først opretter jeg min klasse og min metode, der i første omgang kun skal returnere en tom streng:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class StudentEvaluation
{
    private ILogService _logService;

    public StudentEvaluation(ILogService logService)
    {
        this._logService = logService;
    }

    public string HasPassed(int grade)
    {
        return string.Empty;
    }
}

Trin 2

Uden at bruge nogen som helst teknik eller strategi kunne vi teste vores metode med alle syv karakterer, da det trods alt er et overskueligt antal. Men hvis vi fx havde 70 eller 700 karakterer ville det være en helt anden sag. Ækvivalenspartitionering kan bruges uanset om du har 7, 70 eller flere mulige input værdier. Hvis jeg skulle opdele de mulige input værdier i partitioner ville jeg sige, at jeg har fire partitioner: ‘bestået’, ‘ikke bestået’, ‘gyldig’ og ‘ugyldig’.

Ugyldig
Ikke bestået
Bestået
Ugyldig
-4
-3
00
02
4
7
10
12
13

Jeg skriver mine unit tests ud fra mine partitioner. For mig resulterer det i tre unit tests; ‘bestået’, ‘ikke bestået’ og ‘ugyldig’. Jeg laver bevidst ikke en unit test for partitionen ‘gyldig’, da denne er dækket af både ‘bestået’ og ‘ikke bestået’.
  Jeg bruger desuden AAA mønstret i mine unit tests. AAA mønstret (arrange, act og assert) hjælper dig med at strukturere dine unit tests og understøtter et godt testdesign. Evt. input værdier og startbetingelser placeres under arrange, og forventede resultater verificeres under assert.
  Bemærk også at jeg bruger Moq til at mocke min log komponent ved hjælp af det interface, som min log komponent implementerer. Det betyder, at teamets reelle log komponent aldrig kaldes, og at mine unit tests derfor aldrig vil skabe rod i vores log. Som en ekstra bonus kan jeg verificere, at log komponenten kun bliver kaldt, som jeg forventer det og kun med den forventede besked.

Altså får jeg nedenstående unit tests:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    [TestClass]
    public class Tests_StudentEvaluation
    {
        [TestMethod]
        public void Tests_HasPassed_Passed()
        {
            //Arrange
            int grade = 10;
            string expectedResult = "Passed";
            var mockLog = new Mock<ILogService>();

            //Act
            StudentEvaluation evaluation = new StudentEvaluation(mockLog.Object);
            string actualResult = evaluation.HasPassed(grade);

            //Assert
            Assert.IsTrue(expectedResult.Equals(actualResult), "The student didn't pass as expected.");
            mockLog.VerifyNoOtherCalls();
        }

        
        [TestMethod]
        public void Tests_HasPassed_Not_Passed()
        {
            //Arrange
            int grade = 0;
            string expectedResult = "Failed";
            var mockLog = new Mock<ILogService>();

            //Act
            StudentEvaluation evaluation = new StudentEvaluation(mockLog.Object);
            string actualResult = evaluation.HasPassed(grade);

            //Assert
            Assert.IsTrue(expectedResult.Equals(actualResult), "The student didn't fail as expected.");
            mockLog.VerifyNoOtherCalls();
        }

        
        [TestMethod]
        public void Tests_HasPassed_Invalid_Grade()
        {
            //Arrange
            int invalidGrade = 13;
            string expectedResult = string.Empty;
            var mockLog = new Mock<ILogService>();
            string expectedLogMessage = $"An attempt to evaluate the invalid grade {invalidGrade} was made.";

            //Act
            StudentEvaluation evaluation = new StudentEvaluation(mockLog.Object);
            string actualResult = evaluation.HasPassed(invalidGrade);

            //Assert
            Assert.IsTrue(expectedResult.Equals(actualResult), "Invalid grade wasn't handled as expected.");
            mockLog.Verify(log => log.Log(expectedLogMessage), "The expected message wasn't logged.");
            mockLog.VerifyNoOtherCalls();
        }
    }

Trin 3

Da jeg kørte min unit test fejlede de som forventet. Herefter ville jeg kunne fortsætte med at implementere min metode HasPassed, indtil mine unit tests bestod og derefter lave refactoring af min kode.

Alt kode i denne serie af indlæg kan findes på min Github profil:

Fortsættes...

Del 12: Testdesignteknik: Ækvivalenspartitionering

Mocking

Hvorfor bruge Moq

Hvis du tidligere har arbejdet med unit tests, fandt du sikkert hurtigt ud af, at det hurtigt bliver svært at skrive unit tests til alt andet end den mest simple kode. Fx kan det være svært at isolere den specifikke del af koden, som du ønsker at teste, når metoden du skal teste også indeholder kald til andre klasser og tilgår eksterne ressourcer som fx database servere og web services. Ikke alene besværliggør dette udviklingen af dine unit tests; det gør dig også bekymret for at skabe problemer i de eksterne systemer, når du kører dine tests.
  Moq kan hjælpe dig i alle disse scenarier. Mockede objekter kan hjælpe dig med at isolere den kode, som du ønsker at teste, og de kan hjælpe dig med at sikre, at de eksterne ressourcer overhovedet ikke kommer i spil. Derudover opfordrer brugen af mockede objekter dig til at bruge gode kode principper, der vil gøre din kode lettere at vedligeholde.

Forestil dig dette scenarie; du skal teste en metode, der indeholder kald til andre klasser, der måske yderligere kalder videre til andre klasser.


Dette gør det svært at overholde egenskaberne af en god unit test. Men det er også her, at mockede objekter kommer ind og gør det nemmere at teste den ønskede funktionalitet isoleret fra kald til andre klasser. Mockede objekter bruges til at fjerne disse afhængigheder og erstatte dem med falske kopier af afhængighederne.


Dette giver dig fuldstændig kontrol over, hvad de falske afhængigheder gør, og de falske afhængigheder kan spore interaktionen mellem den kode, som du tester og afhængighederne, så du med din test kan verificere, at alt sker efter hensigten.

Kom i gang med Moq her

Moq er et mocking framework til .NET platformen, og du kommer hurtigt i gang med brugen af mockede objekter i dine unit tests med dette link:

https://github.com/moq/moq4/wiki/Quickstart

Fortsættes...

Del 11: Mocking

Egenskaberne af en god unit test

  1. En unit test skal kun teste en lille del af din funktionalitet.
    • Hvis din unit tester mere end én ting, bliver det svært at forstå og vedligeholde testen. 

  2. En unit test skal altid fejle eller bestå
    • En unit test bør aldrig være inkonklusiv.

  3. En unit test skal kunne gentages.
    • Hvis din unit test består og derefter fejler uden at testen eller den afhængige kode ændres, så kan testen ikke gentages.

  4. En unit test skal kunne køres uafhængigt af rækkefølge og sammenhæng.
    • Din unit test må ikke være afhængig af en rækkefølge i dine tests og dens evne til at bestå må ikke være påvirket af andre tests eller eksterne ressourcer.

  5. En unit test skal være hurtig.
    • Den skal tage millisekunder at køre. Hvis den tager 1 sekund at køre, er den for langsom.

  6. En unit test skal være nem at sætte op.
    • Hvis du skal lave en masse kode bare for at sætte testen op og få den klar til at køre, bør der være en bedre måde at gøre tingene på.

Fortsættes...

Del 10: Egenskaberne af en god unit test

Guideline til dit testdesign

Test design er at skrive test cases og testdata, og en test case er skrevet til at dække en eller flere objekter/hændelser, der skal verificeres. En test case består typisk af:

  • Et sæt input værdier
  • Evt. startbetingelser
  • Forventede resultater
    • Forventede resultater skal defineres forud for test afviklingen.

Sandsynlige resultater kan tolkes som korrekte, når de faktisk er forkerte, hvis du ikke på forhånd har gjort dig klart, hvad det forventede resultat er. Lad os sige, at formålet med funktionaliteten er at gemme et objekt i en database med et sæt specifikke felter. Hvis du først definerer, hvad de felter skal være, når du har kørt testen, kan du være tilbøjelig til at vurdere de felter, der bliver gemt under testen som værende tilstrækkelige i stedet for at vurdere felterne ud fra kravspecifikationen.

Fortsættes...

Del 9: Guideline til dit testdesign

Guideline til din testanalyse

  1. Hvad er det vi skal teste?

  2. Hvilke elementer/hændelser kan verificeres af en test case?
    • Er det funktioner (fx beregne antal uger mellem to datoer)
    • Er det transaktioner (fx tilføje en ny kunde)
    • Er det kvalitetsegenskaber (performance, brugervenlighed)
    • Er det strukturer (menuer, modul hierarkier)
    • Osv.

  3. Hvor megen test er nok?
    • Den vurderede risiko spiller en stor rolle, når der skal vælges testdesignteknikker. Er elementet/hændelsen kritisk for den applikationens infrastruktur? Er den kritisk for forretningen/brugeren? Vil test af elementet/hændelsen tage så lang tid, at det kan blive kritisk for projektets deadline?

Fortsættes...

Del 8: Guideline til din testanalyse

TDD

Hvad er test-driven development? Jeg har læst og hørt flere forklaringer, heriblandt de nedenstående:

  • “Test-driven development er en avanceret brug at automatiserede unit tests, der skal drive designet af din software og gennemtvinge lav kobling. Resultatet af denne praksis er en omfattende samling af unit tests, der kan køres når som helst du ønsker en tilbagemelding på, om applikationen stadig virker efter hensigten.”

  • “Tænk på test-driven development som en måde, hvorpå du kan forvandle din kode til en instruktionsmanual for sig selv. Ved at skrive testene først laver du et detaljeret roadmap for, hvordan din applikation skal se ud for at leve op til kravene. Du lader dine tests fortælle dig, hvad det næste skridt er.”

I praksis foregår TDD som illustreret på nedenstående tegning:


Og hvis du ligesom mig har brug for en mere uddybende guideline til TDD i praksis, så kommer den her:
  1. Du opretter klassen (hvis den ikke allerede eksisterer) og tilføjer din nye metode til klassen med metodenavn, evt. parametre og en return type. Metodens krop lader du være tom, medmindre du har en return type. I det tilfælde skriver du en enkelt linje til at returnere null, -1, false eller hvad en fejlet værdi nu ellers ville komme til udtryk som. Du kan selvfølgelig også lade være med at skrive noget som helst, men så vil du få kompileringsfejl, når du prøver at debugge dine tests, og det er ikke disse fejl, som vi er interesserede i at få. 

  2. Du skriver de tests, som du mener er nødvendige for at verificere, at funktionaliteten i din metode lever op til kravspecifikationen. 

  3. Kør dine tests og se dem fejle.

  4. Skriv den kode, der er nødvendig for at testene består. Hold det enkelt og lad være med at tænke for meget over design og god kode skik (og ja, det sagde jeg lige!).

  5. Kør dine tests og se dem bestå.

  6. Refactor din kode og verificer, at den stadig lever op til kravene ved at køre dine tests.

  7. Gå videre til den næste metode/funktionalitet.

Note; du behøver ikke at begrænse din implementering til den ene metode. Hvis det giver mere mening at opdele den i flere metoder, der alle indgår i den første metodes flow, så er du mere end velkommen til at gøre det. Dine unit tests kigger på resultatet, ikke på hvordan du kom frem til det. 

Du opnår hermed flere fordele med dine tests:

  • Du tænker over, hvad din kode skal bruges til, før du begynder at implementere kravspecifikationen. Ikke nok med at du tester for fejl, du forebygger dem også.

  • Dine tests bliver ikke en træls opgave, der skal udføres efter du selv føler, at du har løst opgaven. De bliver den rettesnor, der fortæller dig, hvornår du er færdig med opgaven, og hver eneste grønne lampe bliver et win undervejs i implementeringen.

  • Bagefter har du en samling unit tests, som både du og andre til enhver tid kan køre for at verificere, at koden stadig overholder kravspecifikationen.

  • Sidst, men absolut ikke mindst, refactoring indgår som en naturlig del af din udvikling.

Fortsættes...

Del 7: TDD

Hvorfor unit testing?

Softwaretestere kalder unit testing for komponenttest, og hensigten for både udviklere og testere er også den samme; at teste en enkelt komponent. I praksis betyder dette som regel én specifik metode på én specifik klasse. Metoden kan indeholde metodekald til andre metoder, fx et database lag eller et 3. parts komponent, men mocking kan hjælpe dig med både at holde fokusset på den metode, som du ønsker at teste og samtidig verificere at andre metoder kaldes med de forventede værdier.

Unit testing er min personlige favorit, fordi den (foruden at være skrevet i kode og automatiseret) også sparer dig udviklingstid, når du skal debugge din kode. Med unit testing kan du hurtigt opstille præcis det scenarie, som du gerne vil tjekke, hvordan din kode håndterer. Hvis du ønsker at debugge koden, kan du derfor gøre det uden at skulle igennem resten af applikationen først. Dette sparer i sig selv mere udviklingstid end man skulle tro.

Det er dog vigtigt at huske, at unit testing er et værktøj, der automatiserer dine tests. Det er stadig dig, der skal analysere kravene til applikationen og designe relevante test cases ud fra dem.

Fortsættes...

Del 6: Hvorfor unit testing?

Generelle testprincipper

  1. Test viser tilstedeværelsen af fejl
    • Test finder fejl.
    • Test kan ikke vise, at der ikke er fejl, men test kan opbygge tillid til, at applikationen kan håndtere de scenarier, som vi forventer, kan forekomme.

  2. Udtømmende test er umuligt
    • Der er for mange mulige kombinationer og som oftest også begrænset tid og budget.

  3. Tidlig test
    • Testaktiviteter bør starte så tidligt som muligt i systemudviklingens cyklus og bør være fokuseret på definerede formål. Det er bedre at få forståelsen af kravene på plads, inden vi begynder at implementere dem.
    • Fejl fundet tidligt i processen er billigere at rette. En designfejl er fx langt billigere at rette, når du kun har modellen end efter du har implementeret modellen i din applikation. En fejl i forståelsen af kravspecifikationen eller i selve kravspecifikationen koster endnu mere, hvis den ikke fanges, inden udviklingen af applikationen starter. 

  4. Klynger af fejl
    • Driftsafvigelser og de fleste af de fejl, der er fundet ved test forud for frigivelsen ses typisk i den samme del af applikationen. Dette kan være og er ofte et mindre område, der af forretningsmæssige eller tekniske årsager er det mest problematiske. 

  5. Pesticid-paradokset
    • Test bliver “trætte” og ineffektive (når alle kendte fejl er rettet). Det er nødvendigt at opdatere gamle tests og lave nye for at finde flere fejl. Unit tests beholder dog deres værdi, da de samtidig fungerer som dokumentation for kravene og demonstrerer, at applikationen lever op til disse, selv når ny funktionalitet føjes til eller gammel kode omskrives under refactoring. Men hvis kravene opdateres, er det samme også nødvendigt for dine unit tests.

  6. Test er afhængig af sammenhængen
    • Her er vi igen tilbage til en risikovurdering. Sikkerhedskritiske systemer (bør) testes anderledes end forretningssystemer.

  7. Fravær-af-fejl-fejltagelsen 
    • At finde og rette fejl er kun en del af test.
    • Test skal sikre, at applikationen opfylder behov og forventninger. At der ikke er nogle fejl, betyder ikke, at applikationen vil blive accepteret.
    • Test er middel, det er ikke et mål!

Fortsættes...

Del 5: Generelle testprincipper

Forskellige synsvinkler - forskellige mål

Udviklingstest (komponent, integration og system)

  • Komponenttest er test af det enkelte komponent. Det vil for udviklere typisk være den enkelte metode, som vi tester ved hjælp af unittest(s).
  • Integrationstest er test af integrationen mellem delsystemer. Det kunne fx være kommunikationen mellem applikation og database eller mellem applikation og 3. parts service.
  • Systemtest er test af systemet som helhed, hvor du gerne vil verificere, at de forskellige delsystemer spiller sammen.
  • Under udviklingstest forebygger (vha. review af kravspecifikationer og modeller, og TDD), finder og retter du fejl.
  • Udviklingstest er udviklernes domæne, selvom vi nogle gange får hjælp af en 100% tester. Vores mål er løbende at sikre, at vi udvikler en applikation, der lever op til de angivne krav, og som kan håndtere de scenarier, som vi forventer, kan forekomme i den daglige brug af applikationen.

Accepttest

  • Her bekræftes det, at den færdige udgave af applikationen virker som den skal, og der opnås tillid til, at applikationen lever op til brugernes krav.
  • Accepttest kan udføres af en Produkt ejer, en bruger eller hvem der nu ellers har beføjelse (og ikke mindst forretningsviden) til at godkende den færdige applikation.

Vedligeholdelsestest

  • Her tjekker du, at der ikke er opstået nye fejl i forbindelse med rettelser og tilføjelse af ny funktionalitet.
  • Jeg gentager, unit tests er din partner-in-crime. Og det skal så vidt muligt være unit tests eller en anden form for automatiseret test, for det er virkeligt ikke sjovt at udføre de præcis samme tests for 20. gang!

Driftstest

  • Her vurderes applikationens pålidelighed og tilgængelighed. Driftstest kan fx udføres vha. performance test.

Fortsættes...

Del 4: Forskellige synsvinkler - forskellige mål

Formålet med test...

Er at finde fejl.

Jeg tager et vildt gæt og siger, at det formål kendte du nok i forvejen? Var du også klar over at formålet med test også inkluderer:
  • At påvise at kravene er overholdt
  • At opnå tillid til applikationen og dens kvalitet
  • At give en analyse af applikationen
  • At forebygge fejl
    • Når du først har lært at tage et test perspektiv på din applikation, vil du også bruge det ubevidst, når du læser kravspecifikation og skriver din kode.

Udvikler, kend din kode

Når du som udvikler tester din kode og dokumenterer disse test, så du og andre kan køre dem igen, opnår i alle et bedre kendskab til koden. Vi kan alle sammen læse og forstå kode, det er trods alt en vigtig del af vores arbejde. Hvad vores egen og andres kode ikke altid fortæller os er, hvorfor koden ser ud, som den gør. Hvad er formålet med den enkelte metode, klasse eller del af applikationen? Hvilke konsekvenser har det, hvis vi ændrer i netop de linjer kode? Vi kan selvfølgelig bruge timer på at læse og prøve at forstå koden i en applikation. Hvis vi er heldige, har vi en kollega eller to, der har arbejdet i så mange år med applikationen, at de kender formålet med det meste af dens kode. De ved, hvorfor koden ser ud, som den gør, og kan give et kvalificeret bud på, hvad konsekvenserne ved en ændring vil være. De kollegaer er som regel også tæt på uundværlige for virksomheden, og hele teamet står ofte med håret i postkassen, hvis de pludselig siger op og på en måned eller to skal overdrage års erfaring.

Formelle test (og med formelle test mener jeg planlagte og nedskrevne tests, som man kan køre på samme måde igen og igen) er et rigtigt godt værktøj til at hjælpe både dig og dine kollegaer med at forstå din kode. I formelle test angiver du et forventet input og output. Dvs. du dokumenterer, hvad du forventer, at din kode skal håndtere, og hvordan slutresultatet af den håndtering skal se ud. Du dokumenterer med andre ord formålet med din kode helt ned til den enkelte metode og beviser i samme kontekst, at din kode fungerer efter hensigten. Dette giver et bedre kendskab til din kode for både dig og dine kollegaer, og det giver hele teamet tillid til koden.

Fortsættes...

Del 1: Formålet med test

Hvorfor er test nødvendig?

  • Test reducerer risikoen for at der opstår problemer i driften
    • Som udvikler ved du allerede, at det er nemmere og hurtigere at rette fejl, som du selv eller dine kollegaer finder end alternativet. Nemlig at skulle håndtere sure henvendelser om fejl fra brugere, prøve at spore sig frem til hvad de mener med “den fejl der kommer, når jeg trykker på knappen nederst på skærmen” for først derefter at kunne rette fejlen.

  • Test bidrager til applikationens kvalitet
    • Når du skriver og udfører test, sætter du dig ind i kravene til applikationen og prøver så vidt muligt at sætte dig i brugerens sted. Det giver et andet perspektiv på applikationens funktionalitet og er med til at øge applikationens slutværdi, der jo desværre vurderes af brugerne, der ikke kan se al den fantastiske kode, der ligger lige under overfladen.
    • Kvaliteten forbedres, når defekter rettes (og gentestes - hvilket er grunden til at vi godt kan lide automatiserede test. Hvorfor udføre det samme arbejde igen, hvis det kan undgås?)
    • Hvis du tror på refactoring af din egen og andres kode, så er unit tests din partner-in-crime, da de er dokumentation for, at applikationen stadig fungerer, som den skal.

  • Test giver dig en status på applikationens helbred.
    • Et host nu og her er at forvente, men hvis halvdelen af dine tests fejler, hver gang du kører dem, skal applikationen sygemeldes og have en omgang penicillin. Og hvis din leder beder applikationen om at starte i arbejde for tidligt, så må han eller hun hellere forberede sig på en langtidssygemelding.

Fortsættes...

Del 2: Hvorfor er test nødvendig?