Fishtank
Homework 8: COR, Constructive

Overview: Under the sea

It's time to write some tests! For this assignment, you will design and implement a set of unit tests for some code we have written.

Our code implements an API for state tracking and rendering of a virtual "fishtank" which, like a real fishtank, contains fish, rocks, and other things that you can see but not interact with. (This is inspired by an older assignment from Tufts' CS 11.) The API assumes that these objects will be drawn to a terminal and so represents each one as an ASCII art string. For example, a fish might look like this:

|\ ,
\`._.-' `--.
) o o =[#]#]
) o -3
/.' `-.,---'
'

The things in the tank (all of which we call "fish", for simplicity) can move on their own, meaning the state of the tank changes over time. The exact rate of change is not fixed; instead, the API operates in "ticks", an abstract unit of time which represents a single movement of each object.

The C++ API for the fishtank consists of several classes, which are declared across several header files. You can find documentation for each class in the "Classes" dropdown of this website, which has been generated using a tool called Doxygen. (See README.md for the source code of this page.)

As you can see, we have a Fish class which represents a single object in the tank that knows its own appearance, position, and speed. We also have a Tank class which represents a tank containing an arbitrary number of fish that knows how to render all of those fish onto a virtual canvas (a subclass of Screen, some of which can render themselves as a multi-line string) of a given width and height.

Steps

Step 0: Play with the fish

  1. Clone our fishtank implementation from github.com/cs50isdt/cor2-fishtank-base/. Do not look at the .cpp files yet. Looking at the header and Markdown files is fine. You will look at the other source files later.
  2. We have provided empty test files for you (fish-test.cpp, etc) that you will later fill in. Build the project. Test that the project builds and runs as-is (./build/fishtank-tests). Zero tests should run.
  3. To demonstrate how the API might be used, we've written a client for it, which you can find in main.cpp. This client creates a tank with some fish, rocks, and seaweed, then calls the Tank::tick, and Tank::draw functions to move the fish and draw them to the screen, respectively. The latter is accomplished with the help of a CursesScreen object, which is not part of the API you need to test, since it's specific to our demo app.

Your work, however, will not involve our demonstration client at all. Instead, you will be writing unit tests that call into the API directly via the classes shown above. Instead of CursesScreen, you'll use StringScreen, which returns the rendered tank as a string that can be easily inspected by automated tests.

Step 1: Write tests, tests, and more tests

  1. Look at the documentation we have provided for each class, either on the Doxygen-generated website or in the header files. Think about some tests that you would write. Test the behavior our documentation specifies, as well as any implicit behavior that you can infer from the API's structure.
  2. Now write those tests. Put tests for a given class inside the corresponding -test.cpp file. You might find the documentation for utest and the example test below helpful. Note that you only need to write unit tests for this assignment. You need not write any integration tests, as UTest is not very well suited to writing integration tests.
    • Remember to test the smallest possible unit of code at a time. If you have to do some setup, though, that's okay.
  3. Get in a defensive mindset. Think about what else you might test to catch logical corner cases or programming errors. Write those tests too. Tom and Max are devious and will break the code in unexpected ways.
  4. Now look at the .cpp files. What corner cases did you miss? Reflect on that. Write some more unit tests.

Here is a sample test template to get you started:

UTEST(SampleTestSuite, FiveIsFive) {
int value = 5;
EXPECT_EQ(5, value);
}

and another one based on the Fish class:

UTEST(FishTests, DrawWritesToScreen) {
Shape shape({"<><"});
Fish fish(shape, 0, 0, 0, 0);
StringScreen screen(5, 5);
fish.draw(&screen);
std::string actual = screen.toString();
EXPECT_STREQ(actual.c_str(), R"(+-----+
|<>< |
| |
| |
| |
| |
+-----+)");
}
Represents the position and velocity of a Shape on an infinite x-y grid.
Definition: fish.h:9
Represents an ASCII-art drawing and its corresponding bounding box.
Definition: shape.h:7
Serializable implementation of Screen using a 2D character buffer.
Definition: string-screen.h:13

All the tests in a file should be part of the same test case, meaning the first argument to each UTEST macro (SampleTestSuite, in this example) should match. It's conventional to name the test case after the class being tested, for example TankTests or FishTests.

You should expect to write many tests for each class. Although the code is relatively simple, there's still a lot that could go wrong, and we'll be running your unit tests to make sure they catch a variety of issues (see next section). The files you provide will likely contain several hundred lines of code in total, although some of this will be UTEST() macros and setup code that's common to multiple tests.

Notes

To write tests for functions returning std::string, you must compare the underlying C string (by calling .c_str()) using EXPECT_STREQ or ASSERT_STREQ. This is a limitation (for now! Max is working on submitting a patch) of the underlying UTest library.

UTEST(SampleTestSuite, StringEquality) {
std::string my_string("hello, world");
EXPECT_STREQ(my_string.c_str(), "hello, world");
}

Grading

Once you submit your unit tests, we will run them against a variety of broken implementations that we (the course staff) have come up with. These implementations are not specifically designed to thwart unit tests; we came up with them before seeing your submissions, and all the errors they introduce might reasonably be made in the real world (so none of them fail only when the screen width is exactly 54, for example). We are keeping these implementations a secret, so the only way you'll catch a majority of them is if you write comprehensive tests for the behavior our API documentation guarantees.

50% of your grade for this assignment will be based purely on these automated test results, although we reserve the right to curve up this portion of the grade for everyone if it's lower than we expect. The other 50% of your grade will be based on subjective evaluation of the code quality and adherence to best practices of the tests you write.

Submitting your work

Submit all of your *-test.cpp files on Gradescope.

Building this documentation yourself

This is not necessary to complete the assignment. It is half a reminder to ourselves for how to build the documentation, and half a curiosity for you.

  1. mkdir -p build/
  2. doxygen
  3. Point your browser at build/doxygen/html/.
  4. Copy the files: cp -r build/doxygen/html/ -T ../../isdt/assignments/08-cor-constructive