The Test Infected article starts out with a Money class, and so
will we. Of course, we can't do classes with C, but we don't really
need to. The Test Infected approach to writing code says that we
should write the unit test before we write the code, and in
this case, we will be even more dogmatic and doctrinaire than the
authors of Test Infected (who clearly don't really get this stuff,
only being some of the originators of the Patterns approach to
software development and OO design).
Here are the changes to check_money.c for our first unit test:
--- tests/check_money.1.c 2006-11-21 18:06:36.726270880 -0500
+++ tests/check_money.2.c 2006-11-21 18:06:36.760265712 -0500
@@ -1,3 +1,18 @@
+#include <check.h>
+#include "../src/money.h"
+
+START_TEST (test_money_create)
+{
+ Money *m;
+ m = money_create (5, "USD");
+ fail_unless (money_amount (m) == 5,
+ "Amount not set correctly on creation");
+ fail_unless (strcmp (money_currency (m), "USD") == 0,
+ "Currency not set correctly on creation");
+ money_free (m);
+}
+END_TEST
+
int
main (void)
{
A unit test should just chug along and complete. If it exits early,
or is signaled, it will fail with a generic error message. (Note: it
is conceivable that you expect an early exit, or a signal. There is
currently nothing in Check to specifically assert that we should
expect either — if that is valuable, it may be worth while adding to
Check.) If we want to get some information about what failed, we need
to use the fail_unless() function. The function (actually a
macro) takes a first Boolean argument, and an error message to send if
the condition is not true.
If the Boolean argument is too complicated to elegantly express within
fail_unless(), there is an alternate function fail()
that unconditionally fails. The second test inside
test_money_create above could be rewritten as follows:
if (strcmp (money_currency (m), "USD") != 0)
{
fail ("Currency not set correctly on creation");
}
There is also a fail_if() function, which is the
inverse of fail_unless(). Using it, the above test then
looks like this:
fail_if (strcmp (money_currency (m), "USD") != 0,
"Currency not set correctly on creation");
For your convenience all fail functions also accepts NULL as the msg argument and substitutes a suitable message for you. So you could also write a test as follows:
fail_unless (money_amount (m) == 5, NULL);
This is equivalent to:
fail_unless (money_amount (m) == 5,
"Assertion 'money_amount (m) == 5' failed");
All fail functions also support varargs and accept
printf-style format strings and arguments. This is especially
useful while debugging. With printf-style formatting the
message could look like this:
fail_unless(money_amount (m) == 5,
"Amount was %d, instead of 5", money_amount (m));
When we try to compile and run the test suite now using make check, we get a whole host of compilation errors. It may seem a bit strange to deliberately write code that won't compile, but notice what we are doing: in creating the unit test, we are also defining requirements for the money interface. Compilation errors are, in a way, unit test failures of their own, telling us that the implementation does not match the specification. If all we do is edit the sources so that the unit test compiles, we are actually making progress, guided by the unit tests, so that's what we will now do.
We will patch our header money.h as follows:
--- src/money.1.h 2006-11-21 18:06:37.554145024 -0500
+++ src/money.2.h 2006-11-21 18:06:37.620134992 -0500
@@ -1,4 +1,11 @@
#ifndef MONEY_H
#define MONEY_H
+typedef struct Money Money;
+
+Money *money_create (int amount, char *currency);
+int money_amount (Money * m);
+char *money_currency (Money * m);
+void money_free (Money * m);
+
#endif /* MONEY_H */
Our code compiles now, and again passes all of the tests. However,
once we try to use the functions in libmoney in the
main() of check_money, we'll run into more problems, as
they haven't actually been implemented yet.