Créez un répertoire
TP13/, ainsi que les répertoires et fichiers suivants.
TP13/
├── include/
│ ├── student.h
│ ├── grade-book.h
│ ├── input-reader.h
│ └── application.h
├── source/
│ ├── student.cpp
│ ├── grade-book.cpp
│ ├── input-reader.cpp
│ ├── application.cpp
│ └── main.cpp
├── tests/
│ ├── include/
│ | ├── student-test.h
│ | └── grade-book-test.h
| └── source/
| ├── student-test.cpp
| ├── grade-book-test.cpp
│ └── main.cpp
├── compile_flags.txt
└── makefileRemplissez
TP13/include/avec le contenu suivant.
#ifndef STUDENT_H
#define STUDENT_H
#include <string>
#include <vector>
class Student {
private:
std::string mName;
std::vector<double> mGrades;
public:
Student(const std::string& name);
std::string getName() const;
void addGrade(double grade);
double getAverage() const;
};
#endif#ifndef GRADE_BOOK_H
#define GRADE_BOOK_H
#include "student.h"
#include <vector>
#include <string>
class GradeBook {
private:
std::vector<Student> mStudents;
public:
void addStudent(const std::string& name);
Student& getStudent(const std::string& name);
};
#endif#ifndef INPUT_READER_H
#define INPUT_READER_H
#include <string>
class InputReader {
public:
static int requestMenuChoice();
static std::string requestStudentName();
static double requestGrade();
};
#endif#ifndef APPLICATION_H
#define APPLICATION_H
#include "grade-book.h"
class Application
{
public:
void run();
private:
GradeBook mGradeBook;
static const int ADD_STUDENT_CHOICE = 1;
static const int ADD_GRADE_TO_STUDENT_CHOICE = 2;
static const int SHOW_STUDENT_AVERAGE_CHOICE = 3;
static const int EXIT_CHOICE = 4;
void displayMenu() const;
void addStudentToGradeBook();
void addGradeToStudent();
void showStudentAverage();
};
#endifQue fait cette application ?
Une première vue des déclarations nous indique que deux objets principaux sont manipulés ici : Student et GradeBook.
Studentcontient un nom et une liste de notes. Nous pouvons définir unStudentavec un nom, lui ajouter des notes avecaddGradeet obtenir sa moyenne avecgetAverage.GradeBookcontient une liste deStudent. Nous pouvons ajouter unStudentavecaddStudentet récupérer unStudentavecgetStudent.
L’Application affiche un menu et nous offre 4 choix :
Ajouter un
StudentauGradeBook.Ajouter une note à un
Student.Afficher la moyenne d’un
Student.Quitter.
InputReader nous aide à obtenir des entrées de l’utilisateur avec requestMenuChoice, requestStudentName et requestGrade.
Remplissez
TP13/source/avec le contenu suivant.
#include "student.h"
#include <string>
#include <vector>
Student::Student(const std::string& name) : mName(name) {}
std::string Student::getName() const {
return mName;
}
void Student::addGrade(double grade) {
mGrades.push_back(grade);
}
double Student::getAverage() const {
double sum = 0.0;
for (double grade : mGrades) {
sum += grade;
}
return sum / mGrades.size();
}#include "grade-book.h"
#include <string>
#include <vector>
void GradeBook::addStudent(const std::string& name) {
mStudents.push_back(Student(name));
}
Student& GradeBook::getStudent(const std::string& name) {
int index = 0;
while (index < mStudents.size() && mStudents[index].getName() != name) {
index++;
}
return mStudents[index];
}#include "input-reader.h"
#include <string>
#include <iostream>
int InputReader::requestMenuChoice() {
std::cout << "Menu choice: ";
int input;
std::cin >> input;
return input;
}
std::string InputReader::requestStudentName() {
std::cout << "Student name: ";
std::string input;
std::cin >> input;
return input;
}
double InputReader::requestGrade() {
std::cout << "Grade: ";
double input;
std::cin >> input;
return input;
}#include "application.h"
#include "input-reader.h"
#include "student.h"
#include "grade-book.h"
#include <iostream>
#include <string>
void Application::run() {
int choice = 0;
while (choice != EXIT_CHOICE) {
displayMenu();
choice = InputReader::requestMenuChoice();
switch (choice) {
case ADD_STUDENT_CHOICE:
addStudentToGradeBook();
break;
case ADD_GRADE_TO_STUDENT_CHOICE:
addGradeToStudent();
break;
case SHOW_STUDENT_AVERAGE_CHOICE:
showStudentAverage();
break;
case EXIT_CHOICE:
break;
}
}
}
void Application::displayMenu() const {
std::cout << std::endl;
std::cout << "1. Add student" << std::endl;
std::cout << "2. Add grade to student" << std::endl;
std::cout << "3. Show student average" << std::endl;
std::cout << "4. Exit" << std::endl;
}
void Application::addStudentToGradeBook() {
std::string name = InputReader::requestStudentName();
mGradeBook.addStudent(name);
std::cout << "Student added." << std::endl;
}
void Application::addGradeToStudent() {
std::string name = InputReader::requestStudentName();
Student& student = mGradeBook.getStudent(name);
double grade = InputReader::requestGrade();
student.addGrade(grade);
std::cout << "Grade added." << std::endl;
}
void Application::showStudentAverage() {
std::string name = InputReader::requestStudentName();
Student& student = mGradeBook.getStudent(name);
std::cout << student.getName() << "'s average: " << student.getAverage() << std::endl;
}#include "application.h"
int main() {
Application application;
application.run();
return 0;
}Est-ce que cette application est robuste ?
Nous pouvons déjà observer plusieurs problèmes avec cette application :
getAveragedeStudentne prend pas en compte le cas oùmGradesest vide et risque de provoquer une division par zéro.getStudentdeGradeBookne prend pas en compte le cas où aucunStudentn’a le nom recherché et risque de provoquer une erreur de segmentation (crash du programme quand on tente d’accéder à une mémoire non allouée, par exemple avecmStudents[index]lorsqueindex >= mStudents.size()).InputReaderne valide pas les entrées, ce qui peut provoquer des erreurs et entraîner des comportements inattendus.
Remplissez
TP13/tests/include/etTP13/tests/source/avec le contenu suivant.
#ifndef STUDENT_TEST_H
#define STUDENT_TEST_H
class StudentTest {
public:
static void runTests();
private:
static void getAverage_NoGrades_ThrowsRuntimeError();
};
#endif#ifndef GRADE_BOOK_TEST_H
#define GRADE_BOOK_TEST_H
class GradeBookTest {
public:
static void runTests();
private:
static void getStudent_StudentNotFound_ThrowsRuntimeError();
};
#endif#include "student.h"
#include "student-test.h"
#include <cassert>
#include <stdexcept>
#include <iostream>
void StudentTest::runTests() {
getAverage_NoGrades_ThrowsRuntimeError();
std::cout << "All Student tests passed\n";
}
void StudentTest::getAverage_NoGrades_ThrowsRuntimeError() {
Student student = Student("A");
try {
student.getAverage();
assert(false);
} catch (const std::runtime_error&) {
std::cout << "getAverage_NoGrades_ThrowsRuntimeError passed\n";
} catch (...) {
assert(false);
}
}#include "grade-book.h"
#include "grade-book-test.h"
#include <cassert>
#include <stdexcept>
#include <iostream>
void GradeBookTest::runTests() {
getStudent_StudentNotFound_ThrowsRuntimeError();
std::cout << "All GradeBook tests passed\n";
}
void GradeBookTest::getStudent_StudentNotFound_ThrowsRuntimeError() {
GradeBook gradeBook;
try {
gradeBook.getStudent("A");
assert(false);
} catch (const std::runtime_error&) {
std::cout << "getStudent_StudentNotFound_ThrowsRuntimeError passed\n";
} catch (...) {
assert(false);
}
}#include "grade-book-test.h"
#include "student-test.h"
int main() {
GradeBookTest::runTests();
StudentTest::runTests();
return 0;
}Que font ces tests ?
Ici, nous testons seulement les deux éléments suivants :
getAverage_NoGrades_ThrowsRuntimeErrorqui vérifie quegetAveragedeStudentlance unestd::runtime_errorlorsquemGradesest vide.getStudent_StudentNotFound_ThrowsRuntimeErrorqui vérifie quegetStudentdeGradeBooklance unestd::runtime_errorlorsqu’aucunStudentn’a le nom recherché. Ces tests ne doivent pas passer pour le moment.
Comment ce test fonctionne-t-il ?
GradeBook gradeBook;
try {
// gradeBook vient d'être défini et ne contient aucun `Student`
gradeBook.getStudent("A"); // cette ligne doit `throw` une `std::runtime_error`
assert(false); // si nous arrivons ici, alors la ligne précédente n'a pas interrompu l'exécution avec `throw`
} catch (const std::runtime_error&) { // nous attrapons uniquement les `std::runtime_error`
std::cout << "getStudent_StudentNotFound_ThrowsRuntimeError passed\n"; // si nous avons attrapé une `std::runtime_error`, alors le test est passé
} catch (...) { // si nous attrapons une autre erreur,
assert(false); // alors le test a échoué
}Remplissez
compile_flags.txtetmakefileavec le contenu suivant.
-xc++
-std=c++17
-Iinclude
-Itests/includeEXECUTABLE = gradebook-app
TEST_EXECUTABLE = gradebook-app-tests
CXX = g++
CXXFLAGS = -std=c++17 -Iinclude
TEST_CXXFLAGS = -std=c++17 -Iinclude -Itests/include
DEBUG_FLAGS = -g -O0
SOURCES = $(wildcard source/*.cpp)
TEST_SOURCES = $(wildcard tests/source/*.cpp)
OBJECTS = $(patsubst source/%.cpp,build/objects/%.o,$(SOURCES))
TEST_OBJECTS = $(patsubst tests/source/%.cpp,build/tests/objects/%.o,$(TEST_SOURCES))
DEPENDENCIES = $(patsubst source/%.cpp,build/dependencies/%.d,$(SOURCES))
TEST_DEPENDENCIES = $(patsubst tests/source/%.cpp,build/tests/dependencies/%.d,$(TEST_SOURCES))
SRC_OBJECTS_NO_MAIN = $(filter-out build/objects/main.o,$(OBJECTS))
all: build/binaries/$(EXECUTABLE)
debug: clean
$(MAKE) all CXXFLAGS="$(CXXFLAGS) $(DEBUG_FLAGS)"
build:
mkdir -p build/objects build/dependencies build/binaries build/tests/objects build/tests/dependencies build/tests/binaries
build/binaries/$(EXECUTABLE): $(OBJECTS) | build
$(CXX) $(OBJECTS) -o build/binaries/$(EXECUTABLE)
build/tests/binaries/$(TEST_EXECUTABLE): $(SRC_OBJECTS_NO_MAIN) $(TEST_OBJECTS) | build
$(CXX) $(SRC_OBJECTS_NO_MAIN) $(TEST_OBJECTS) -o build/tests/binaries/$(TEST_EXECUTABLE)
build/objects/%.o: source/%.cpp | build
$(CXX) $(CXXFLAGS) -MMD -MF build/dependencies/$*.d -c $< -o $@
build/tests/objects/%.o: tests/source/%.cpp | build
$(CXX) $(TEST_CXXFLAGS) -MMD -MF build/tests/dependencies/$*.d -c $< -o $@
clean:
rm -rf build
run:
./build/binaries/$(EXECUTABLE)
tests: build/tests/binaries/$(TEST_EXECUTABLE)
./build/tests/binaries/$(TEST_EXECUTABLE)
.PHONY: all build clean run tests
-include $(DEPENDENCIES)
-include $(TEST_DEPENDENCIES)Compilez et exécutez le code pour observer son fonctionnement.
Ctrl + C pour arrêter l’exécution d’un programme
Comme notre programme n’est pas robuste, il y a un risque de boucles infinies à cause des entrées non validées.
Au cas où, n’oubliez pas que vous pouvez arrêter l’exécution d’un programme dans le terminal avec Ctrl + C !
Essayez de produire des erreurs en cherchant un étudiant qui n’existe pas dans le carnet de notes ou en affichant la moyenne d’un étudiant sans note.
Rectifiez ces problèmes en levant des exceptions aux bons endroits pour passer les tests.
Les exceptions que nous avons levées viennent d’erreurs récupérables et ne doivent pas interrompre le programme.
Ajoutez des blocs
tryetcatchautour deswitch(choice) {...}pour attraper les exceptions levées et afficher leur message d’erreur.
Avez-vous bien attrapé uniquement les exceptions que nous avons levées ?
Rappel : il est important d’attraper uniquement les exceptions que nous avons levées nous-mêmes dans le code pour ne pas gérer des exceptions inattendues.
Nous avons toujours le problème des entrées qui ne sont pas validées :
Le choix du menu doit être un entier entre 1 et 4.
Le nom d’un étudiant doit contenir des caractères différents des espaces. Par exemple, nous n’acceptons pas
" "comme nom.Une note doit être un
doubleentre 0 et 20.
Déclarez les fonctions de validation suivantes dans
input-reader.h:
static bool isValidStudentName(std::istream& inputStream);static bool isValidMenuChoice(std::istream& inputStream);static bool isValidGrade(std::istream& inputStream);
istream
istreamistream est une classe de flux d’entrée de la bibliothèque <istream>.
Ajoutez les tests suivants aux bons endroits et appeler
InputReaderTest::runTests()danstests/source/main.cpp.
#ifndef INPUT_READER_TEST_H
#define INPUT_READER_TEST_H
#include <istream>
#include <vector>
#include <string>
class InputReaderTest {
public:
static void runTests();
private:
static void assertValidity(bool (*validationFunction)(std::istream&), const std::vector<std::string>& inputStrings, bool expectedValidity);
static void isValidGrade_DoubleBetween0And20_ReturnsTrue();
static void isValidGrade_DoubleOutsideAcceptedRange_ReturnsFalse();
static void isValidGrade_DoubleOutOfRange_ReturnsFalse();
static void isValidGrade_NonDouble_ReturnsFalse();
static void isValidMenuChoice_IntegerBetween1And4_ReturnsTrue();
static void isValidMenuChoice_IntegerOutsideAcceptedRange_ReturnsFalse();
static void isValidMenuChoice_IntegerOutOfRange_ReturnsFalse();
static void isValidMenuChoice_NonInteger_ReturnsFalse();
static void isValidStudentName_NonBlankInput_ReturnsTrue();
static void isValidStudentName_BlankInput_ReturnsFalse();
};
#endif#include "input-reader.h"
#include "input-reader-test.h"
#include <cassert>
#include <iostream>
#include <sstream>
#include <vector>
#include <string>
void InputReaderTest::runTests() {
isValidGrade_DoubleBetween0And20_ReturnsTrue();
isValidGrade_DoubleOutsideAcceptedRange_ReturnsFalse();
isValidGrade_DoubleOutOfRange_ReturnsFalse();
isValidGrade_NonDouble_ReturnsFalse();
isValidMenuChoice_IntegerBetween1And4_ReturnsTrue();
isValidMenuChoice_IntegerOutsideAcceptedRange_ReturnsFalse();
isValidMenuChoice_IntegerOutOfRange_ReturnsFalse();
isValidMenuChoice_NonInteger_ReturnsFalse();
isValidStudentName_NonBlankInput_ReturnsTrue();
isValidStudentName_BlankInput_ReturnsFalse();
std::cout << "All InputReader tests passed\n";
}
void InputReaderTest::assertValidity(bool (*validationFunction)(std::istream&), const std::vector<std::string>& inputStrings, bool expectedValidity) {
for (const std::string& inputString : inputStrings) {
std::istringstream stream(inputString);
bool actualValidity = validationFunction(stream);
if (actualValidity != expectedValidity) {
std::cerr << "Failed for input: " << inputString << std::endl;
}
assert(actualValidity == expectedValidity);
}
}
void InputReaderTest::isValidGrade_DoubleBetween0And20_ReturnsTrue() {
assertValidity(InputReader::isValidGrade, {"0", "20", "10.5", " 15.67 "}, true);
std::cout << "isValidGrade_DoubleBetween0And20_ReturnsTrue passed\n";
}
void InputReaderTest::isValidGrade_DoubleOutsideAcceptedRange_ReturnsFalse() {
assertValidity(InputReader::isValidGrade, {"-11.55", "22.11"}, false);
std::cout << "isValidGrade_DoubleOutsideAcceptedRange_ReturnsFalse passed\n";
}
void InputReaderTest::isValidGrade_DoubleOutOfRange_ReturnsFalse() {
assertValidity(InputReader::isValidGrade, {"999999999999999999999999999999999999999"}, false);
std::cout << "isValidGrade_DoubleOutOfRange_ReturnsFalse passed\n";
}
void InputReaderTest::isValidGrade_NonDouble_ReturnsFalse() {
assertValidity(InputReader::isValidGrade, {"abc", "12abc", "", " "}, false);
std::cout << "isValidGrade_NonDouble_ReturnsFalse passed\n";
}
void InputReaderTest::isValidMenuChoice_IntegerBetween1And4_ReturnsTrue() {
assertValidity(InputReader::isValidMenuChoice, {"1", "2", "3", " 4 "}, true);
std::cout << "isValidMenuChoice_IntegerBetween1And4_ReturnsTrue passed\n";
}
void InputReaderTest::isValidMenuChoice_IntegerOutsideAcceptedRange_ReturnsFalse() {
assertValidity(InputReader::isValidMenuChoice, {"0", "5"}, false);
std::cout << "isValidMenuChoice_IntegerOutsideAcceptedRange_ReturnsFalse passed\n";
}
void InputReaderTest::isValidMenuChoice_IntegerOutOfRange_ReturnsFalse() {
assertValidity(InputReader::isValidMenuChoice, {"999999999999999999999999999999999999999"}, false);
std::cout << "isValidMenuChoice_IntegerOutOfRange_ReturnsFalse passed\n";
}
void InputReaderTest::isValidMenuChoice_NonInteger_ReturnsFalse() {
assertValidity(InputReader::isValidMenuChoice, {"abc", "1abc", "", "1.5"}, false);
std::cout << "isValidMenuChoice_NonInteger_ReturnsFalse passed\n";
}
void InputReaderTest::isValidStudentName_BlankInput_ReturnsFalse() {
assertValidity(InputReader::isValidStudentName, {"", " "}, false);
std::cout << "isValidStudentName_BlankInput_ReturnsFalse passed\n";
}
void InputReaderTest::isValidStudentName_NonBlankInput_ReturnsTrue() {
assertValidity(InputReader::isValidStudentName, {"A", "Alice", "Alice B", " XA 13 "}, true);
std::cout << "isValidStudentName_NonBlankInput_ReturnsTrue passed\n";
}Refactorisation des tests
Ici, nous avons refactorisé les tests des fonctions de validation avec assertValidity, qui prend en argument une fonction de validation, une liste d’entrées sous forme de string ainsi que le résultat attendu de la fonction (true ou false).
Implémentez les fonctions
isValidGrade,isValidMenuChoiceetisValidStudentNamedansinput-reader.cpp, comme vu en cours, pour passer les tests.
Avez-vous pensé aux nombres magiques ?
Pensez à refactoriser les nombres magiques qui pourraient apparaître dans votre code de validation en les définissant comme private static constexpr.
constexpr est une fonctionnalité qui permet de déclarer des valeurs constantes dont la valeur est calculée au moment de la compilation (et non pendant l’exécution).
Ajoutez la fonction privée
requestInputàInputReader, comme vu en cours, pour limiter le nombre de tentatives d’entrée à5, par exemple.Refactorisez les fonctions
requestMenuChoice,requestGradeetrequestStudentNamedeinput-reader.cppen utilisantrequestInputet les fonctions de validation, comme vu en cours.Compilez, exécutez votre code et vérifiez que les entrées non valides affichent des messages d’erreur bien choisis sans interrompre le programme.
Ajoutez un dernier bloc
try-catchpour lesFatal errordanssource/main.cpp, qui attrape toutes les autres exceptions (y comprisstd::invalid_argument("Maximum number of attempts reached.")deInputReader::requestInput), comme vu en cours.Compilez et exécutez votre code une dernière fois. Est-ce que votre programme est robuste ?
Lever (
throw, syntaxe) des exceptions avec des types précis (commestd::invalid_argumentoustd::runtime_error).Utiliser
try(syntaxe) uniquement autour du code qui peut lever des exceptions.Attraper (
catch, syntaxe) des exceptions avec des types précis sans interrompre le programme lorsqu’une erreur est récupérable.Valider les entrées avec des fonctions de validation appropriées (syntaxe).
Limiter le nombre de tentatives d’entrée pour éviter les boucles infinies (avec
requestInput).Attraper les
Fatal errorsdanssource/main.cpp(syntaxe).Écrire un test unitaire pour une fonction qui peut lever une exception (syntaxe).