اختبار العقود الذكية محلياً: كتابة اختبارات الوحدة (Unit Tests) باستخدام Chai & Mocha

دقائق القراءة: 6

اختبار العقود الذكية محلياً: كتابة اختبارات الوحدة (Unit Tests) باستخدام Chai & Mocha

يُعد اختبار العقود الذكية محلياً خطوة أساسية قبل أي نشر على شبكة فعلية أو حتى على Testnet. فعلى عكس التطبيقات التقليدية، أي خطأ في Smart Contracts قد يؤدي إلى خسائر مالية مباشرة أو سلوك غير قابل للإصلاح بعد النشر. لذلك فإن اختبارات الوحدة ليست مجرد تحسين للجودة، بل هي طبقة حماية هندسية لا غنى عنها في أي مشروع Web3.

في هذا الدليل سنشرح كيف تكتب اختبارات احترافية باستخدام Mocha كإطار لتنظيم سيناريوهات الاختبار، وChai لكتابة التوقعات والتحقق من النتائج، ضمن بيئة الانتقال إلى بيئة العمل الاحترافية: تثبيت إطار عمل Hardhat باستخدام Node.js وإعداد مشروع Hardhat وكتابة أول سكربت JavaScript لترجمة (Compile) العقد الذكي.

لماذا نختبر العقد الذكي محلياً قبل النشر؟

الاختبار المحلي يمنحك بيئة سريعة، قابلة للتكرار، ومنخفضة التكلفة. بدلاً من استهلاك Gas Fees عند كل تجربة، يمكنك تشغيل مئات الحالات خلال ثوانٍ على شبكة تطوير داخلية تحاكي EVM.

اختبارات الوحدة تساعدك على التحقق من:

  • صحة القيم الابتدائية داخل State Variables.
  • سلوك الدوال عند الإدخال الصحيح والخاطئ.
  • التعامل السليم مع الصلاحيات وModifiers.
  • إطلاق Events عند تنفيذ العمليات.
  • فشل المعاملات التي يجب أن تفشل باستخدام revert أو require.

الاعتماد على الاختبار اليدوي فقط عبر Remix IDE مفيد في البدايات، لكنه غير كافٍ للمشاريع الجادة. الاختبارات الآلية توفر توثيقاً حياً للسلوك المتوقع، وتكشف الانكسارات البرمجية فور تعديل الكود.

العقد الذكي الذي سنختبره

سنستخدم عقداً بسيطاً يمثل خزنة مصغرة، بحيث يستطيع المالك فقط تحديث رقم مخزن، بينما يمكن للجميع قراءة القيمة. هذا المثال مناسب لتطبيق مفاهيم أساسيات لغة Solidity: أنواع البيانات والمتغيرات (State Variables) والدوال (Functions) في Solidity: من يمكنه قراءة وتعديل بيانات العقد؟ والمعدلات (Modifiers): حماية الدوال برمجياً.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract CounterVault {
    address public owner;
    uint256 private value;

    event ValueUpdated(address indexed updatedBy, uint256 newValue);

    constructor(uint256 initialValue) {
        owner = msg.sender;
        value = initialValue;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    function setValue(uint256 newValue) external onlyOwner {
        value = newValue;
        emit ValueUpdated(msg.sender, newValue);
    }

    function increment() external onlyOwner {
        value += 1;
        emit ValueUpdated(msg.sender, value);
    }

    function getValue() external view returns (uint256) {
        return value;
    }
}

بنية ملف الاختبار داخل مشروع Hardhat

بعد إنشاء المشروع، ضع العقد داخل مجلد contracts، ثم أنشئ ملف الاختبار داخل مجلد test. غالباً سيكون الاسم مثل CounterVault.js.

فلسفة الاختبارات في Mocha تعتمد على تجميع السيناريوهات داخل describe، وكتابة كل حالة منفصلة داخل it. أما Chai فتُستخدم عبر expect للتحقق من القيم والرسائل والأحداث.

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("CounterVault", function () {
    let counterVault;
    let owner;
    let user;

    beforeEach(async function () {
        [owner, user] = await ethers.getSigners();

        const CounterVault = await ethers.getContractFactory("CounterVault");
        counterVault = await CounterVault.deploy(10);
        await counterVault.waitForDeployment();
    });

    it("should set the deployer as owner", async function () {
        expect(await counterVault.owner()).to.equal(owner.address);
    });

    it("should store the initial value", async function () {
        expect(await counterVault.getValue()).to.equal(10);
    });

    it("should allow owner to update value", async function () {
        await counterVault.setValue(25);
        expect(await counterVault.getValue()).to.equal(25);
    });

    it("should revert if non-owner tries to update value", async function () {
        await expect(
            counterVault.connect(user).setValue(99)
        ).to.be.revertedWith("Not owner");
    });

    it("should increment the value by one", async function () {
        await counterVault.increment();
        expect(await counterVault.getValue()).to.equal(11);
    });

    it("should emit event when value is updated", async function () {
        await expect(counterVault.setValue(77))
            .to.emit(counterVault, "ValueUpdated")
            .withArgs(owner.address, 77);
    });
});

شرح حالات الاختبار الأساسية

1) اختبار التهيئة الأولية بعد النشر

أول ما يجب التأكد منه هو أن constructor يضبط القيم بشكل صحيح. إذا أخطأ العقد عند لحظة النشر، فكل ما بعد ذلك سيكون مبنياً على حالة ابتدائية فاسدة.

لهذا اختبرنا أن الحساب الناشر تم تعيينه إلى owner، وأن القيمة الأولية أصبحت 10.

2) اختبار المسار الناجح للدوال

الاختبار الجيد لا يركز على الفشل فقط، بل يثبت أن السيناريو الطبيعي يعمل كما هو متوقع. في حالتنا، يجب أن يتمكن المالك من استدعاء setValue وincrement دون مشاكل، مع تحديث الحالة الداخلية فعلياً.

هذا النوع من الاختبارات مهم خصوصاً عند العمل على عقود أعقد مثل معيار ERC-20: ما هي الرموز المميزة (Tokens) وكيف يتم إنشاؤها؟، حيث توجد دوال عديدة يجب أن تنجح تحت شروط محددة.

3) اختبار الفشل المتوقع ورفض المعاملة

في العقود الذكية، الفشل المقصود ليس عيباً، بل جزء من التصميم الأمني. عندما يمنع modifier مستخدماً غير مخول من تنفيذ عملية حساسة، فهذا نجاح أمني.

لهذا نستخدم to.be.revertedWith للتحقق من أن المعاملة رُفضت فعلاً وبالرسالة الصحيحة. هذا يرتبط مباشرة بمفاهيم التعامل مع الأخطاء وإرجاع الأموال: استخدام require, assert, revert.

4) اختبار الأحداث الصادرة من العقد

كثير من تطبيقات DApps تعتمد على Events لتحديث الواجهة أو أرشفة النشاط على مستوى Frontend. لذا يجب ألا تكتفي بالتحقق من تغير القيمة فقط، بل تأكد أيضاً من أن الحدث خرج بالوسائط الصحيحة.

هذا يفيدك لاحقاً عند ربط العقد مع الواجهة كما شرحنا في الأحداث (Events): كيف يخبر العقد الذكي واجهة الموقع (React) بأن شيئاً ما قد حدث؟.

تشغيل الاختبارات وقراءة النتائج

بعد كتابة العقد وملف الاختبار، شغّل الأمر المعتاد داخل المشروع:

npx hardhat test

سيقوم Hardhat بترجمة العقد، تشغيل شبكة محلية مؤقتة، ثم تنفيذ كل حالات الاختبار. إذا فشلت حالة ما، ستظهر لك الرسالة وموضع المشكلة بدقة، ما يسرّع دورة التطوير بشكل كبير.

من الممارسات الجيدة أن تعيد تشغيل الاختبارات بعد كل تعديل صغير، لا بعد الانتهاء من مجموعة تغييرات كبيرة. بهذه الطريقة ستعرف فوراً أي سطر كسر السلوك السابق.

أفضل الممارسات عند كتابة اختبارات احترافية

  • اجعل كل اختبار يركز على سلوك واحد فقط.
  • استخدم beforeEach لإعادة نشر العقد بحالة نظيفة.
  • اختبر النجاح والفشل معاً لكل دالة حساسة.
  • تحقق من Events وليس من القيم فقط.
  • سمّ الاختبارات بجمل واضحة تصف ما يجب أن يحدث.
  • افصل اختبارات الصلاحيات عن اختبارات منطق الأعمال.

من منظور Security Auditing، أخطر العقود ليست تلك التي تحتوي أخطاء واضحة فقط، بل تلك التي تفتقر إلى تغطية اختبارية كافية. كل دالة تعدل الرصيد، الملكية، أو صلاحيات الوصول يجب أن تملك اختبارات صريحة للمسارات الطبيعية والمسارات العدائية.

في جانب Gas Optimization، الاختبارات لا تقيس التكلفة بشكل مباشر دائماً، لكنها تساعدك على اكتشاف الاستدعاءات غير الضرورية، وتمنع إدخال منطق زائد داخل الدوال المعدِّلة للحالة. راجع أيضاً التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟ وأنواع الدوال : فهم view و pure لتوفير رسوم الـ Gas.

متى تنتقل من اختبارات الوحدة إلى اختبارات أكثر تقدماً؟

اختبارات الوحدة ممتازة للتحقق من كل دالة بمعزل نسبي، لكنها ليست النهاية. عندما يبدأ عقدك بالتفاعل مع عقود أخرى، أو مع رموز ERC-20، أو يعتمد على تحويلات Ether، ستحتاج لاحقاً إلى اختبارات تكامل ومحاكاة سيناريوهات متعددة الأطراف.

ومع ذلك، فإن بناء أساس قوي من Unit Tests هو أول معيار احترافي يدل على نضج المشروع البرمجي، ويقلل بدرجة كبيرة الأخطاء قبل الوصول إلى المراجعة الأمنية أو الإطلاق الفعلي.

خلاصة عملية

اختبار العقود الذكية محلياً باستخدام Chai وMocha داخل بيئة Hardhat ليس مجرد تمرين تعليمي، بل ممارسة أساسية لحماية منطق العقد قبل أن يصبح غير قابل للتغيير على الشبكة. ابدأ دائماً باختبار القيم الابتدائية، ثم الصلاحيات، ثم حالات الفشل، ثم الأحداث.

كلما كانت اختباراتك أوضح وأكثر تغطية، أصبح تطويرك أسرع ومراجعتك الأمنية أسهل وثقة المستخدمين أعلى. وهذه هي العقلية الاحترافية الحقيقية في بناء أنظمة Blockchain موثوقة وقابلة للتوسع.

21 comments

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *