رفتن به محتوا

تست، محک‌زنی و سنیتایزرها

تست‌های واحد با GoogleTest بنویسید، با Google Benchmark محک‌زنی کنید، باگ‌ها را با سنیتایزرها (ASan، UBSan، TSan) بگیرید، و تحلیل ایستا را در خط لوله CI/CD خود ادغام کنید.

هرم تست برای ‎C++‎

توسعه حرفه‌ای ‎C++‎ بر چندین لایه تضمین کیفیت تکیه دارد: تست‌های واحد توابع و کلاس‌های منفرد را تأیید می‌کنند، تست‌های یکپارچه‌سازی تعاملات مؤلفه‌ها را بررسی می‌کنند، سنیتایزرها خطاهای حافظه و رفتار تعریف‌نشده را در زمان اجرا می‌گیرند، تحلیل ایستا باگ‌ها را بدون اجرای کد پیدا می‌کند، و محک‌ها اطمینان می‌دهند عملکرد الزامات را برآورده می‌کند. این ابزارها با هم شبکه ایمنی تشکیل می‌دهند که باگ‌ها را قبل از رسیدن به محیط تولیدی می‌گیرد.

دو چارچوب تست ‎C++‎ محبوب‌تر GoogleTest (از گوگل، به‌طور گسترده در صنعت استفاده می‌شود) و Catch2 (فقط هدر، نحو به سبک BDD) هستند. هر دو به‌خوبی با CMake و سیستم‌های CI/CD ادغام می‌شوند.

تست واحد با GoogleTest

GoogleTest ماکروهایی برای تأییدیه‌ها فراهم می‌کند (EXPECT_* برای غیرکشنده، ASSERT_* برای کشنده)، فیکسچرهای تست برای آماده‌سازی/پاکسازی مشترک، و تست‌های پارامتریک:

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
}

ادغام تست‌ها با CMake

GoogleTest را با CMake تنظیم کنید و تست‌ها را از طریق CTest، اجراکننده تست داخلی، اجرا کنید:

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)

سنیتایزرها: تشخیص باگ در زمان اجرا

سنیتایزرها بررسی‌های زمان اجرای ابزاردهی‌شده توسط کامپایلر هستند که کلاس‌های کاملی از باگ‌ها را می‌گیرند. آن‌ها را با فلگ‌های کامپایلر فعال کنید — حدود ۲ برابر کاهش سرعت اضافه می‌کنند اما باگ‌هایی را می‌گیرند که هیچ مقدار تست بدون آن‌ها پیدا نمی‌کند:

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)

تحلیل ایستا و پوشش کد

تحلیل ایستا کد شما را بدون اجرا بررسی می‌کند و باگ‌ها، نقض‌های سبک و مسائل امنیتی را شناسایی می‌کند:

clang-tidy — قدرتمندترین لینتر ‎C++‎. فرصت‌های مدرن‌سازی، مسائل عملکردی، خوانایی و الگوهای مستعد باگ را بررسی می‌کند:

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

cppcheck — مکمل clang-tidy، کلاس‌های باگ متفاوتی را می‌گیرد:

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

پوشش کد — اندازه‌گیری کنید تست‌هایتان واقعاً کدام خطوط را اجرا می‌کنند:

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

برای مسیرهای کد حیاتی هدف بیش از ۸۰٪ پوشش خطی را داشته باشید. ۱۰۰٪ پوشش به‌ندرت ارزش تلاش را دارد — بر تست منطق، حالات لبه‌ای و مدیریت خطا تمرکز کنید.

بهترین شیوه

یک راهبرد تست جامع بسازید:

1. تست‌ها را همراه کد بنویسید — نه بعد از آن. توسعه تست‌محور (TDD) برای ‎C++‎ به‌خوبی کار می‌کند: یک تست شکست‌خورده بنویسید، حداقل کد برای موفقیت را پیاده‌سازی کنید، بازسازی (Refactor) کنید.
2. سنیتایزرها را در CI اجرا کنید — کارهای CI جداگانه‌ای برای ASan+UBSan و TSan داشته باشید. این‌ها باگ‌هایی را می‌گیرند که تست‌ها به‌تنهایی از دست می‌دهند.
3. از فیکسچرهای تست استفاده کنید برای آماده‌سازی/پاکسازی منابع مشترک (اتصالات پایگاه داده، دسته‌های فایل، اشیای پیچیده).
4. حالات لبه‌ای و مسیرهای خطا را تست کنید — ورودی‌های تهی، ظروف خالی، سرریز عدد صحیح، شکست تخصیص.
5. تست‌ها را سریع نگه دارید — تست‌های واحد باید در ثانیه‌ها اجرا شوند. تست‌های کند (شبکه، دیسک) را به مجموعه‌های تست یکپارچه‌سازی منتقل کنید.
6. قبل از بهینه‌سازی محک‌زنی کنید — اول اندازه‌گیری کنید، سپس بهینه‌سازی کنید. حدس نزنید گلوگاه‌ها کجا هستند.

دام

تست پیاده‌سازی به‌جای رفتار: تست‌هایی که به ساختارهای داده داخلی وابسته هستند وقتی بازسازی می‌کنید خراب می‌شوند. API عمومی و رفتار قابل مشاهده را تست کنید، نه متدهای خصوصی.

نادیده‌گرفتن یافته‌های سنیتایزر: گزارش‌های سنیتایزر مثبت کاذب نیستند (با استثناهای بسیار نادر). هر گزارش ASan یا UBSan نشان‌دهنده رفتار تعریف‌نشده واقعی است که بر روی پلتفرم یا سطح بهینه‌سازی دیگری مشکل ایجاد خواهد کرد.

تست نکردن با بهینه‌سازی‌ها: برخی باگ‌ها فقط با -O2 یا -O3 بروز می‌کنند (مخصوصاً رفتار تعریف‌نشده‌ای که بهینه‌ساز از آن بهره می‌برد). مجموعه تست خود را با هر دو بیلد Debug و Release اجرا کنید.

تست‌های ناپایدار: تست‌هایی که گاهی موفق و گاهی شکست می‌خورند بدتر از نداشتن تست هستند — اعتماد به مجموعه تست را از بین می‌برند. تست‌های ناپایدار را فوراً اصلاح یا حذف کنید.

نکات کلیدی
  • GoogleTest ماکروهای EXPECT_* (غیرکشنده) و ASSERT_* (کشنده)، فیکسچرها و تست‌های پارامتریک فراهم می‌کند
  • از gtest_discover_tests() در CMake برای ثبت خودکار تست‌ها با CTest استفاده کنید
  • ASan خطاهای حافظه، UBSan رفتار تعریف‌نشده و TSan مسابقه داده را می‌گیرد — همه را در CI اجرا کنید
  • تحلیل ایستا (clang-tidy، cppcheck) باگ‌ها را بدون اجرای کد می‌گیرد
  • رفتار را تست کنید، نه پیاده‌سازی — و همیشه حالات لبه‌ای و مسیرهای خطا را تست کنید
  • تست‌ها را با هر دو بیلد Debug و Release اجرا کنید تا باگ‌های وابسته به بهینه‌سازی را بگیرید

آزمون — دانش خود را بسنجید

(15 امتیاز)

1. تفاوت بین `EXPECT_EQ` و `ASSERT_EQ` در GoogleTest چیست؟

2. کدام سنیتایزر باگ‌های استفاده‌پس‌ازآزادسازی و سرریز بافر را تشخیص می‌دهد؟

3. چرا باید تست‌ها را با هر دو بیلد Debug و Release اجرا کنید؟