CS 50 Software Design and Implementation
Lecture 18
The Art of Testing: Unit Testing
In this lecture we continue our discussion on the art of testing and discuss unit testing. The notes only
currently give an overview of the unit test for the dictionary.c file and its functions and the test harness.
The Test Harness Spec is also included.
Goals
We plan to learn the following from today’s lecture:
- Why is unit testing important?
- Designing a unit test
- The dictionary.c file as an example unit test case.
- Run the test harness and look at the output
Unit testing
The following files are in the unit_test tarball
Makefile
dictionary.c
dictionary.h
dictionary_test.c
hash.h
header.h
README
All the files included in the unit_test tarball are listed below.
dictionary˙test.c- This is the test harness where all the unit tests are driven from.
dictionary.c- This is the unit under test.
dictionary.h- Dictionary header.
hash.h- Hash.c is not included in the test code because the hash function is dummied out in the test to
control the testing of the dictionary functions.
header.h- header file that include a number of useful marcos such BZERO, MALLOC˙CHECK and
MYASSERT.
In the tarball sent you there is also a README file which we point to here for completeness.
README- README
Makefile- Makefile
Test harness
The test harness file include a Test Harness Spec that defines the functions under test and the test cases
as shown below. This is included in dictionary˙test.c
//
// Test Harness Spec:
// ------------------
//
// It uses these files but they are not unit tested in this test harness:
//
// DICTIONARY* InitDictionary();
// int make_hash(char* c);
// void CleanDictionary(DICTIONARY* dict)
//
// It tests the following functions:
//
// void DAdd(DICTIONARY* dict, void* data, char* key);
// void DRemove(DICTIONARY* dict, char* key);
// void* GetDataWithKey(DICTIONARY* dict, char* key);
//
// If any of the tests fail it prints status
// If all tests pass it prints status.
//
// Test Cases:
// -----------
//
// The test harness runs a number of test cases to test the code.
// The approach is to first set up the environment for the test,
// invoke the function to be tested, then validate the state of
// the data structures using the SHOULD_BE macro. This is repeated
// for each test case.
//
// The test harness isolates the functions under test and setting
// up the environment the code expects when integrated in the real
// system.
//
// The test harness dummies out the real hash function and through
// use of a variable called hash manipulates where DNODEs are
// inserted into the DICTIONARY. For example, collisions can be controlled.
//
Test cases for void DAdd(DICTIONARY* dict, void* data, char* key);
// The following test cases (1-3) are for function:
//
// void DAdd(DICTIONARY* dict, void* data, char* key);
//
// Test case: DADD:1
// This test case calls DAdd() for the condition where dict is empty
// result is to add a DNODE to the dictionary and look at its values.
//
// Test case: DADD:2
// This test case calls DAdd() puts multiple DNODEs on the dict when there is no hash collisions
// We put multiple elements in dictionary with no collisions.
//
// Test case: DADD:3
// This test case calls DAdd() puts multiple DNODEs on the dict when there is hash collisions
// We put multiple elements in dictionary with collisions.
//
Test cases for void DRemove(DICTIONARY* dict, char* key);
// The following test cases (1-4) for function:
//
// void DRemove(DICTIONARY* dict, char* key);
//
// Test case:DREMOVE:1
// This test case DAdd() and DRemove() DNODE from dict for only one element.
//
// Test case:DREMOVE:2
// This test case is tries to see how DRemove() works with multiple nodes for the same
// hash value, the node to be deleted is at the end of the dynamic list.
//
// Test case:DREMOVE:3
// This test case is tries to see how DRemove() works with multiple nodes of the same hash value,
// the node to be deleted is at the start of the dynamic list.
//
// Test case:DREMOVE:4
// This test case is tries to see how DRemove() works with multiple nodes of the same hash value,
// the node to be deleted is at the middle of the dynamic list.
Test cases for void* GetDataWithKey(DICTIONARY* dict, char* key);
//
// The following test cases (1) for function:
//
// void* GetDataWithKey(DICTIONARY* dict, char* key);
//
// Test case:GetDataWithKey:1
// This test case tests GetDataWithKey - to get a data with the a certain key.
//
The test defines a number of useful marcos that help to structure the test harness and make the test more
readable.
// Useful MACROS for controlling the unit tests.
// each test should start by setting the result count to zero
#define START_TEST_CASE int rs=0
// check a condition and if false print the test condition failed
// e.g., SHOULD_BE(dict->start == NULL)
#define SHOULD_BE(x) if (!(x)) {rs=rs+1; \
printf("Line %d Fails\n", __LINE__); \
}
// return the result count at the end of a test
#define END_TEST_CASE return rs
//
// general macro for running a best
// e.g., RUN_TEST(TestDAdd1, "DAdd Test case 1");
// translates to:
// if (!TestDAdd1()) {
// printf("Test %s passed\n","DAdd Test case 1");
// } else {
// printf("Test %s failed\n", "DAdd Test case 1");
// cnt = cnt +1;
// }
//
#define RUN_TEST(x, y) if (!x()) { \
printf("Test %s passed\n", y); \
} else { \
printf("Test %s failed\n", y); \
cnt = cnt + 1; \
}
To control the control flow of the test the hash.c file and specifically the hash1() function is replaced by a
dummy function call and a global variable called hash that can control whether the state if the hash table
is empty of has a collision. This dummy function also helps to drive a number of the test cases under
study.
// Dummying out routines
// We want to isolate this set of functions and test them and control
// various conditions with the hash table and function. For example,
// we want to create collisions. So in order to do this we dummy out the
// hash function and make it return whatever the current value of hash is
// In our test suite we mainpulate the value of hash so when the "real code"
// calls our dummy hash function it always returns the value we set in hash.
// Devious, hey?
// what we want the hash function to return
unsigned long hash = 0;
// The dummy hash function, which returns the value we set up in hash
unsigned long hash1(char* str) {
return hash;
}
If we run our test harness then we get the following output. All tests passed.
[atc@Macintosh-25 unit_test] ./dictionary_test
Test DAdd Test case 1 passed
Test DAdd Test case 2 passed
Test DAdd Test case 3 passed
Test DRemove Test case 1 passed
Test DRemove Test case 2 passed
Test DRemove Test case 3 passed
Test DRemove Test case 4 passed
Test GetDataWithKey Test case 1 passed
All passed!