نحوه تست قراردادهای هوشمند (smart Contracts) اتریوم

پیش نیازها: درک اولیه از بلاک چین، اتریوم و جاوا اسکریپت.

کد کامل پروژه کار را می توان در Github یافت.

اهمیت تست نرم افزار

اگر می‌خواهید کد به روشی که در نظر گرفته شده است کار کند، آزمایش نرم‌افزار بسیار مهم است. دو نوع کلی تست نرم افزار وجود دارد: تست های واحد (unit tests) و تست های یکپارچه سازی (integration tests).

  • بر روی هر تابع به صورت مجزا تمرکز کنید. : Unit tests
  • تمرکز بر حصول اطمینان از کارکرد چندین بخش کد با هم همانطور که انتظار می رود. : Integration tests

نرم افزار بلاک چین نیز تفاوتی ندارد. ممکن است استدلال شود که برنامه های بلاک چین به دلیل تغییر ناپذیری نیاز به تاکید بیشتری بر آزمایش دارند.

تست های بلاک چین

مجموعه Truffle دو راه برای آزمایش قراردادهای هوشمند استحکام در اختیار ما قرار می دهد: تست های سالیدیتی و تست های جاوا اسکریپت. سوال این است که از کدام یک استفاده کنیم؟

پاسخ هر دو است.

Figure 1: Test structure diagram
Figure 1: Test structure diagram

تست های سالیدیتی

نوشتن تست ها در Solidity به ما این امکان را می دهد که آزمایش های لایه بلاک چین را اجرا کنیم. آن‌ها به آزمایش‌ها اجازه می‌دهند تا قراردادها و عملکردها را طوری فراخوانی کنند که انگار خودشان روی بلاک چین هستند. برای آزمایش رفتار داخلی قراردادهای هوشمند می توانیم:

  • تست های واحد Unit tests را بنویسید تا مقادیر بازگشتی تابع و مقادیر متغیر حالت را بررسی کنید.
  • تست های یکپارچه سازی integration tests را بنویسید که تعاملات بین قراردادها را آزمایش می کند. اینها تضمین می کنند که مکانیسم هایی مانند وراثت و تزریق وابستگی مطابق انتظار عمل کنند.

تست های جاوا اسکریپت

ما همچنین باید مطمئن شویم که قراردادهای هوشمند رفتار خارجی درستی از خود نشان می دهند. برای آزمایش قراردادهای هوشمند خارج از بلاک چین، از Web3js استفاده می کنیم، درست همانطور که DApp ما، این کار را می کند. هنگام فراخوانی قراردادهای هوشمند، باید اطمینان داشته باشیم که قسمت فرانت اند DApp ما به درستی کار خواهد کرد. اینها تحت آزمون های یکپارچه سازی integration tests قرار می گیرند.

پروژه نمونه

کد کامل پروژه را می‌توانید در Github پیدا کنید

ما دو قرارداد هوشمند داریم: Background و EntryPoint.

Background یک قرارداد داخلی است که بخش فرانت اند DApp ما با آن تعامل ندارد.

EntryPointقراردادی است که برای DApp ما طراحی شده است تا با آن تعامل داشته باشد. در کد خود به Background رفرنس می دهد.

قراردادهای هوشمند

pragma solidity >=0.5.0;

contract Background {
    uint[] private values;

    function storeValue(uint value) public {
        values.push(value);
    }

    function getValue(uint initial) public view returns(uint) {
        return values[initial];
    }

    function getNumberOfValues() public view returns(uint) {
        return values.length;
    }
}

Figure 2: Background.sol

در بالا، قرارداد Background خود را می بینیم. این سه تابع را نشان می دهد: storeValue(uint)، getValue(uint) و ()getNumberOfValues. همه این توابع دستورالعمل های ساده ای دارند، بنابراین آزمایش واحد آنها آسان است.

pragma solidity >=0.5.0;

import &quot./Background.sol&quot

contract EntryPoint {
    address public backgroundAddress;

    constructor(address _background) public{
        backgroundAddress = _background;
    }

    function getBackgroundAddress() public view returns (address) {
        return backgroundAddress;
    }

    function storeTwoValues(uint first, uint second) public {
        Background(backgroundAddress).storeValue(first);
        Background(backgroundAddress).storeValue(second);
    }

    function getNumberOfValues() public view returns (uint) {
        return Background(backgroundAddress).getNumberOfValues();
    }
}

Figure 3: EntryPoint.sol

این قرارداد EntryPoint ما است. یک آدرس برای قرارداد Background ما به سازنده constructor تزریق می شود و به عنوان یک متغیر حالت به نام backgroundAddress استفاده می شود. EntryPoint سه تابع را نشان می دهد: getBackgroundAddress()، storeTwoValues(uint, uint) و ()getNumberOfValues.

toreTwoValues(uint, uint) یک تابع را در قرارداد Background دو بار فراخوانی می کند، بنابراین آزمایش واحد این تابع به صورت مجزا دشوار خواهد بود. همین امر برای getNumberOfValues(). اینها موارد خوبی برای تست های ادغام هستند.

سالیدیتی

در Solidity، ما می خواهیم unit tests و integration tests را برای قراردادهای هوشمند خود بنویسیم. بیایید با unit tests شروع کنیم زیرا آنها ساده تر هستند.
این اولین unit tests ما است : TestBackground :

pragma solidity >=0.5.0;

import &quottruffle/Assert.sol&quot
import &quottruffle/DeployedAddresses.sol&quot
import &quot../../../contracts/Background.sol&quot

contract TestBackground {

    Background public background;

    // Run before every test function
    function beforeEach() public {
        background = new Background();
    }

    // Test that it stores a value correctly
    function testItStoresAValue() public {
        uint value = 5;
        background.storeValue(value);
        uint result = background.getValue(0);
        Assert.equal(result, value, &quotIt should store the correct value&quot);
    }

    // Test that it gets the correct number of values
    function testItGetsCorrectNumberOfValues() public {
        background.storeValue(99);
        uint newSize = background.getNumberOfValues();
        Assert.equal(newSize, 1, &quotIt should increase the size&quot);
    }

    // Test that it stores multiple values correctly
    function testItStoresMultipleValues() public {
        for (uint8 i = 0; i < 10; i++) {
            uint value = i;
            background.storeValue(value);
            uint result = background.getValue(i);
            Assert.equal(result, value, &quotIt should store the correct value for multiple values&quot);
        }
    }
}

Figure 4: TestBackground.sol

قرارداد،Backgroundما را آزمایش می‌کند تا مطمئن شود:

  • یک مقدار جدید در آرایهvaluesخود ذخیره می کند.
  • مقادیر را با ایندگس آنها برمی گرداند.
  • چندین مقدار را در آرایهvaluesخود ذخیره می کند.
  • اندازه آرایهvaluesآن را برمی‌گرداند.

اینTestEntryPointاست، با یک unit test به نام ()testItHasCorrectBackground برای قرارداد EntryPointما:

pragma solidity >=0.5.0;

import &quottruffle/Assert.sol&quot
import &quottruffle/DeployedAddresses.sol&quot
import &quot../../../contracts/Background.sol&quot
import &quot../../../contracts/EntryPoint.sol&quot

contract TestEntryPoint {

    // Ensure that dependency injection working correctly
    function testItHasCorrectBackground() public {
        Background backgroundTest = new Background();
        EntryPoint entryPoint = new EntryPoint(address(backgroundTest));
        address expected = address(backgroundTest);
        address target = entryPoint.getBackgroundAddress();
        Assert.equal(target, expected, &quotIt should set the correct background&quot);
    }

}

Figure 5: TestEntryPoint.sol

این تابع تزریق وابستگی را تست می‌کند. همانطور که قبلا ذکر شد، سایر توابع در قراردادEntryPointما نیاز به تعامل باBackgroundدارند، بنابراین ما نمی توانیم آنها را به صورت مجزا آزمایش کنیم.

این توابع در integration tests ما آزمایش می شوند:

pragma solidity >=0.5.0;

import &quottruffle/Assert.sol&quot
import &quottruffle/DeployedAddresses.sol&quot
import &quot../../../contracts/Background.sol&quot
import &quot../../../contracts/EntryPoint.sol&quot

contract TestIntegrationEntryPoint {

    BackgroundTest public backgroundTest;
    EntryPoint public entryPoint;

    // Run before every test function
    function beforeEach() public {
        backgroundTest = new BackgroundTest();
        entryPoint = new EntryPoint(address(backgroundTest));
    }

    // Check that storeTwoValues() works correctly.
    // EntryPoint contract should call background.storeValue()
    // so we use our mock extension BackgroundTest contract to
    // check that the integration workds
    function testItStoresTwoValues() public {
        uint value1 = 5;
        uint value2 = 20;
        entryPoint.storeTwoValues(value1, value2);
        uint result1 = backgroundTest.values(0);
        uint result2 = backgroundTest.values(1);
        Assert.equal(result1, value1, &quotValue 1 should be correct&quot);
        Assert.equal(result2, value2, &quotValue 2 should be correct&quot);
    }

    // Check that entry point calls our mock extension correctly
    // indicating that the integration between contracts is working
    function testItCallsGetNumberOfValuesFromBackground() public {
        uint result = entryPoint.getNumberOfValues();
        Assert.equal(result, 999, &quotIt should call getNumberOfValues&quot);
    }
}

// Extended from Background because values is private in actual Background
// but we're not testing background in this unit test
contract BackgroundTest is Background {
    uint[] public values;

    function storeValue(uint value) public {
        values.push(value);
    }

    function getNumberOfValues() public view returns(uint) {
        return 999;
    }
}

Figure 6: Solidity Integration test

می‌توانیم ببینیم کهTestIntegrationEntryPointاز یکBackgroundبه نامBackgroundTestاستفاده می‌کند که در خط 43 تعریف شده است تا به عنوان قرارداد ساختگی ما عمل کند. این به تست‌های ما امکان می‌دهد بررسی کنند که آیاEntryPointتوابع صحیح را در قراردادbackgroundAddress که به آن ارجاع می‌دهد فراخوانی می‌کند یا خیر.

تست فایل ها در جاوا اسکریپت

در جاوا اسکریپت، یک integration test می‌نویسیم تا مطمئن شویم که قراردادها همانطور که انتظار داریم عمل می‌کنند تا بتوانیم یک DApp بسازیم که از آنها استفاده می‌کند.

در اینجا تست جاوا اسکریپت ما،entryPoint.test.jsآمده است:

const EntryPoint = artifacts.require(&quot./EntryPoint.sol&quot);

require('chai')
    .use(require('chai-as-promised'))
    .should();

contract(&quotEntryPoint&quot, accounts => {
    describe(&quotStoring Values&quot, () => {
        it(&quotStores correctly&quot, async () => {
            const entryPoint = await EntryPoint.deployed();

            let numberOfValues = await entryPoint.getNumberOfValues();
            numberOfValues.toString().should.equal(&quot0&quot);

            await entryPoint.storeTwoValues(2,4);
            numberOfValues = await entryPoint.getNumberOfValues();
            numberOfValues.toString().should.equal(&quot2&quot);
        });
    });
});

Figure 7: entryPoint.test.js

با استفاده از توابع موجود در قراردادEntryPoint، تست های جاوا اسکریپت اطمینان حاصل می‌کند که می‌توان مقادیر خارج از بلاک چین را با ایجاد تراکنش‌هایی که تابعstoreTwoValues(uint, uint)را هدف قرار می‌دهند به قرارداد هوشمند ارسال کرد (خط 15). بازیابی تعداد مقادیر ذخیره شده در بلاک چین با فراخوانی()getNumberOfValuesدر خطوط 12 و 16 تست ها، ذخیره شدن آنها را تضمین می کند.

جمع بندی

وقتی نوبت به تست قراردادهای هوشمند می‌رسد، هرچه بیشتر بهتر. هنگام حصول اطمینان از اینکه همه مسیرهای ممکن اجرا نتایج مورد انتظار را برمی‌گردانند. از تست های Solidity سطح بلاک چین برای unit tests و integration tests استفاده کنید و از تست های جاوا اسکریپت برای integration tests در سطح DApp استفاده کنید.

نکاتی در این پروژه وجود دارد که می‌توانست تست‌های unit یا integration بیشتری نوشته شود، بنابراین اگر فکر می‌کنید می‌توانید به این پروژه اضافه کنید، حتماً یک درخواست کشش به repo on Github ارسال کنید!

منبع : medium