Unit Testing Frameworks
Unit Testing
Unit testing involves testing the smallest testable units in a program, typically functions or modules. In C, unit testing usually requires writing test cases to verify whether a function behaves as expected.
Example Code Suppose we have a simple function add, we will write unit tests for it.
#include <stdio.h>
// Function definition
int add(int a, int b) {
return a + b;
}
// Main function
int main() {
printf("Testing add function...\n");
// Test cases
if (add(1, 2) == 3) {
printf("Test 1 passed.\n");
} else {
printf("Test 1 failed.\n");
}
if (add(-1, -1) == -2) {
printf("Test 2 passed.\n");
} else {
printf("Test 2 failed.\n");
}
if (add(0, 0) == 0) {
printf("Test 3 passed.\n");
} else {
printf("Test 3 failed.\n");
}
return 0;
}
Unit Testing Frameworks
MinUnit
Features:
- Extremely lightweight.
- No additional library dependencies.
- Creates test cases via macro definitions.
Usage:
#include "minunit.h"
int add(int a, int b) {
return a + b;
}
MU_TEST(test_addition) {
MU_ASSERT_EQUAL(add(1, 2), 3);
MU_ASSERT_EQUAL(add(-1, -1), -2);
}
MU_RUN_TESTS(MU_TESTS);
int main() {
MU_RUN_TESTS(MU_TESTS);
return 0;
}
CUnit
Features:
- Relatively comprehensive functionality.
- Supports multiple test types (e.g., regression testing).
- Can generate detailed test reports.
Usage: First, you need to install CUnit. On Ubuntu, use the following command:
sudo apt-get install libcunit1 libcunit1-doc libcunit1-dev
Then, you can write test cases:
#include <CUnit/CUnit.h>
void test_addition(void) {
CU_ASSERT_EQUAL(add(1, 2), 3);
CU_ASSERT_EQUAL(add(-1, -1), -2);
}
int main(void) {
CU_initialize_registry();
CU_pSuite suite = CU_add_suite("Addition Suite", NULL, NULL);
CU_add_test(suite, "test_addition", test_addition);
CU_basic_run_tests();
CU_cleanup_registry();
return 0;
}
CuTest
Features:
- Very compact.
- Includes basic testing functionality.
- Suitable for embedded systems.
Usage:
#include "CuTest.h"
void test_addition(CuTest *testCase) {
CuAssertIntEquals(testCase, 3, add(1, 2));
CuAssertIntEquals(testCase, -2, add(-1, -1));
}
int main(void) {
CuString *output = CuStringNew();
CuTestResult *result = CuTestResultNew();
CuTest *test = CuTestNew(output, result);
CuSuite *suite = CuSuiteNew();
SUITE_ADD_TEST(suite, test_addition);
CuSuiteRun(suite);
CuSuiteSummary(suite, output);
CuSuiteDetails(suite, output);
CuTestResultDestroy(result);
CuTestFree(test);
CuStringDelete(output);
CuSuiteDelete(suite);
return 0;
Check
Features:
- Supports multiple test types.
- Provides a rich assertion library.
- Supports test coverage analysis.
Usage: First, you need to install Check. On Ubuntu, use the following command:
sudo apt-get install check
Then, you can write test cases:
#include <check.h>
void test_addition(t_case *tc) {
t_assert(tc, 3 == add(1, 2), "Addition of 1 and 2 should be 3");
t_assert(tc, -2 == add(-1, -1), "Addition of -1 and -1 should be -2");
}
int main(void) {
t_suite *s = t_suite_new("Addition Suite", NULL, NULL);
t_case *tc = t_case_new("test_addition");
t_case_set_tc_fn(tc, test_addition);
t_suite_add_tcase(s, tc);
t_run_suite(s, NULL, 1);
t_suite_free(s);
return 0;
}
Googletest
Although Googletest is primarily used for C++, it can also be used for C language testing.
Features:
- Highly configurable.
- Supports multiple assertions.
- Detailed test reports.
Usage: First, you need to install Googletest. On Ubuntu, use the following commands:
git clone https://github.com/google/googletest.git
cd googletest
mkdir build
cd build
cmake ..
make
sudo make install
Then, you can write test cases:
#include "gtest/gtest.h"
TEST(AdditionTest, Basic) {
EXPECT_EQ(add(1, 2), 3);
EXPECT_EQ(add(-1, -1), -2);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Code Analysis and Debugging
Static Analysis Tools
Static analysis tools detect potential issues without executing the code, such as uninitialized variables, pointer errors, etc.
Recommended Tools
- Valgrind: A powerful tool suite including Memcheck, Cachegrind, etc.
- Clang Static Analyzer: A static analysis tool that detects various issues.
- GCC Warnings: Enable more warnings using
-Wallor-Wextrawhen compiling with GCC.
Example Use Valgrind’s Memcheck to detect memory leaks:
valgrind --leak-check=yes ./your_program
Dynamic Analysis Tools
Dynamic analysis tools detect issues during program execution, such as memory leaks, illegal memory access, etc.
Recommended Tools
- Valgrind: Also used for dynamic analysis.
- AddressSanitizer: A fast memory error detector that can be used as part of the compiler.
Example Compile and run the program with AddressSanitizer:
gcc -fsanitize=address your_program.c -o your_program
./your_program
Debugging Techniques
Debugging is the process of locating and fixing program errors.
Common Debugging Commands
- printf: Insert
printfstatements in the code to output variable values. - GDB: GNU Debugger, a powerful debugging tool.
Example Debug the program using GDB:
gdb ./your_program
Set a breakpoint and run the program in GDB:
(gdb) break main
(gdb) run
View variable values:
(gdb) print a
Code Review
Code review is a method of discovering errors through peer review of code.
Practical Suggestions
- Code Standards: Ensure team members follow consistent coding style.
- Code Review Tools: Use tools like GitHub, GitLab, etc., for code review.
- Automated Testing: Combine unit testing and other automated tests.
Coverage Testing
Coverage testing measures how much code is covered by test cases.
Recommended Tools
- gcov: GCC’s code coverage tool.
- lcov: A tool for generating HTML reports.
Example Use gcov and lcov for coverage testing:
gcc -fprofile-arcs -ftest-coverage your_program.c -o your_program
./your_program
gcov your_program.c
genhtml your_program.c.gcov -o coverage_report
Dynamic Testing and Stress Testing
Dynamic Testing
Dynamic testing is performed while the software is running, aiming to verify the software’s functionality, performance, and stability. Dynamic testing typically includes but is not limited to unit testing, integration testing, system testing, and acceptance testing.
Features
- Runtime Testing: Test the software in the actual runtime environment.
- Functional Verification: Verify whether the software functions as expected.
- Performance Evaluation: Test software performance under different loads.
- Stability Testing: Ensure the software remains stable during prolonged operation.
Example Suppose we have a simple C program that includes a factorial calculation function. We can use dynamic testing to verify the correctness of this function.
#include <stdio.h>
#include <assert.h>
// Calculate factorial
long factorial(long n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
int main() {
// Dynamic testing
assert(factorial(0) == 1); // 0! = 1
assert(factorial(1) == 1); // 1! = 1
assert(factorial(5) == 120); // 5! = 120
assert(factorial(10) == 3628800); // 10! = 3628800
printf("All tests passed!\n");
return 0;
}
In this example, we used the assert function for dynamic testing. If any assertion fails, the program terminates immediately and displays an error message.
Stress Testing
Stress testing is a special form of dynamic testing that focuses on testing software behavior under extreme conditions. This testing typically simulates harsher environments than normal operating conditions to ensure the software can function properly under such conditions.
Purpose
- Performance Limits: Determine the maximum load capacity of the software.
- Stability: Check software stability under high load.
- Resource Usage: Monitor resource consumption, such as CPU, memory, etc.
Example Suppose we have a web service, and we need to perform stress testing to ensure it can handle a large number of concurrent requests.
Tools
- Apache JMeter: A widely used open-source tool for stress testing web applications.
- LoadRunner: A commercial tool for simulating a large number of concurrent user accesses.
- Gatling: A high-performance stress testing tool written in Scala.
Example A simple example of stress testing using Apache JMeter:
Install Apache JMeter:
sudo apt-get install jmeter
Create a Test Plan:
- Open JMeter and create a new test plan.
- Add an HTTP Request sampler and configure the target URL.
- Set the number of threads and loop count.
- Add listeners such as “Aggregate Report” or “View Results Tree” to view test results.
Run the Test:
- Save the test plan and run the test.
- Observe the results and analyze performance metrics.



