Skip to Content
Course content

198: Unit Testing with the Check Framework

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been working on a small string utility library for a project, and I just finished a function called trim_whitespace. It's one of those functions that seems trivial—just strip the leading and trailing spaces—but as soon as you start thinking about null terminators and empty strings, it becomes a minefield.

char *trim_whitespace(char *str) {
    if (str == NULL) return NULL;

    char *end;
    while (*str == ' ') str++;

    if (*str == 0) return str;

    end = str + strlen(str) - 1;
    while (end > str && *end == ' ') end--;

    end[1] = '\0';

    return str;
}

The manual slog

My first instinct, like many of us, was to just write a main function and pepper it with printf calls to see if it worked. It looks like this:

int main() {
    char test1[] = "  hello  ";
    if (strcmp(trim_whitespace(test1), "hello") == 0) {
        printf("Test 1 Passed\n");
    } else {
        printf("Test 1 Failed\n");
    }
    // ... and so on for ten more cases
    return 0;
}

This works for a minute, but it's exhausting. I'm spending more time writing the "test harness" than the actual logic. Plus, if the function crashes on a NULL pointer, the whole program just dies, and I don't get a nice report of which specific test case triggered the failure. I need something that handles the boilerplate and doesn't blow up the entire process when I hit a segfault.

Switching to Check

I've used the Check framework before for exactly this reason. It's a unit testing framework for C that doesn't just run your code; it forks the process for each test. That's a huge deal in C because it means a crash in one test won't stop the rest of the suite from running.

To get started, I'll define a test case. In Check, we use START_TEST and END_TEST macros. Let's try to verify the basic trimming logic first.

#include 
#include "string_utils.h"

START_TEST(test_trim_basic) {
    char str[] = "  hello world  ";
    ck_assert_str_eq(trim_whitespace(str), "hello world");
}
END_TEST

I noticed that ck_assert_str_eq is much cleaner than my strcmp logic. It handles the comparison and, if it fails, it tells me exactly what the expected vs. actual strings were. Now I just need to wire this into a test suite. I'll create a TCase (a group of tests) and a Suite (a collection of TCases).

Suite *string_suite(void) {
    Suite *s = suite_create("StringUtilities");
    TCase *tc_core = tcase_create("Core");

    tcase_add_test(tc_core, test_trim_basic);
    suite_add_tcase(s, tc_core);

    return s;
}

Catching the crash

Now, here is where I usually mess up. I want to see what happens when I pass a NULL pointer to trim_whitespace. If I hadn't written that if (str == NULL) check at the top of my function, the program would segfault. Let's assume I removed that check for a second to see how Check handles it.

I add a new test case:

START_TEST(test_trim_null) {
    ck_assert_ptr_null(trim_whitespace(NULL));
}
END_TEST

When I run the suite using the SRunner, I don't get a "Segmentation fault (core dumped)" and a silent exit. Instead, Check reports: "test_trim_null: Process exited unexpectedly". It caught the signal, marked that specific test as failed, and continued to run every other test in the suite. That's the "magic" of the forking model. I can find the bug without the test runner itself crashing.

To actually execute this, I use a standard runner block in my main:

int main(void) {
    int number_failed = 0;
    Suite *s = string_suite();
    SRunner *sr = srunner_create(s);

    srunner_run_all(sr, CK_NORMAL);
    number_failed = srunner_ntests_failed(sr);
    srunner_free(sr);

    return (number_failed == 0) ? 0 : 1;
}

I've found that the biggest hurdle with Check is usually the linker. Since it's an external library, you have to remember to link it with -lcheck. On some systems, you might also need -lm (math library) and -lpthread and -lrt because Check relies on them for timing and process management. If you see a bunch of "undefined reference" errors, check your linker flags first.




📋 Practical Task

Implementing a Robust String Reverse Test Suite

You have been given a function char *reverse_string(char *str) that reverses a string in-place. However, you suspect it has bugs when dealing with empty strings, strings with a single character, or NULL pointers.

Your task is to build a comprehensive test suite using the Check framework. You must implement the following:

  • A test case that verifies a standard multi-character string is reversed correctly (e.g., "apple" becomes "elppa").
  • A test case that handles a string containing only one character.
  • A test case that handles an empty string ("").
  • A test case that ensures the function handles a NULL input without crashing the entire test suite.

Ensure you organize your tests into a TCase called "ReverseTests" and a Suite called "StringReverseSuite". Your main function should return 0 only if all tests pass.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.