1. Overview and Core Features of CMocka
CMocka is a lightweight yet powerful unit testing framework for the C programming language, derived from Google’s cmockery project. It has the following notable features:
- Pure C Implementation: Completely written in standard C with no external dependencies
- Cross-Platform Support: Can run on various platforms including Linux, Windows, and embedded systems
- Mock Object Support: Capable of simulating complex function behaviors
- Flexible Test Organization: Supports test groups, setup/teardown functions
- Multiple Output Formats: Supports text, XML, and other test report formats
- Exception Handling: Provides a graceful handling mechanism for assertion failures
2. Installation and Project Integration
2.1 Installation on Linux Systems
# Download the latest stable version (example: 1.1.7)
wget https://cmocka.org/files/1.1/cmocka-1.1.7.tar.xz
tar -xvJf cmocka-1.1.7.tar.xz
cd cmocka-1.1.7
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
make
sudo make install
2.2 CMake Project Integration
find_package(cmocka REQUIRED)
add_executable(tests
test_example.c
# Other test files...
)
target_link_libraries(tests PRIVATE cmocka)
2.3 Cross-Compilation for Embedded Systems
# Example for ARM Cortex-M
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain-arm.cmake \
-DCMAKE_INSTALL_PREFIX=../sysroot \
..
3. Writing Basic Tests
3.1 Basic Test Structure
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
// Example function to be tested
int add(int a, int b) {
return a + b;
}
// Test case 1: Normal addition
static void test_add_normal(void **state) {
(void)state; // Unused state parameter
assert_int_equal(add(2, 3), 5);
assert_int_not_equal(add(-1, 1), 3);
}
// Test case 2: Boundary value testing
static void test_add_boundary(void **state) {
assert_int_equal(add(INT_MAX, 0), INT_MAX);
assert_int_equal(add(INT_MIN, 0), INT_MIN);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_add_normal),
cmocka_unit_test(test_add_boundary),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
3.2 Common Assertion Macros
| Assertion Type | Macro | Description |
|---|---|---|
| General | <span>assert_true(expr)</span> |
Expression is true |
<span>assert_false(expr)</span> |
Expression is false | |
| Integer | <span>assert_int_equal(a, b)</span> |
a == b |
<span>assert_int_not_equal(a, b)</span> |
a != b | |
<span>assert_in_range(val, min, max)</span> |
val ∈ [min,max] | |
| Memory | <span>assert_memory_equal(a, b, size)</span> |
Memory regions are equal |
<span>assert_memory_not_equal(a, b, size)</span> |
Memory regions are not equal | |
| String | <span>assert_string_equal(a, b)</span> |
Strings are equal |
<span>assert_string_not_equal(a, b)</span> |
Strings are not equal | |
| Pointer | <span>assert_null(ptr)</span> |
Pointer is NULL |
<span>assert_non_null(ptr)</span> |
Pointer is not NULL |
4. Advanced Testing Techniques
4.1 Using Mock Objects
#include <cmocka.h>
// Mocked database query function
int db_query(const char *sql, char **result) {
check_expected_ptr(sql); // Validate input parameter
*result = (char *)mock_ptr_type(char *); // Return mock value
return (int)mock(); // Return mock status code
}
// Function to be tested
int get_user_age(const char *username) {
char *result = NULL;
int ret = db_query("SELECT age FROM users WHERE name=?", &result);
if (ret != 0 || result == NULL) {
return -1;
}
int age = atoi(result);
free(result);
return age;
}
// Test case
static void test_get_user_age(void **state) {
(void)state;
// Set expectations: validate SQL statement format, return mock result
expect_string(db_query, sql, "SELECT age FROM users WHERE name=?");
will_return(db_query, cast_ptr_to_largest_integral_type("25")); // Mock return value
will_return(db_query, 0); // Mock success status code
assert_int_equal(get_user_age("testuser"), 25);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_get_user_age),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
4.2 Test Groups and Fixtures
#include <cmocka.h>
typedef struct {
int *array;
size_t size;
} TestState;
// Test group setup function
static int setup(void **state) {
TestState *ts = malloc(sizeof(TestState));
if (ts == NULL) return -1;
ts->size = 10;
ts->array = calloc(ts->size, sizeof(int));
if (ts->array == NULL) return -1;
*state = ts;
return 0;
}
// Test group teardown function
static int teardown(void **state) {
TestState *ts = *state;
free(ts->array);
free(ts);
return 0;
}
// Test case 1: Array initialization test
static void test_array_init(void **state) {
TestState *ts = *state;
for (size_t i = 0; i < ts->size; i++) {
assert_int_equal(ts->array[i], 0);
}
}
// Test case 2: Array operation test
static void test_array_operations(void **state) {
TestState *ts = *state;
ts->array[0] = 42;
assert_int_equal(ts->array[0], 42);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(test_array_init, setup, teardown),
cmocka_unit_test_setup_teardown(test_array_operations, setup, teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
5. Advanced Features and Techniques
5.1 Parameter Validation and Constraints
// Test strict parameter validation
static void test_strict_parameter_check(void **state) {
(void)state;
// Expect the second parameter to be a non-NULL pointer during the call
expect_any(db_query, sql);
expect_not_value(db_query, result, NULL);
// This will cause the test to fail
char *result = NULL;
db_query("SELECT...", &result); // This call meets expectations
// db_query("SELECT...", NULL); // This call will cause the test to fail
}
5.2 Call Sequence Control
static void test_call_sequence(void **state) {
(void)state;
// Set expected call sequence
expect_string(db_query, sql, "BEGIN TRANSACTION");
expect_string(db_query, sql, "INSERT...");
expect_string(db_query, sql, "COMMIT");
// The tested code must call db_query in this order
db_query("BEGIN TRANSACTION", NULL);
db_query("INSERT...", NULL);
db_query("COMMIT", NULL);
}
5.3 Exception Testing
// Test exception cases
static void test_error_handling(void **state) {
(void)state;
// Simulate database failure
expect_any(db_query, sql);
will_return(db_query, NULL); // Return NULL result
will_return(db_query, -1); // Return error code
assert_int_equal(get_user_age("testuser"), -1);
}
6. Practical Application Cases
6.1 Testing Embedded Device Drivers
// Simulate hardware register read/write
#define REG_ADDR (volatile uint32_t *)0x12340000
uint32_t mock_reg_value = 0;
uint32_t read_reg(void) {
function_called(); // Mark function called
return mock_reg_value;
}
void write_reg(uint32_t value) {
check_expected(value); // Validate written value
mock_reg_value = value;
}
static void test_register_operations(void **state) {
(void)state;
// Set expectations: write 0xAA, then read should return 0xAA
expect_value(write_reg, value, 0xAA);
will_return(read_reg, 0xAA);
write_reg(0xAA);
assert_int_equal(read_reg(), 0xAA);
}
6.2 Testing Network Protocol Stacks
// Simulate network send function
ssize_t mock_send(int sockfd, const void *buf, size_t len, int flags) {
check_expected(sockfd);
check_expected_ptr(buf);
check_expected(len);
// Return simulated number of bytes sent
return (ssize_t)mock();
}
static void test_packet_sending(void **state) {
(void)state;
const char test_packet[] = {0x01, 0x02, 0x03, 0x04};
// Set expectations: validate parameters and simulate successful send
expect_value(mock_send, sockfd, 42);
expect_memory(mock_send, buf, test_packet, sizeof(test_packet));
expect_value(mock_send, len, sizeof(test_packet));
will_return(mock_send, sizeof(test_packet)); // Simulate successful send
ssize_t sent = mock_send(42, test_packet, sizeof(test_packet), 0);
assert_int_equal(sent, sizeof(test_packet));
}
7. Best Practices and Debugging Techniques
7.1 Test Organization Recommendations
- Modular Testing: Create corresponding test files for each source file
- Naming Conventions: Use
<span>test_<function>_<scenario></span>format for test functions - Test Granularity: Each test case should validate a specific behavior
- Test Isolation: Ensure no dependencies between test cases
7.2 Debugging Techniques
# Use gdb to debug test cases
gdb --args ./test_program -v
# Set breakpoints in gdb
(gdb) b test_example
(gdb) run
# Generate XML test report
./test_program --xml_output=results.xml
# Display detailed test information
./test_program -v
7.3 Continuous Integration Integration
# Example .gitlab-ci.yml
unit_test:
stage: test
script:
- mkdir build && cd build
- cmake .. -DENABLE_TESTING=ON
- make
- ctest --output-on-failure
artifacts:
when: always
paths:
- build/Testing/**/*.xml
reports:
junit: build/Testing/**/*.xml
8. Common Issues and Solutions
8.1 Memory Leak Detection
// Enable memory checks in test groups
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_example),
};
// Enable memory checks
_cmocka_set_skip_final_teardown(0);
return cmocka_run_group_tests(tests, NULL, NULL);
}
8.2 Multithreaded Testing
#include <pthread.h>
static void *thread_func(void *arg) {
// Thread testing logic
return NULL;
}
static void test_multithreaded(void **state) {
(void)state;
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// Validate multithreaded operation results
}
8.3 Platform-Specific Issues
// Handle endianness differences
static void test_endianness(void **state) {
(void)state;
uint32_t value = 0x12345678;
uint8_t *bytes = (uint8_t *)&value;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
assert_int_equal(bytes[0], 0x78);
#else
assert_int_equal(bytes[0], 0x12);
#endif
}
9. Summary and Further Reading
CMocka, as a lightweight solution for unit testing in C, has the following core advantages:
- Easy to Use: Intuitive API design with a gentle learning curve
- Feature-Rich: Supports advanced features like mock objects and parameter validation
- Resource-Friendly: Suitable for resource-constrained environments like embedded systems
- Flexible Extension: Can be integrated with various build systems and CI tools
Recommended Extension Directions:
- Integration with coverage tools (e.g., gcov/lcov)
- Development of domain-specific extensions (e.g., hardware simulation for embedded systems)
- Combining static analysis tools to enhance test effectiveness
- Exploring interoperability with C++ testing frameworks
For C developers, CMocka provides all the tools needed to build reliable and maintainable test suites, serving as an important guarantee of code quality.