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

Ingen kommentarer:

Send en kommentar