Next Previous Contents

4. Advanced Features

4.1 Running multiple cases

What happens if we pass -1 as the amount in money_create? What should happen? Let's write a unit test. Since we are testing limits, we should also test what happens when we create money with amount 0:


START_TEST (test_neg_create) 
{ 
  Money *m = money_create (-1, "USD"); 
  fail_unless (m == NULL, "NULL should be returned on attempt to create with a negative amount"); 
} 
END_TEST
START_TEST (test_zero_create) 
{ 
  Money *m = money_create (0, "USD"); 
  fail_unless (money_amount (m) == 0, "Zero is a valid amount of money"); 
} 
END_TEST

Let's put these in a separate test case, called "Limits" so that money_suite looks like so:


Suite *money_suite (void) { 
  Suite *s = suite_create ("Money"); 
  TCase *tc_core = tcase_create ("Core"); 
  TCase *tc_limits = tcase_create ("Limits"); 
  suite_add_tcase (s, tc_core); 
  suite_add_tcase (s, tc_limits); 
  tcase_add_test (tc_core, test_create); 
  tcase_add_test (tc_limits, test_neg_create); 
  tcase_add_test (tc_limits, test_zero_create); 
  return s; 
}

Now we can rerun our suite, and fix the problem(s). Note that errors in the Core test case will be reported as "Core" and errors in the Limits test case will be reported as "Limits," giving you additional information about where things broke.

4.2 No fork mode

Check normally forks to create a separate address space. This allows a signal or early exit to be caught and reported, rather than taking down the entire test program, and is normally very useful. However, when you are trying to debug why the segmentation fault or other program error occurred, forking makes it difficult to use debugging tools. To define fork mode for an SRunner object, you can do one of the following:

  1. Define the CK_FORK environment variable to equal "no".
  2. Explicitly define the fork status through the use of the following function:
    void srunner_set_fork_status (SRunner *sr, enum fork_status fstat); 
     
    

The enum fork_status defines the following values: CK_FORK and CK_NOFORK.

The explicit definition overrides the environment variable.

4.3 Test fixtures

We may want multiple tests that all use the same Money. In such cases, rather than setting up and tearing down objects for each unit test, it may be convenient to add some setup that is constant across all the tests in a test case. In the extreme programming approach to unit tests, such setup is called a test fixture.

A fixture is created by defining a setup and/or a teardown function, and associating it with a test case. There are two kinds of test fixtures in Check: checked and unchecked fixtures. These are defined as follows:

Checked fixtures

are run inside the address space created by the fork to create the unit test. Before each unit test, the setup function is run, if defined. After each unit test, the teardown function is run, if defined. Because they run inside the forked address space, if checked fixtures signal or otherwise fail, they will be caught and reported by the SRunner.

Unchecked fixtures

are run before and after the run of unit tests for a test case. They may use the fail functions, but may not signal or exit, since they are run in the same address space as the test program.

Test fixture examples

We create a test fixture in Check as follows:

  1. Define the global variables, and functions to setup and teardown the globals. The functions both take void and return void
    Money *five_dollars; 
    void setup (void) { 
      five_dollars = money_create (5, "USD"); 
    }
    
    void teardown (void) 
    { 
      money_free (five_dollars); 
    }
     
    

  2. Add the setup and teardown functions to the test case with "tcase_add_checked_fixture" (this belongs in the suite setup function "money_suite":
    tcase_add_checked_fixture (tc_core, setup, teardown);
     
    

  3. We can now rewrite the first test we wrote as follows:
    START_TEST(test_create) 
    { 
       fail_unless (money_amount (five_dollars) == 5,
                   "Amount not set correctly on creation"); 
       fail_unless (strcmp (money_currency (five_dollars), "USD") == 0,
                    "Currency not set correctly on creation"); 
    } 
    END_TEST
     
    

Checked vs Unchecked fixtures

Because checked fixtures run once per unit test, they should not be used for expensive setup. Because unchecked fixtures may take down the entire test program, they should only be used if they are known to be safe.

Additionally, checked and uncheck fixtures may behave differently in CK_NOFORK mode. Because the fork creates a separate address space, in CK_FORK mode unit tests may abuse the objects created in an unchecked fixture with impunity, without affecting other unit tests in the same test case. However, in CK_NOFORK mode, all tests live in the same address space, and side effects in one test will affect the unchecked fixture for other tests. A checked fixture, however, will generally not be affected by unit test side effects, since the setup is run before each unit test. (There is an exception for side effects to the total environment in which the test program lives: e.g., if the setup function initializes a file that a unit test then changes, the combination of the teardown function and setup fuction must be able to restore the environment for the next unit test).

If the setup function in a fixture fails, in either checked or unchecked fixtures, the unit tests for the test case, and the teardown function for the fixture will not be run. A fixture error will be created and reported to the SRunner.

4.4 Multiple suites run through the same SRunner

In a large program, it will be convenient to create multiple suites, each testing a module of the program. While one can create several test programs, each running one Suite, it may be convenient to create one main test program, and use it to run multiple suites. The Check test suite provides an example of how to do this. The main testing program is called check_check, and has a header file that declares suite creation functions for all the module tests:


Suite *make_sub_suite (void);
Suite *make_sub2_suite (void);
Suite *make_master_suite (void);
Suite *make_list_suite (void);
Suite *make_msg_suite (void);
Suite *make_log_suite (void);

The function srunner_add_suite is used to add additional suites to an SRunner. Here is the code to setup and run the SRunner in the main function:


SRunner *sr;
sr = srunner_create (make_master_suite ());
srunner_add_suite (sr, make_list_suite ());
srunner_add_suite (sr, make_msg_suite ());
srunner_add_suite (sr, make_log_suite ()); 

4.5 Test Logging

Check supports operation to log the results of a test run. To use test logging, use the srunner_set_log function with the name of the log file you wish to create:


SRunner *sr;
sr = srunner_create (make_s1_suite ());
srunner_add_suite (sr, make_s2_suite ());
srunner_set_log (sr, "test.log");
srunner_run_all (sr, CRNORMAL);

Check will write the results of the run to test.log. The printmode argument to srunner_run_all does not apply to test logging; the log will contain a result entry, organized by suite, for every test run . Here is an example of test log output:


Running suite S1 
ex_log_output.c:7:P:Core: Test passed
ex_log_output.c:13:F:Core: Failure
ex_log_output.c:17:E:Core: (after this point) Early exit with return value 1
Running suite S2 
ex_log_output.c:25:P:Core: Test passed
Results for all suites run:
50%: Checks: 4, Failures: 1, Errors: 1

4.6 Conclusion

This tutorial has provided an introduction to all of the functionality available in Check. Hopefully, this is enough to get you started writing unit tests with Check. All the rest is simply application of what has been learned in the tutorial through multiple repetitions of "test a little, code a little."


Next Previous Contents