Chuyển đến nội dung

Kiểm tra, đo điểm chuẩn và khử trùng

Viết bài kiểm thử đơn vị bằng GoogleTest, đo điểm chuẩn bằng Google Benchmark, phát hiện lỗi bằng công cụ khử trùng (ASan, UBSan, TSan) và tích hợp phân tích tĩnh vào quy trình CI/CD của bạn.

Kim tự tháp thử nghiệm cho C++

Quá trình phát triển C++ chuyên nghiệp dựa trên nhiều lớp đảm bảo chất lượng: kiểm tra đơn vị xác minh các chức năng và lớp riêng lẻ, kiểm tra tích hợp kiểm tra tương tác thành phần, trình dọn dẹp phát hiện lỗi bộ nhớ và hành vi không xác định trong thời gian chạy, phân tích tĩnh tìm thấy lỗi mà không cần chạy mã và điểm chuẩn đảm bảo hiệu suất đáp ứng yêu cầu. Cùng với nhau, những công cụ này tạo thành một mạng lưới an toàn giúp phát hiện lỗi trước khi chúng được đưa vào sản xuất.

Hai khung kiểm tra C++ phổ biến nhất là GoogleTest (của Google, được sử dụng rộng rãi trong ngành) và Catch2 (cú pháp kiểu BDD, chỉ có tiêu đề). Cả hai đều tích hợp tốt với hệ thống CMake và CI/CD.

Kiểm tra đơn vị với GoogleTest

GoogleTest cung cấp macro để xác nhận (EXPECT_*cho trường hợp không gây tử vong,ASSERT_*cho trường hợp tử vong), các thiết bị kiểm tra để thiết lập/phân hủy chung và các kiểm tra được tham số hóa:

test_factorial.cpp
#include <gtest/gtest.h>
#include <vector>
#include <stdexcept>

// Function under test
int factorial(int n) {
    if (n < 0) throw std::invalid_argument("negative input");
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

// Basic test — TEST(TestSuite, TestName)
TEST(FactorialTest, HandlesZero) {
    EXPECT_EQ(factorial(0), 1);
}

TEST(FactorialTest, HandlesPositive) {
    EXPECT_EQ(factorial(1), 1);
    EXPECT_EQ(factorial(5), 120);
    EXPECT_EQ(factorial(10), 3628800);
}

TEST(FactorialTest, ThrowsOnNegative) {
    EXPECT_THROW(factorial(-1), std::invalid_argument);
}

// Test fixture — shared setup/teardown
class StackTest : public ::testing::Test {
protected:
    std::vector<int> stack;

    void SetUp() override {
        stack.push_back(1);
        stack.push_back(2);
        stack.push_back(3);
    }

    void TearDown() override {
        stack.clear();
    }
};

TEST_F(StackTest, TopElement) {
    EXPECT_EQ(stack.back(), 3);
}

TEST_F(StackTest, PopReducesSize) {
    stack.pop_back();
    EXPECT_EQ(stack.size(), 2);
    EXPECT_EQ(stack.back(), 2);
}

TEST_F(StackTest, IsNotEmpty) {
    ASSERT_FALSE(stack.empty());  // fatal — stops test if false
    EXPECT_GT(stack.size(), 0u);  // non-fatal — continues on failure
}

Tích hợp thử nghiệm với CMake

Thiết lập GoogleTest bằng CMake và chạy thử nghiệm thông qua CTest, trình chạy thử nghiệm tích hợp sẵn:

CMakeLists.txt
# CMakeLists.txt for testing
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Fetch GoogleTest
include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

# Main library
add_library(mylib src/math.cpp src/utils.cpp)
target_include_directories(mylib PUBLIC include)

# Test executable
enable_testing()
add_executable(unit_tests
    tests/test_math.cpp
    tests/test_utils.cpp
)
target_link_libraries(unit_tests PRIVATE mylib GTest::gtest_main)

# Register tests with CTest
include(GoogleTest)
gtest_discover_tests(unit_tests)

# Run tests:
# cmake -B build && cmake --build build
# cd build && ctest --output-on-failure --parallel $(nproc)

Chất khử trùng: Phát hiện lỗi trong thời gian chạy

Trình dọn dẹp là các công cụ kiểm tra thời gian chạy được biên dịch dựa trên công cụ để phát hiện toàn bộ các loại lỗi. Kích hoạt chúng bằng cờ trình biên dịch - chúng làm chậm khoảng ~2 lần nhưng phát hiện các lỗi mà không có số lượng thử nghiệm nào có thể tìm thấy nếu không có chúng:

terminal
# AddressSanitizer (ASan) — catches memory errors
# Buffer overflows, use-after-free, double-free, memory leaks
cmake -B build-asan \
    -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"

# UndefinedBehaviorSanitizer (UBSan) — catches UB
# Signed overflow, null dereference, misaligned access, shift errors
cmake -B build-ubsan \
    -DCMAKE_CXX_FLAGS="-fsanitize=undefined -fno-omit-frame-pointer -g" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=undefined"

# ThreadSanitizer (TSan) — catches data races
# Data races, lock-order inversions, deadlocks
cmake -B build-tsan \
    -DCMAKE_CXX_FLAGS="-fsanitize=thread -g" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"

# MemorySanitizer (MSan) — catches uninitialized reads (Clang only)
cmake -B build-msan \
    -DCMAKE_CXX_FLAGS="-fsanitize=memory -fno-omit-frame-pointer -g" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=memory"

# Combine ASan + UBSan (commonly done together)
cmake -B build-asan-ubsan \
    -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"

# IMPORTANT: ASan and TSan cannot be combined — use separate builds
# IMPORTANT: Always compile ALL code with sanitizer flags (including libraries)

Phân tích tĩnh & Phạm vi mã

Phân tích tĩnh kiểm tra mã của bạn mà không cần chạy, phát hiện lỗi, vi phạm kiểu và các vấn đề bảo mật:

clang-tidy — Kẻ nói dối C++ mạnh mẽ nhất. Kiểm tra các cơ hội hiện đại hóa, các vấn đề về hiệu suất, khả năng đọc và các mẫu dễ bị lỗi:

clang-tidy src/*.cpp -- -std=c++20 -I include
# Or with CMake:
cmake -B build -DCMAKE_CXX_CLANG_TIDY="clang-tidy;-checks=*,-fuchsia-*"

cppcheck — Complementary to clang-tidy, catches different bug classes:

cppcheck --enable=all --std=c++20 src/

Code Coverage — Measure which lines your tests actually execute:

cmake -B build-cov -DCMAKE_CXX_FLAGS="--coverage -g -O0"
cmake --build build-cov && cd build-cov && ctest
gcovr --html-details coverage.html -r ../src

Đặt mục tiêu 80%+ phạm vi phủ sóng trên các đường dẫn mã quan trọng. Mức độ bao phủ 100% hiếm khi đáng nỗ lực — hãy tập trung vào kiểm tra logic, các trường hợp khó khăn và xử lý lỗi.

Thực hành tốt

Xây dựng chiến lược thử nghiệm toàn diện:

1. Viết bài kiểm tra cùng với mã - không phải sau. Phát triển dựa trên thử nghiệm (TDD) hoạt động tốt cho C++: viết một bài kiểm tra thất bại, triển khai mã tối thiểu để vượt qua, tái cấu trúc.
2. Chạy chất khử trùng trong CI — có các công việc CI riêng biệt cho Asan+UBSan và TSan. Những lỗi này bắt được các lỗi mà chỉ riêng các bài kiểm tra đã bỏ sót.
3. Sử dụng các thiết bị kiểm tra để thiết lập/phân tách các tài nguyên dùng chung (kết nối cơ sở dữ liệu, xử lý tệp, các đối tượng phức tạp).
4. Các trường hợp biên kiểm tra và đường dẫn lỗi — đầu vào rỗng, vùng chứa trống, tràn số nguyên, lỗi phân bổ.
5. Giữ các bài kiểm tra nhanh — các bài kiểm tra đơn vị sẽ chạy trong vài giây. Di chuyển các bài kiểm tra chậm (mạng, đĩa) sang bộ kiểm tra tích hợp.
6. Điểm chuẩn trước khi tối ưu hóa — đo lường trước, sau đó tối ưu hóa. Đừng đoán nút thắt ở đâu.

Lỗi thường gặp

Việc triển khai thử nghiệm, không phải hành vi: Các thử nghiệm phụ thuộc vào cấu trúc dữ liệu nội bộ bị hỏng khi bạn tái cấu trúc. Kiểm tra API công khai và hành vi có thể quan sát được, không phải phương pháp riêng tư.

Bỏ qua các phát hiện về chất khử trùng: Các báo cáo về chất khử trùng KHÔNG có kết quả dương tính giả (với rất ít trường hợp ngoại lệ). Mọi báo cáo của ASan hoặc UBSan đều chỉ ra hành vi thực sự không xác định SẼ gây ra sự cố trên một số nền tảng hoặc mức độ tối ưu hóa.

Không thử nghiệm với các tính năng tối ưu hóa: Một số lỗi chỉ xuất hiện với-O2hoặc-O3(đặc biệt là UB mà trình tối ưu hóa khai thác). Chạy bộ thử nghiệm của bạn với cả bản dựngDebugvàRelease.

Thử nghiệm không ổn định: Các thử nghiệm đôi khi đạt và đôi khi thất bại còn tệ hơn là không có thử nghiệm nào — chúng làm xói mòn niềm tin vào bộ thử nghiệm. Sửa chữa hoặc xóa các bài kiểm tra không ổn định ngay lập tức.

Điểm chính
  • GoogleTest cung cấp các macro, cố định và kiểm tra tham sốEXPECT_*(không gây tử vong) vàASSERT_*(gây tử vong)
  • Sử dụnggtest_discover_tests()trong CMake để tự động đăng ký bài kiểm tra với CTest
  • ASan phát hiện lỗi bộ nhớ, UBSan phát hiện hành vi không xác định, TSan phát hiện các cuộc đua dữ liệu - chạy tất cả trong CI
  • Phân tích tĩnh (clang-tidy, cppcheck) phát hiện lỗi mà không cần chạy mã
  • Kiểm tra hành vi chứ không phải việc triển khai - và luôn kiểm tra các trường hợp khó khăn và đường dẫn lỗi
  • Chạy thử nghiệm với cả bản dựng Gỡ lỗi và Bản phát hành để phát hiện các lỗi phụ thuộc vào việc tối ưu hóa

Câu đố — Kiểm tra kiến thức

(15 XP)

1. Sự khác biệt giữa`EXPECT_EQ`và`ASSERT_EQ`trong GoogleTest là gì?

2. Chất khử trùng nào phát hiện lỗi use-after-free và lỗi tràn bộ đệm?

3. Tại sao bạn nên chạy thử nghiệm với cả bản dựng Gỡ lỗi và Bản phát hành?