3.3. Test a little, code a little

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, 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 is our first unit test:

START_TEST (test_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

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 above can 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(), the above test then looks like:

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 the line:

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, 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 add the following to our header 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);

and our code now compiles, but fails to link, since we haven't implemented any of the functions. Let's do that now, creating stubs for all of the functions:

#include <stdlib.h>
#include "money.h"
Money *money_create(int amount, char *currency) 
{ 
  return NULL; 
}
int money_amount(Money *m) 
{ 
  return 0; 
}
char *money_currency(Money *m) 
{ 
  return NULL; 
}
void money_free(Money *m) 
{ 
  return; 
}

Now, everything compiles, and we still pass all our tests. How can that be??? Of course -- we haven't run any of our tests yet....