Create a
Lab13/directory, along with the following directories and files.
Lab13/
├── 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
└── makefileFill
Lab13/include/with the following content.
#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();
};
#endifWhat does this application do?
A first look at the declarations shows us that two main objects are manipulated here: Student and GradeBook.
Studentcontains a name and a list of grades. We can create aStudentwith a name, add grades usingaddGrade, and obtain their average usinggetAverage.GradeBookcontains a list ofStudentobjects. We can add aStudentusingaddStudentand retrieve aStudentusinggetStudent.
The Application displays a menu and offers us 4 choices:
Add a
Studentto theGradeBook.Add a grade to a
Student.Display the average of a
Student.Exit.
InputReader helps us get user input with requestMenuChoice, requestStudentName, and requestGrade.
Fill
Lab13/source/with the following content.
#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;
}Is this application robust?
We can already observe several problems with this application:
getAveragefromStudentdoes not handle the case wheremGradesis empty and may cause a division by zero.getStudentfromGradeBookdoes not handle the case where noStudenthas the requested name and may cause a segmentation fault (a program crash that happens when we try to access unallocated memory, for example withmStudents[index]whenindex >= mStudents.size()).InputReaderdoes not validate inputs, which can cause errors and lead to unexpected behavior.
Fill
Lab13/tests/include/andLab13/tests/source/with the following content.
#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;
}What do these tests do?
Here, we only test the following two elements:
getAverage_NoGrades_ThrowsRuntimeError, which checks thatgetAveragefromStudentthrows astd::runtime_errorwhenmGradesis empty.getStudent_StudentNotFound_ThrowsRuntimeError, which checks thatgetStudentfromGradeBookthrows astd::runtime_errorwhen noStudenthas the requested name. These tests should not pass for now.
How does this test work?
GradeBook gradeBook;
try {
// gradeBook has just been defined and does not contain any `Student`
gradeBook.getStudent("A"); // this line must `throw` a `std::runtime_error`
assert(false); // if we get here, then the previous line did not interrupt execution with `throw`
} catch (const std::runtime_error&) { // we only catch `std::runtime_error`
std::cout << "getStudent_StudentNotFound_ThrowsRuntimeError passed\n"; // if we caught a `std::runtime_error`, then the test passed
} catch (...) { // if we catch another error,
assert(false); // then the test failed
}Fill
compile_flags.txtandmakefilewith the following content.
-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)Compile and run the code to observe how it works.
Ctrl + C to stop a program’s execution
Since our program is not robust, there is a risk of infinite loops because of non-validated inputs.
Just in case, remember that you can stop a program’s execution in the terminal with Ctrl + C!
Try to produce errors by looking for a student who does not exist in the grade book or by displaying the average of a student without any grades.
Fix these problems by throwing exceptions in the right places to pass the tests.
The exceptions we threw come from recoverable errors and should not interrupt the program.
Add
tryandcatchblocks aroundswitch(choice) {...}to catch the thrown exceptions and display their error message.
Did you only catch the exceptions that we threw?
Reminder: it is important to catch only the exceptions that we threw ourselves in the code so that we do not handle unexpected exceptions.
We still have the problem of inputs that are not validated:
The menu choice must be an integer between 1 and 4.
A student’s name must contain characters other than spaces. For example, we do not accept
" "as a name.A grade must be a
doublebetween 0 and 20.
Declare the following validation functions in
input-reader.h:
static bool isValidStudentName(std::istream& inputStream);static bool isValidMenuChoice(std::istream& inputStream);static bool isValidGrade(std::istream& inputStream);
istream
istreamistream is an input stream class from the <istream> library.
Add the following tests in the right places and call
InputReaderTest::runTests()intests/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";
}Test refactoring
Here, we refactored the tests for the validation functions with assertValidity, which takes a validation function, a list of inputs as string, and the expected result of the function (true or false) as arguments.
Implement the
isValidGrade,isValidMenuChoice, andisValidStudentNamefunctions ininput-reader.cpp, as seen in class, to pass the tests.
Did you think about magic numbers?
Remember to refactor any magic numbers that may appear in your validation code by defining them as private static constexpr.
constexpr is a feature that lets you declare constant values whose value is computed at compile time (and not during execution).
Add the private
requestInputfunction toInputReader, as seen in class, to limit the number of input attempts to5, for example.Refactor the
requestMenuChoice,requestGrade, andrequestStudentNamefunctions frominput-reader.cppby usingrequestInputand the validation functions, as seen in class.Compile and run your code, and check that invalid inputs display well-chosen error messages without interrupting the program.
Add one final
try-catchblock forFatal errorinsource/main.cpp, which catches all other exceptions (includingstd::invalid_argument("Maximum number of attempts reached.")fromInputReader::requestInput), as seen in class.Compile and run your code one last time. Is your program robust?
Throw (
throw, syntax) exceptions with precise types (such asstd::invalid_argumentorstd::runtime_error).Use
try(syntax) only around code that can throw exceptions.Catch (
catch, syntax) exceptions with precise types without interrupting the program when an error is recoverable.Validate inputs with appropriate validation functions (syntax).
Limit the number of input attempts to avoid infinite loops (with
requestInput).Catch
Fatal errorsinsource/main.cpp(syntax).Write a unit test for a function that can throw an exception (syntax).