Jorgen Grahn said:
Good with an example, because I think you may be making it harder by
looking at it from a too general perspective. I don't think you need
a general pattern for writing assertions than you need for, say, text
output.
Maybe. I have wondered if I was making a rod for my own back. My thought is
that if I can work out a general approach now it will make future testing
easier.
In fact I have got to a point I am happy enough with to ask for comments on
(if that's not too much a mix of trailing prepositions). It combines a
number of the suggestions people have made in this thread. Comments and
criticsms would be appreciated. Bear in mind that I'm not as familiar with C
as I would like so I would welcome suggestions even on small things like
choice of names, use of the elements of C, style etc. The purpose of the
approach is to use some fairly simple code (no large libraries etc) while
making it easy to write clear tests for arbitrary types of data.
The approach is intended to be generally usable but the main example of an
awkward case is to test manipulation of a list. As mentioned before, for a
list of, say, five elements, rather than having to write five tests, one for
each element and probably another one or two to confirm the limits of the
list, I felt it would be much clearer to test the list as a whole on a
single line.
First, the list module has an addional function which converts the list to a
string. It is named, naturally enough, list_to_striing(). The numbers that
make up the list are written in decimal with one space between them. At the
moment that converter is included only if debugging. I have phrased that
test as #ifndef NDEBUG so it is present by default. I don't know if that's
considered a good way to do it or not.
In practice, list_to_string() is quite short so it may be worth keeping in
the load module even when not debugging. It is limited but it might be handy
in other circumstances.
Second, there is a macro which compares two strings and produces a suitable
one-line report if they are not identical. The macro is called CHECK_SEQ for
check-string-equal (there is currently another macro to compare integers).
The string comparison macro is intended to be called as
CHECK_SEQ(function_call(args), "expected result", "descriptive text")
Then if the string returned from the function call does not match the
expected result a one-line report is printed saying what was expected and
what was received. The function call has to return a pointer to some
allocated memory.
The macro is as follows.
#define CHECK_SEQ(fcall, expected, note) \
check_buf = fcall; \
if (strcmp(check_buf, expected) != 0) { \
CHECK_PRINT_LINE_INFO \
fprintf(CHECK_STREAM, " expected %s", expected); \
fprintf(CHECK_STREAM, ", got %s", check_buf); \
CHECK_PRINT_NOTE(note) \
fprintf(CHECK_STREAM, ".\n"); \
check_errors++; \
} \
else { \
check_passes++; \
} \
free(check_buf);
The two helper macros it uses are
#define CHECK_PRINT_LINE_INFO \
/* fprintf(CHECK_STREAM, " file %s", __FILE__); */ \
fprintf(CHECK_STREAM, " line %i", __LINE__);
#define CHECK_PRINT_NOTE(note) \
if (strcmp(note, "") != 0) { \
fprintf(CHECK_STREAM, ", %s", note); \
}
Third, and finally, the above makes it easy to write tests (which is the
whole point of the exercise) such as the following.
Here are some tests to check the creation of and the effects of changes to a
list.
list_create(list, space, ELEMENTS);
CHECK_SEQ(list_to_string(list), "", "new list")
list_insert(list, 0, 0);
CHECK_SEQ(list_to_string(list), "0", "single-element list")
list_insert(list, 1, 1);
CHECK_SEQ(list_to_string(list), "0 1", "two elements")
list_insert(list, 2, 2);
list_insert(list, 1, 11);
list_insert(list, 0, 10);
CHECK_SEQ(list_to_string(list), "10 0 11 1 2", "many elements")
To me, the above is a bit wordy because the list needs to be manipulated
between tests. To try to illustrate why I think the macro part of the
approach is useful here are some tests which check the results of a
function. They can be one per line. IMO the brevity makes the tests easier
to write and easier to read. For the moment, at least, these compare
integers rather than strings.
CHECK_IEQ(list_scan_ae(list, 0, 4), 0, "scan from 0 for 4")
CHECK_IEQ(list_scan_ae(list, 3, 12), 4, "scan from 3 for 12")
CHECK_IEQ(list_scan_ae(list, 0, 55), 5, "scan from 0 for 55")
....
Why not simply define an equality test for lists? That leaves you
with the problem of creating reference lists and not confusing them
with each other, but perhaps you can fix your tests so that when the
manipulations are done, the expected contents are 9,10,11. Then you
only need a 'static struct list ref_9_10_11'.
I can see that it could be done and there is a possiblity that an equality
test will be needed anyway. One big advantage of strings, though, is that
although C doesn't support arbitrary use of manifest constants string
literals can be written directly anywhere they are needed. For the case in
point I can just compare the results of the call against "9 10 11". There is
no need to separately define a variable with 9,10,11 as content.
Also, strings should be immediately usable in other contexts apart from
lists of numbers so similar comparisons should work for other data types.
I'm aware that they wouldn't work well for comparisons other than equality
but testing whether the results of a call are equal to what was expected are
probably the most useful.
Or you could fix your tests so all expected outcomes are lists on the
form N, N+1, N+2, ... and have an assertion
void assert_list_is_sequence(const struct List*, int m, int n);
Agreed. I did think about that when I was trying to work out how best to do
this ... before strings came to the rescue.
James