Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

  1. 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
└── makefile
  1. Fill Lab13/include/ with the following content.

student.h
grade-book.h
input-reader.h
application.h
#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
  1. Fill Lab13/source/ with the following content.

student.cpp
grade-book.cpp
input-reader.cpp
application.cpp
main.cpp
#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();
}
  1. Fill Lab13/tests/include/ and Lab13/tests/source/ with the following content.

student-test.h
grade-book-test.h
student-test.cpp
grade-book-test.cpp
main.cpp
#ifndef STUDENT_TEST_H
#define STUDENT_TEST_H

class StudentTest {
public:
    static void runTests();

private:
    static void getAverage_NoGrades_ThrowsRuntimeError();
};

#endif
  1. Fill compile_flags.txt and makefile with the following content.

compile_flags.txt
makefile
-xc++
-std=c++17
-Iinclude
-Itests/include
  1. Compile and run the code to observe how it works.

  1. 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.

  2. 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.

  1. Add try and catch blocks around switch(choice) {...} to catch the thrown exceptions and display their error message.

We still have the problem of inputs that are not validated:

  1. Declare the following validation functions in input-reader.h:

  1. Add the following tests in the right places and call InputReaderTest::runTests() in tests/source/main.cpp.

input-reader-test.h
#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
input-reader-test.cpp
#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";
}
  1. Implement the isValidGrade, isValidMenuChoice, and isValidStudentName functions in input-reader.cpp, as seen in class, to pass the tests.

  1. Add the private requestInput function to InputReader, as seen in class, to limit the number of input attempts to 5, for example.

  2. Refactor the requestMenuChoice, requestGrade, and requestStudentName functions from input-reader.cpp by using requestInput and the validation functions, as seen in class.

  3. Compile and run your code, and check that invalid inputs display well-chosen error messages without interrupting the program.

  4. Add one final try-catch block for Fatal error in source/main.cpp, which catches all other exceptions (including std::invalid_argument("Maximum number of attempts reached.") from InputReader::requestInput), as seen in class.

  5. Compile and run your code one last time. Is your program robust?

Key takeaways
  • Throw (throw, syntax) exceptions with precise types (such as std::invalid_argument or std::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 errors in source/main.cpp (syntax).

  • Write a unit test for a function that can throw an exception (syntax).