EECS 268 - Laboratory 3

20pts

Due: Sunday 2/12 at 11:59pm

The objectives of this lab are to learn testing approaches to improve programming practice and to practice writing recursive functions. Automated unit testing with assert() will be covered.

Testing and Debugging Discussion

No one writes perfect code the first time, every time.

Even after code compiles, it must be tested to verify that it works the way the programmer thinks it should work (unit and integration testing) and the way the client thinks it should work (acceptance testing).

Tests may reveal defects in the code and defects must be corrected. Often defects can be located and corrected simply by looking at the code and thinking about it. Sometimes defects are more difficult to find and fix. One approach with these types of defects is to insert output statements into the code. This approach however is not recommended as it is often very time-consuming and frustrating, plus it introduces changes to the very code that you are testing and debugging. Another approach is to use a debugger. A debugger is a tool that lets you "walk" through the program one line at a time and inspect variables (see their current values) as you go. Last week in lab, we discussed debugging and gained experience with the Data Display Debugger (DDD) software. Debuggers are useful tools for finding the causes of defects after we already know there is a defect. Testing is used to discover the existence of defects.

Entire programs can be tested directly by running the program with various inputs and checking for corresponding outputs. This process can be manual (by hand) or it can be automated using scripts, input/output files, file redirection, and diff tools. Individual units (e.g. functions) can also be tested directly by writing unit tests. We will look at a mechanism for writing automated unit tests in lab today. Automated unit tests can be written before and as you write your program (Test-First) or they can be written after you have written your program (Test-Last). The great thing about automated unit tests is that you can run them over and over again very quickly. If you make a change to your code, you can immediately run the tests to see if you broke anything.

Writing automated unit tests with assert()

Suppose you are writing a Date class. The code written so far might look like the following:

In file Date.h

class Date
{
public:
  Date();
  Date(int,int,int);
  int getMonth();
  int getDay();
  int getYear();
private:
  int month;
  int day;
  int year;
};

In file Date.cpp

#include "Date.h"

Date::Date()
{
  month = 0;
  day = 0;
  year = 0;
}

Date::Date(int m, int d, int y)
{
  month = m;
  day = d;
  year = y;
}

int Date::getMonth()
{
  return month;
}

int Date::getDay()
{
  return day;
}

int Date::getYear()
{
  return year;
}

Although this code is fairly simple and probably works correctly, how would we test it? One approach is to write automated unit tests using the assert() macro. assert() is defined in the standard include file cassert. assert() takes one parameter which is a boolean expression. If the expression evaluates to true, then nothing happens when the assert() is executed. If the expression evaluates to false, then the program halts at that point and prints a message indicating the line and the expression of the failing assert(). A test driver might look like the following:

In file driver.cpp


#include "Date.h"
#include <cassert>

void run_tests();

int main()
{
  run_tests();
}

void run_tests()
{
  { //test 1
    Date d;
    assert(d.getMonth() == 0);
    assert(d.getDay() == 0);
    assert(d.getYear() == 0);
  }
  { //test 2
    Date d(6,14,1924);
    assert(d.getMonth() == 6);
    assert(d.getDay() == 14);
    assert(d.getYear() == 1924);
  }
}
This test driver consists of two unit tests. Each test has been isolated in its own block and labelled with a comment. All of the unit tests have been separated in a function called run_tests(). This keeps the tests separate from the actual code, and makes it easy to turn off the tests if you want to (e.g. comment out the call to run_tests()). Note that you can still have main() perform whatever actions you would normally put in main().

Test-First Programming

Now suppose you want to add new functionality to your Date class such as a new member function that allows you to add a number of days to a Date. You have two choices. You could write the new member function first, then write additional tests to see if your function works correctly. Or you could write the test first, run it (it should fail, if it doesn't, ask why not), then implement the code to make the test pass.

Test-first programming (or test-driven development) involves the following steps:
  1. Write a new test
  2. Write just enough code to make the test pass
  3. Improve the structure of the code and the test
  4. repeat to 1
Completing the first three steps should be kept fairly short, maybe only about 15 minutes. That means your tests are very focused and specific, not too broad or complex.

Test-Last Programming

Test-last programming is similar to test-first except the tests are written after the code is implemented, not before. Notice that a test-last approach focuses on verifying that the code you wrote works correctly. A test-first approach influences design decisions because you have not yet written the code. With a test-first approach, you decide the name of a function, its parameters, and its return types (i.e. its interface) as well as its expected behavior when you write the test. A test-last approach assumes you've already made these design decisions before you wrote the code that you are now testing.

Test-last programming involves the following steps:
  1. Write a small amount of code (e.g. a function)
  2. Write one or more tests to verify the code you just wrote
  3. Improve the structure of the code and the test
  4. repeat to 1

Recursion with Tests

lab3Test.cpp contains an example of a recursive function that is tested with assert(). The function sumToN(int) sums the integers from 1 to n for n > 0 and returns 1 for n <= 0. Download, compile, and run this program to verify that all tests pass. Add another test that fails just to see what happens. Fix your new test and verify that all tests pass.

Assignment

Write a program that contains the following two functions:
int sum(int start, int end)
Pre: start < end
Post: Returns start + (start+1) + ... + (end-1) + end
For example, sum(3,5) should return 12.

bool palindrome(char phrase[])
Pre: Phrase is a null-terminated character string
Post: Returns true if phrase reads the same forward as backward, false otherwise
For example, palindrome("aba") should return true, but palindrome("abb") should return false.

Both functions should be implemented recursively. The sum function should be implemented with direct recursion (sum calls itself), but the palindrome function will probably use indirect recursion where palindrome calls another function that is recursive. You will also need to use the strlen function which returns the length of a character string.

Write the first (sum) function in a test-last manner and the second (palindrome) function in a test-first manner. Include a run_tests() function as described above. Complete all code in a single file named <firstname><lastname>Recursion.cpp. When you are finished, submit your file as specified in the submission guidelines.