مقدمة: لماذا يعتبر الاختبار أمرًا بالغ الأهمية لمشاريع C++
اختبار كود C++ ليس مجرد إجراء شكلي، بل هو ممارسة أساسية تضمن استقرار البرمجيات وأمانها وقابليتها للصيانة. على عكس اللغات المفسرة، حيث يمكن أن يظهر الخطأ فورًا، في C++ غالبًا ما تظل الأخطاء (bugs) مخفية حتى وقت البناء (build) أو التنفيذ في ظروف محددة. يمكن أن يؤدي تسرب الذاكرة (memory leak) الذي تم تجاهله، أو سلوك غير محدد (undefined behavior)، أو التعامل غير الصحيح مع المؤشرات (pointers) إلى أعطال قاتلة.
تميز صناعة التطوير الحديثة ثلاثة مستويات رئيسية للاختبار: Unit (اختبار الوحدات)، Integration (اختبار التكامل)، و E2E (الاختبار الشامل). كل منها يحل مشكلته الخاصة، وتشكل معًا "هرم الاختبار" (testing pyramid) الذي يسمح باكتشاف ما يصل إلى 90% من الأخطاء قبل الإصدار (production).
في هذه المقالة، سنقوم بتحليل كل نوع من أنواع الاختبارات من خلال أمثلة عملية بلغة C++، باستخدام أطر العمل الشائعة Google Test و Catch2، وسنناقش أفضل الممارسات لاستخدامها.
1. اختبار الوحدات (Unit Testing): التحقق من الوحدات الذرية
اختبارات الوحدات (Unit tests) هي المستوى الأدنى والأسرع. إنها تتحقق من الدوال (functions) أو الفئات (classes) أو الطرق (methods) الفردية بمعزل عن بقية النظام. الهدف هو التأكد من أن كل "لبنة" في الكود تعمل بشكل صحيح.
مثال على اختبار وحدة باستخدام Google Test
لننظر إلى دالة رياضية بسيطة:
// math_utils.h#pragma once
namespace MathUtils { int add(int a, int b); int divide(int a, int b);}// math_utils.cpp#include "math_utils.h"
namespace MathUtils { int add(int a, int b) { return a + b; }
int divide(int a, int b) { if (b == 0) { throw std::invalid_argument("Division by zero"); } return a / b; }}الآن سنكتب اختبارات الوحدة باستخدام Google Test:
// test_math_utils.cpp#include <gtest/gtest.h>#include "math_utils.h"
TEST(MathUtilsTest, AddPositiveNumbers) { EXPECT_EQ(MathUtils::add(2, 3), 5); EXPECT_EQ(MathUtils::add(0, 0), 0);}
TEST(MathUtilsTest, AddNegativeNumbers) { EXPECT_EQ(MathUtils::add(-1, -1), -2); EXPECT_EQ(MathUtils::add(-5, 3), -2);}
TEST(MathUtilsTest, DivideNormal) { EXPECT_EQ(MathUtils::divide(10, 2), 5); EXPECT_EQ(MathUtils::divide(7, 3), 2); // قسمة الأعداد الصحيحة}
TEST(MathUtilsTest, DivideByZeroThrows) { EXPECT_THROW(MathUtils::divide(5, 0), std::invalid_argument);}استخدام الكائنات الوهمية (Mock Objects)
لعزل الوحدة، غالبًا ما يكون من الضروري استبدال التبعيات. يسمح Google Mock (جزء من Google Test) بإنشاء نماذج بديلة (stubs):
// database_interface.hclass DatabaseInterface {public: virtual ~DatabaseInterface() = default; virtual bool save(const std::string& data) = 0;};
// user_service.cppclass UserService { DatabaseInterface& db;public: explicit UserService(DatabaseInterface& db) : db(db) {} bool registerUser(const std::string& name) { return db.save("user:" + name); }};
// test_user_service.cpp#include <gmock/gmock.h>
class MockDatabase : public DatabaseInterface {public: MOCK_METHOD(bool, save, (const std::string&), (override));};
TEST(UserServiceTest, RegisterUserCallsDb) { MockDatabase mockDb; EXPECT_CALL(mockDb, save("user:Alice")).WillOnce(testing::Return(true));
UserService service(mockDb); EXPECT_TRUE(service.registerUser("Alice"));}الإيجابيات: سرعة التنفيذ (بالمللي ثانية)، تحديد دقيق لموقع الأخطاء، إمكانية التشغيل دون اعتماديات خارجية.
السلبيات: لا تتحقق من التفاعل بين المكونات الحقيقية.
2. اختبار التكامل (Integration Testing): التحقق من ترابط المكونات
تتحقق اختبارات التكامل (Integration tests) من كيفية عمل عدة م