Skip to article frontmatterSkip to article content

Comment tester le code ?

IUT d'Orsay, Université Paris-Saclay

Tests unitaires

Vous apprendrez à utiliser des frameworks de test (comme JUnit en Java) en S4.
Pour l’instant, nous écrirons des tests manuellement.

calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H

class Calculator {
public:
    static int factorial(int number);
    static double divide(double numerator, double denominator); 
};

#endif
calculator-test.h
#ifndef CALCULATOR_TEST_H
#define CALCULATOR_TEST_H

class CalculatorTest {
public :
    static void runTests();

private :
    static void factorial_ZeroInput_ReturnsOne();
    static void factorial_PositiveInput_ReturnsFactorial();
    static void factorial_NegativeInput_ThrowsInvalidArgument();
    static void divide_NonZeroDenominator_ReturnsDivision();
    static void divide_ZeroDenominator_ThrowsInvalidArgument();
};

#endif

Convention générale de nommage :

object-test.h
#ifndef OBJECT_TEST_H
#define OBJECT_TEST_H

// Same name as the class, with Test added at the end
class ObjectTest {
public :
    static void runTests(); // Method which calls all private tests
private :
    // Tests should be:
    // - static (does not depend on an instance),
    // - void (have no return value),
    // - and take no arguments.
    static void firstMethod_StateUnderTest_ExpectedBehavior();
    static void secondMethod_StateUnderTest_ExpectedBehavior();

    // There are no attributes
};

#endif

Exemple de tests :

calculator-test.cpp
#include "calculator.h"
#include "calculator-test.h"
#include <iostream>
#include <cassert>
#include <stdexcept>

void CalculatorTest::runTests() {
    factorial_ZeroInput_ReturnsOne();
    factorial_PositiveInput_ReturnsFactorial();
    factorial_NegativeInput_ThrowsInvalidArgument();
    divide_NonZeroDenominator_ReturnsDivision();
    divide_ZeroDenominator_ThrowsInvalidArgument();
    std::cout << "All tests passed\n";
}

void CalculatorTest::factorial_ZeroInput_ReturnsOne() {
    assert(Calculator::factorial(0) == 1);
    std::cout << "factorial_ZeroInput_ReturnsOne passed\n";
}

void CalculatorTest::factorial_PositiveInput_ReturnsFactorial() {
    assert(Calculator::factorial(1) == 1);
    assert(Calculator::factorial(2) == 2);
    assert(Calculator::factorial(3) == 6);
    assert(Calculator::factorial(4) == 24);
    assert(Calculator::factorial(5) == 120);
    assert(Calculator::factorial(10) == 3628800);
    std::cout << "factorial_PositiveInput_ReturnsFactorial passed\n";
}

void CalculatorTest::factorial_NegativeInput_ThrowsInvalidArgument() {
    try {
        Calculator::factorial(-1);
        assert(false); // Should not reach this line
    } catch (const std::invalid_argument& error) {
        std::cout << "factorial_NegativeInput_ThrowsInvalidArgument passed\n";
    } catch (...) {
        assert(false); // Catch unexpected exceptions
    }
}

void CalculatorTest::divide_NonZeroDenominator_ReturnsDivision() {
    assert(Calculator::divide(100, 5) == 20.0);
    assert(Calculator::divide(45, 9) == 5.0);
    assert(Calculator::divide(-25, 5) == -5.0);
    assert(Calculator::divide(7, -2) == -3.5);
    assert(Calculator::divide(-36, -6) == 6.0);
    assert(Calculator::divide(0, 3) == 0.0);
    std::cout << "divide_NonZeroDenominator_ReturnsDivision passed\n";
}

void CalculatorTest::divide_ZeroDenominator_ThrowsInvalidArgument() {
    try {
        Calculator::divide(10, 0);
        assert(false); // Should not reach this line
    } catch (const std::invalid_argument& error) {
        std::cout << "divide_ZeroDenominator_ThrowsInvalidArgument passed\n";
    } catch (...) {
        assert(false); // Catch unexpected exceptions
    }
}
main.cpp
#include "calculator-test.h"

int main() {
    CalculatorTest::runTests();
    return 0;
}

Test Driven Development (TDD)

Test Driven Development

Le cycle de développement du TDD (Credits: Medium.com).

Un exemple qui illustre bien ce cycle de développement !

Principes du test propre

void StringProcessorTest::reverse_MultipleScenarios_ReturnsVariousResults() {
    assert(StringProcessor::reverse("hello") == "olleh"); // Normal case
    assert(StringProcessor::reverse("") == "");  // Empty string
    assert(StringProcessor::reverse("a") == "a");  // Single character
    assert(StringProcessor::reverse("racecar") == "racecar");  // Palindrome
    assert(StringProcessor::reverse("12345") == "54321");  // Numeric string
    std::cout << "reverse_MultipleScenarios_ReturnsVariousResults passed\n";
}
void LogTest::produceComplexLog_SomeArgument_ReturnsSomeComplexLog() {
    ArgumentType someArgument;
    // Code defining someArgument
    //...
    Log::produceComplexLog(someArgument);
}
void SomeClassTest::someFunction_SomeArgument_ReturnsTrueOnMondayOnly () {
    auto currentTime = chrono::system_clock::now();
    auto currentDay = chrono::weekday(currentTime);
    if (currentDay == chrono::Monday)
        assert(SomeClass::someFunction(currentDay));
    else
        assert(!SomeClass::someFunction(currentDay));
    std::cout << "someFunction_SomeArgument_ReturnsTrueOnMondayOnly passed\n";
}
// In the header
class SomeClassTest{
private :
    int someValue = 10;
//...
}

// In the test code

void SomeClassTest::getSomeValue_IncrementValue_Returns11() {
    someValue++;
    assert(SomeClass::getSomeValue() == 11);
    std::cout << "getSomeValue_IncrementValue_Returns11 passed\n";
}

void SomeClassTest::getSomeValue_OriginalValue_Returns10() {
    assert(SomeClass::getSomeValue() == 10);
    std::cout << "getSomeValue_OriginalValue_Returns10 passed\n";
}
// Don't run unless you have some time to kill
void ParserTest::parseFile_ReallyBigFile_ReturnsCorrectString() {
    //...
}