Inleiding Software Engineering Unit Testing, Contracten, Debugger 13 Februari 2014 Beknopte info over Unit Testing en Contracten kan je vinden op het einde van dit document. Eclipse beschikt over een handige debug-perspective. Maak voor dit practicum gebruik van de debug functionaliteit van Eclipse. Google C++ Testing Framework is een framework dat je in staat stelt om je code efficiënt te testen. Er bestaat geen plugin voor Eclipse voor het Google C++ Testing Framework, maar via een omweg je kan je het integreren in Eclipse. We zullen ook gebruik van maken van eenvoudige macroʼs om het werken met contracten aan te leren. Oefening 1: Installeer Het Testing Framework Volg de stappen beschreven in SE1BAC - Laad TicTacToe in Eclipse, om je testing framework op te zetten en een nieuw project aan te maken. http://ansymo.ua.ac.be/inleiding-software-engineering-1e-bac/studiemateriaal/se1baclaad-tictactoe-eclipse Volg volgende tutorial: http://www.ibm.com/developerworks/aix/library/augoogletestingframework.html?ca=drs-#list1 Oefening 2: Wat zou je testen? Lees eerst de uitleg in Appendix a over Unit Testing. a) Beschouw de volgende functie: int largest(vector<int> list){ /* code */ } Schrijf minstens 5 testen in het Google Test framework voor deze functie. Denk aan Algemene correctheid, Rand gevallen en Fouten. Beschouw volgende (foute) implementatie van bovenstaande functie. Hoeveel van de testen die je hiervoor hebt geschreven falen hierop? In Appendix c kan je een implementatie vinden die beter werkt int largest(vector<int> list){ int max = numeric_limits<int>::max(); for(int i = 0; i < list.size() - 1; i++){ if(list[i] > max){ max = list[i]; } } return max; }
b) Beschouw de volgende klasse: Schrijf testen in het Google Test framework voor iedere methode van deze klasse. // // Fighterplane.cpp // // Created by Quinten Soetens on 13/02/14. // Copyright (c) 2014 Quinten Soetens. All rights reserved. // #include <fstream> class Fighterplane{ private: std::string callsign; int x; //positional coordinate int y; //positional coordinate int z; //positional coordinate int ammo; //keeps track of the number of missiles int health; //the remaining hitpoints the plane still has. When this reaches 0 the plane is dead public: //Default constructor will place a new plane with no name at location (0,0,0) Fighterplane() { this->callsign = ""; this->x = 0; this->y = 0; this->z = 0; } //constructor to set a plane on a certain location. //ammo and healt are automaticalle initialized to 10 and 20 respectively Fighterplane(std::string callsign, int x, int y, int z) : ammo(10), health(20){ this->callsign = callsign; this->x = x; this->y = y; this->z = z; } std::string getcallsign(){ return callsign; } //Getter to obtain positional coordinate int getcoords(){ return x; } //Getter to obtain positional coordinate int gety(){ return y; } //Getter to obtain positional coordinate int getz(){ return z;
} //Method that allows a plane to move to a new location. void moveto(int x, int y, int z){ this->x += x; this->y += y; this->z += z; } //Method to attack another plane. void attackplane(fighterplane* target){ this->ammo -= 1; target->health -= 5; } //Prints all the information about this fighterplane to a file. void printtofile(std::string filename){ std::ofstream outfile; outfile.open (filename); outfile << "Information about Fighterplane: \n"; outfile << "Callsign: " + callsign + "\n"; outfile << "Position: (" << x << "," << y << "," << z << ")\n"; outfile << "Health: " << health << "\n"; outfile << "Remaining ammo: " << ammo << "\n"; outfile.close(); } }; Oefening 3: Pre- en Post Condities Lees de uitleg in Appendix b over Contracten a) Beschouw de functie uit oefening 2a: Bedenk geschikte pre- en postcondities voor deze functie. b) Beschouw de klasse in Oefening 2b: Bedenk geschikte contracten (pre- en postcondities) voor iedere methode in die klasse. Gebruik de header file: http://ansymo.ua.ac.be/system/files/uploads/courses/se1bac/ DesignByContract.h om precondities (REQUIRE) en postcondities (ENSURE) te implementeren.
Appendix a: Unit Testing Software Testing heeft als doel het evalueren van een bepaalde eigenschap of functionaliteit van een programma of een systeem en te verifiëren dat dit overeenkomt met het verwachtte resultaat. Er zijn verschillende soorten van Software Testing: Unit Testing Verifieert de functionaliteit van een specifiek deeltje code, meestal op een enkele klasse, of een enkele methode. Integration Testing Testen van de samenwerking van enkele modules. System Testing Testen van het volledig geïntegreerde systeem. System Integration Testing Testen van de samenwerking tussen verschillende systemen. Een unit test is code die een heel klein specifiek gedeelte van de functionaliteit van een systeem gaat uittesten. In de meeste gevallen gaat een test één methode uitvoeren in een bepaalde context. Dit wil natuurlijk niet zeggen dat elke methode maar door één test mag getest worden. Neen, je gaat verschillende testen schrijven die eenzelfde methode oproepen in verschillende contexten. Een goede Unit Test is: Herhaalbaar Elke keer dat de test wordt uitgevoerd, moet deze hetzelfde resultaat bekomen. (vermijd randomness, het gebruik van current time, etc ) Onafhankelijk Test één methode per keer. Verschillende testen mogen niet afhankelijk zijn van elkaar. Waardevol Het testen van eenvoudige getter/setter methodes is niet echt waardevol. Grondig Test ALLE pre/post condities, randgevallen, Om grondig te testen, moet je een aantal dingen nakijken: Algemene correctheid Dit zijn meestal de meest eenvoudige testen om te schrijven. Deze testen het algemeen goed gedrag van de use cases. Rand gevallen Orde? Heeft een andere volgorde een effect op het resultaat? Bereik? Hoe reageert het op nul, het minimum, het maximum, positieve waarden, negatieve waarden,.. Bestaat het? Wat als je een nullptr meegeeft als parameter? Wat met lege verzamelingen (lege array, lege list, lege vector, ), wat met lege Strings? Kardinaliteit? Wat is het verwachtte aantal items? Fouten Worden de juiste foutboodschappen gegeven? Wat met I/O issues, zoals ontbrekende bestanden, onleesbare of lege bestanden?
Appendix b: Contracten Contracten worden gebruikt om jezelf ervan te verzekeren dat je software juist is opgebouwd. Het is een manier om formeel te documenteren welke functionaliteit een bepaalde functie of methode heeft. Je kan dit programmatisch vast leggen met behulp van asserts (of in ons geval met de macro s ENSURE en REQUIRE, gedefinieerd in DesignByContract.h). We gebruiken REQUIRE voor precondities en ENSURE voor postcondities. REQUIRE legt vast in welke staat het systeem zich moet bevinden alvorens deze functie of methode uit te voeren. ENSURE legt vast hoe de staat van het systeem verandert zal zijn na het uitvoeren van deze functie of methode. Het is de taak van de code die de functie of methode oproept om ervoor te zorgen dat het systeem zich in de juiste staat (zoals beschreven in de preconditie) bevindt. Je moet dus in de functie of methode zelf geen extra controle s meer implementeren en foutboodschappen genereren, wanneer het vanuit de verkeerde toestand wordt opgeroepen. Deze controle s en foutboodschappen moeten dus in de oproepende kant worden geïmplementeerd. Elke kant van een methode/functie oproep haalt voordelen uit een contract, maar moet wel zijn verplichtingen nakomen: Contract Voordelen Verplichtingen Methode of functie met pre- en postcondities (de PROVIDER) Code die de provider oproept (de CLIENT) - Geen nood om de unput waarden na te kijken. - De input voldoet gegarandeerd aan de preconditie. - Geen nood om de output waarden na te kijken. - Het resultaat voldoet gegarandeerd aan de postconditie. Moet er voor zorgen dat de postconditie voldaan is. Moet er voor zorgen dat aan de precondities van de Provider voldaan wordt.
Appendix c: Een betere implementatie voor de functie largest #include <exception> #include <vector> using namespace std; class IllegalArgException: public exception{ const char* message; public: IllegalArgException(const char * message){ this->message = message; } virtual const char* what() const throw(){ return message; } }; int largest2(vector<int>* list) { if(list == nullptr) { throw new IllegalArgException("list cannot be nullptr"); }else if(list->empty()){ throw new IllegalArgException("list cannot be empty"); } int max = numeric_limits<int>::min(); for(int i = 0; i < list->size(); i++){ if(list->at(i) > max){ max = list->at(i); } } return max; }