Behandelde onderwerpen • unit tests • asserts • Catch2
Het is een ‘fact of life’ dat software fouten (bugs) bevat. Het is natuurlijk een goed voornemen om geen fouten te maken tijdens het schrijven, maar dat zal het aantal fouten nooit tot nul reduceren. Uit onderzoek is gebleken dat het herstellen van een fout (veel) duurder* is in latere fasen van de ontwikkeling van een applicatie, dus het is verstandig fouten zo vroeg mogelijk te vinden. Eén reden is dat het verbeteren van een fout vaak een nieuwe fout introduceert, en als je pech hebt meer dan één.
Voor technische software geldt dit misschien nog wel sterker dan voor andere software: als een bug pas tijdens het gebruik van een kerncentrale aan het licht komt is er misschien geen centrale meer om je gewijzigde software te gebruiken. (Als je dichtbij woonde is er misschien ook geen ‘jij’ meer om die wijziging te maken.)
Het is dus verstandig software zo vroeg mogelijk te testen. Er is zelfs een ontwikkelmethode (Test Driven Development) die er op is gebaseerd dat je pas code mag schrijven of wijzigen als je eerst een test hebt geschreven die (met de code die je tot nu toe hebt) faalt.
De kleinste eenheden code die we schrijven worden units genoemd, en het testen daarvan dus unit testing.
Een unit test heeft meestal een simpel patroon:
Neem als voorbeeld de operator+
voor twee vectoren.
Om te checken of aan een bepaalde voorwaarde is voldaan kun je assert()
gebruiken.
NDEBUG
definieert) dan worden alle asserts genegeerd.
Sommige mensen vergelijken dit met een fabriek die veiligheidsriemen en airbags plaatst in testauto’s, maar niet in de productie-modellen.Richt je test zo in dat het waarschijnlijk is dat realistische fouten in de te testen code ook aan het licht komen. Als je alleen test dat (0,0) + (0,0)
gelijk is aan (0,0)
dan zijn er voor de hand liggende fouten die je niet zult ontdekken.
De onderstaande test is al wat beter, maar wat zou deze test niet ontdekken en wat zou jij er aan veranderen om dat wel te ontdekken?
#include <assert.h>
vector a( 1, 2 );
vector b( 4, 3 );
vector c = a + b;
assert( a.x == 1 ); assert( a.y == 2 );
assert( b.x == 4 ); assert( b.y == 3 );
assert( c.x == 5 ); assert( c.y == 5 );
Codevoorbeeld 09-01 - Recht-toe-recht aan test van vector::operator+
De operator+
test bevat een reeks asserts op de attributen van een vector. Je kunt het jezelf (en je lezer) wat makkelijker maken door een operator==
te definiëren voor vector en die te gebruiken. Maar dan moet je die operator==
natuurlijk wel testen!
bool operator==( const vector &lhs, const vector &rhs ){
return ( lhs.x == rhs.x ) && ( lhs.y == rhs.y );
}
vector a( 1, 2 );
vector b( 4, 3 );
vector c = a + b;
assert( a == vector( 1, 2 ) );
assert( b == vector( 4, 3 ) );
assert( c == vector( 5, 5 ) );
Codevoorbeeld 09-02 - Testen met gebruik van operator==
Als één van de tests faalt krijg je een foutmelding, maar die vermeldt alleen dat een assert faalde op een bepaald regelnummer.
Het zou best handig zijn als de bovenstaande foutmelding ook aangaf wat de beide waarden zijn. Je kunt zelf een macro schrijven die dat doet, maar dat werk is natuurlijk al lang een keer gedaan.
Een library die zoiets (en vaak nog veel meer) voor je doet heet een ‘unit test framework’. Wij gebruiken een heel simpel unit test framework: Catch2.
Een eenvoudige Catch2 applicatie bevat
TEST_CASE
’sREQUIRE
.Een main is meestal niet nodig: de main die de header catch.hpp
levert als CATCH_CONFIG_MAIN
is gedefinieerd, is doorgaans voldoende.
Een TEST_CASE
leest als een soort functie definitie, maar op de plaats van de parameter lijst staat een string literal die de test identificeert.
#include "ostream"
#include "vector.hpp"
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
bool operator==( vector lhs, vector rhs ){
return ( lhs.x == rhs.x ) && ( lhs.y == rhs.y );
}
std::ostream & operator<<( std::ostream & lhs, vector rhs ){
return lhs << "(" << rhs.x << "," << rhs.y << ")";
}
TEST_CASE( "constructors, two_parameters" ){
vector v( 3, 4 );
REQUIRE( v.x == 3 );
REQUIRE( v.y == 4 );
}
TEST_CASE( "constructors, default" ){
vector v;
REQUIRE( v == vector( 0, 0 ) );
}
Codevoorbeeld 09-03 - Een eenvoudige Catch applicatie
De uitvoer van Catch voor een run waarin geen fouten zijn gevonden vermeldt het aantal assertions en het aantal tests dat is uitgevoerd.
Als een assertion faalt dan wordt dit gemeld.
TEST_CASE( "constructors, default" ){
vector v;
REQUIRE( v == vector( 1, 7 ) );
}
Codevoorbeeld 09-04 - Een test case die een fout zal opleveren
Er wordt nu gemeld dat er een fout is opgetreden en waar, maar het zou nog handiger zijn als we ook zouden zien welke waarden er aan beide kanten van de operator==
staan. Dit kan Catch2 voor je doen, maar dan moet er voor die waarden wel een operator<<
bestaan.
std::ostream & operator<<( std::ostream & lhs, vector pos ){
lhs << "(" << pos.x << "," << pos.y << ")";
return lhs;
}
TEST_CASE( "constructors, default" ){
vector v;
REQUIRE( v == vector( 1, 7 ) );
}
Codevoorbeeld 09-05 - Falende test case met een operator<<
Tot nu toe hebben we operator<<
alleen gebruikt om te schrijven naar de standaard uitvoer.
Om een operator<<
te testen is het handig om te weten dat je die via een omweg ook kunt gebruiken om naar een string
te schrijven, want dan kun je na afloop de inhoud van die string checken. Dit doe je door naar een (lege) std::stringstream
te printen, en daarna de waarde van die stringstream
op te vragen met de s.str()
methode.
TEST_CASE( "operator<<" ){
std::stringstream s;
vector v( 1, 2 );
s << v;
REQUIRE( s.str() == "(1,2)" );
}
Codevoorbeeld 09-06 - test de operator<<
door middel van std::stringstream