C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
198: Unit Testing with the Check Framework
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
NULLinput 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.
There are no comments for now.