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:
- Write a new test
- Write just enough code to make the test pass
- Improve the structure of the code and the test
- 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:
- Write a small amount of code (e.g. a function)
- Write one or more tests to verify the code you just wrote
- Improve the structure of the code and the test
- 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.