Lesson 22-C Language Testing and Quality Assurance

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 -Wall or -Wextra when 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 printf statements 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.

Share your love