Solidity-测试

1.概述

在构建智能合约时编写自动化测试至关重要,因为这关系到用户的资金。

为了测试我们的合约,我们将使用 Hardhat Network,这是一个专为开发而设计的本地以太坊网络。它内置于 Hardhat 中,并用作默认网络。无需设置任何内容即可使用它。

在我们的测试中我们将使用ethers.js与我们构建的以太坊合约进行交互,我们将使用mocha.js作为我们的测试运行者。使用Chai作为测试用例的断言库,进行测试用例的编写。

ethers.js 库旨在成为一个完整且紧凑的库,用于与以太坊区块链及其生态系统进行交互。

它通常用于创建去中心化应用程序(dapps)、钱包以及其他需要读取和写入区块链的工具和简单脚本。

Mocha 是一个功能丰富的 JavaScript 测试框架,运行在Node.js和浏览器中,使异步测试变得简单而有趣。Mocha 测试串行运行,允许灵活、准确的报告,同时将未捕获的异常映射到正确的测试用例。

Chai 是一个用于节点和浏览器的 BDD / TDD 断言库,可以与任何 javascript 测试框架完美搭配。 

2.合约示例

pragma solidity ^0.8.17;

contract Token{
    string public name = "My Hardhat Token";
    string public symbol = "MHT";
    uint256 public totalSupply = 1000000;

    address public owner;

    //A mapping is a key/value map. Here we store each account's balance.
    mapping(address=>uint256) balances;


    // The Transfer event helps off-chain applications understand
    // what happens within your contract.
    event Transfer(address indexed _from,address indexed _to,uint256 value);

    /*
    *Contract initialization.
     */
    constructor (){
        // The totalSupply is assigned to the transaction sender, which is the
        // account that is deploying the contract.
        balances[msg.sender] = totalSupply;
        owner = msg.sender;
    }

    /**
     * A function to transfer tokens.
     *
     * The `external` modifier makes a function *only* callable from *outside*
     * the contract.
     */
    function transfer (address to,uint256 amount) external {
        // Check if the transaction sender has enough tokens.
        // If `require`'s first argument evaluates to `false` then the
        // transaction will revert.
        require(balances[msg.sender] >= amount,"Not enough tokens") ;

        // Transfer the amount.
        balances[msg.sender]-=amount;
        balances[to]+=amount;

        // Notify off-chain applications of the transfer.
        emit Transfer(msg.sender,to,amount);
    }

    /**
     * Read only function to retrieve the token balance of a given account.
     *
     * The `view` modifier indicates that it doesn't modify the contract's
     * state, which allows us to call it without executing a transaction.
     */
    function balanceOf(address account) external view returns(uint256){
        return balances[account];
    }
}

3.使用 Mocha + Chai 为合约编写测试用例

  • 测试脚本:测试用例的脚本,通常命名为*.js、 *.test.js  *.spec.js一般情况下测试脚本是按功能模块划分,一个测试脚本里面包含一个或多个 describe 块。
  • describe 块:称为"测试套件"(test suite),表示一组相关的测试。一个 describe 块里面包含一个或多个 it 块,甚至 describe 块里面还可能包含 describe 块。
  • it 块:称为"测试用例"(test case),表示一个单独的测试,是测试的最小单位。一个 it 块里面包含一个或多个 断言。
  • 断言 :用于预判、校验用例逻辑的执行结果,我们使用了 Chai 来写断言,并使用 expect 这种 BDD 方式,原因是它更接近于自然语言,相对比较容易理解些。
  • 路径:通常会在hardhat项目中的test目录下面编写测试脚本。

Token.sol测试用例Token.js: 

const {loadFixture,} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { expect } = require("chai");

describe("Token contract", function () {
    async function deployTokenFixture() {
        const [owner,addr1,addr2] = await ethers.getSigners();
    
        const hardhatToken = await ethers.deployContract("Token");
        //console.log("contract addr:",hardhatToken);

        await hardhatToken.waitForDeployment();

        // Fixtures can return anything you consider useful for your tests
        return {owner,addr1,addr2,hardhatToken};
    }

    // You can nest describe calls to create subsections.
    describe("Deployment", function () {
        // `it` is another Mocha function. This is the one you use to define each
        // of your tests. It receives the test name, and a callback function.
        //
        // If the callback function is async, Mocha will `await` it.
        it("Should set the right owner", async function () {
        // We use loadFixture to setup our environment, and then assert that
        // things went well
        const { hardhatToken, owner } = await loadFixture(deployTokenFixture);

        // `expect` receives a value and wraps it in an assertion object. These
        // objects have a lot of utility methods to assert values.

        // This test expects the owner variable stored in the contract to be
        // equal to our Signer's owner.
        expect(await hardhatToken.owner()).to.equal(owner.address);
        });

        it("Should assign the total supply of tokens to the owner", async function () {
        const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
        const ownerBalance = await hardhatToken.balanceOf(owner.address);
        expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
        });
    });

    describe("Transactions", function () {
        it("Should transfer tokens between accounts", async function () {
          const { hardhatToken, owner, addr1, addr2 } = await loadFixture(
            deployTokenFixture
          );
          // Transfer 50 tokens from owner to addr1
          await expect(
            hardhatToken.transfer(addr1.address, 50)
          ).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);
    
          // Transfer 50 tokens from addr1 to addr2
          // We use .connect(signer) to send a transaction from another account
          await expect(
            hardhatToken.connect(addr1).transfer(addr2.address, 50)
          ).to.changeTokenBalances(hardhatToken, [addr1, addr2], [-50, 50]);
        });
    
        it("Should emit Transfer events", async function () {
          const { hardhatToken, owner, addr1, addr2 } = await loadFixture(
            deployTokenFixture
          );
    
          // Transfer 50 tokens from owner to addr1
          await expect(hardhatToken.transfer(addr1.address, 50))
            .to.emit(hardhatToken, "Transfer")
            .withArgs(owner.address, addr1.address, 50);
    
          // Transfer 50 tokens from addr1 to addr2
          // We use .connect(signer) to send a transaction from another account
          await expect(hardhatToken.connect(addr1).transfer(addr2.address, 50))
            .to.emit(hardhatToken, "Transfer")
            .withArgs(addr1.address, addr2.address, 50);
        });
    
        it("Should fail if sender doesn't have enough tokens", async function () {
          const { hardhatToken, owner, addr1 } = await loadFixture(
            deployTokenFixture
          );
          const initialOwnerBalance = await hardhatToken.balanceOf(owner.address);
    
          // Try to send 1 token from addr1 (0 tokens) to owner.
          // `require` will evaluate false and revert the transaction.
          await expect(
            hardhatToken.connect(addr1).transfer(owner.address, 1)
          ).to.be.revertedWith("Not enough tokens");
    
          // Owner balance shouldn't have changed.
          expect(await hardhatToken.balanceOf(owner.address)).to.equal(
            initialOwnerBalance
          );
        });
      });
});

我们对关键代码进一步解释说明:

3.1 getSigners() 

const [owner,addr1,addr2] = await ethers.getSigners();

ethers.js 中的Signer是代表以太坊账户的对象。它用于将交易发送到合约和其他帐户。getSigners() 获取了我们连接到的节点中的帐户列表,其中owner账户是部署合约的账户。默认情况下,合约实例连接到第一个签名者owner。

3.2 deployContract()

const hardhatToken = await ethers.deployContract("Token");

调用ethers.deployContract("Token")将开始部署我们的代币合约,并返回该合约的对象实例,通过实例我们能够调用合约内部的函数方法。

3.3 balanceOf()

const ownerBalance = await hardhatToken.balanceOf(owner.address);

部署完成后,我们通过hardhatToken来调用合约中的balanceOf(),获取owner账户的余额。

3.4 equal()

expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);

chai.js的断言函数,它在测试中被经常使用,用于验证前后值是否相等,在相等的情况测试才会通过。而在hardhat中,断言函数被集成在@nomicfoundation/hardhat-chai-matchers 插件中, 封装成matchers(匹配器),更方便合约的测试。

3.5 loadFixture()

// We use loadFixture to setup our environment, and then assert that
// things went well
const { hardhatToken, owner } = await loadFixture(deployTokenFixture);

加载固定配置。在测试用例中,会出现一些重复性的代码和事务,在每个测试开始时执行许多事务可能会使测试套件变慢,因此我们引入loadFixture()来优雅的避免代码重复并提高测试套件的性能,再后续的调用中,不会重复执行其内的操作,而是直接获取其快照的状态和返回值。

3.6 changeTokenBalances()

// Transfer 50 tokens from owner to addr1
 await expect(
    hardhatToken.transfer(addr1.address, 50)
 ).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);

验证账户余额变化情况的匹配器。交易函数会对账户余额进行操作,我们通过changeTokenBalances()来验证账户余额的变化情况是否与预期一致。

3.7 emit()

// Transfer 50 tokens from owner to addr1
await expect(hardhatToken.transfer(addr1.address, 50))
    .to.emit(hardhatToken, "Transfer")
    .withArgs(owner.address, addr1.address, 50);

验证合约是否触发了某个事件的匹配器,句式一般是这样:await expect(...).to.emit(...).withArgs(),在expect(...)中执行事件,且必须加上await同步块保证事件在区块链上执行完毕,通过.withArgs() 中的参数来验证事件中的参数列表是否一致。

 3.8 revertedWith()

// Try to send 1 token from addr1 (0 tokens) to owner.
// `require` will evaluate false and revert the transaction.
await expect(
    hardhatToken.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("Not enough tokens");

验证合约事务回退的匹配器,revertedWith()中的参数为回退时发生的错误,案例中,验证了回退和原因,并验证通过。原因是addr1账户余额小于转账数量,因此无法进行转账操作,并提示"Not enough tokens"的错误信息。

4.运行测试用例

npx hardhat test test/Token.js

运行结果:

Token contract
    Deployment
      ✔ Should set the right owner (2424ms)
      ✔ Should assign the total supply of tokens to the owner
    Transactions
      ✔ Should transfer tokens between accounts (106ms)
      ✔ Should emit Transfer events
      ✔ Should fail if sender doesn't have enough tokens (71ms)


  5 passing (3s)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
要搭建Solidity开发环境,你可以按照以下步骤进行操作: 1. 安装Node.js:首先,你需要安装Node.js,这是一个基于JavaScript的运行环境。你可以从Node.js的官方网站(https://nodejs.org/)下载适用于你的操作系统的安装包,并按照指示进行安装。 2. 安装Solidity编译器:Solidity语言需要使用Solidity编译器来将代码编译为EVM(以太坊虚拟机)可以执行的字节码。你可以使用Solidity编译器的npm包来进行安装。在命令行中运行以下命令: ``` npm install -g solc ``` 3. 选择集成开发环境(IDE):为了方便开发Solidity智能合约,你可以选择使用Remix IDE。Remix是一个基于浏览器的IDE,它提供了一个虚拟区块链环境,可以用来部署和测试合约。你可以通过访问Remix的官方网站(https://remix.ethereum.org/)来开始使用。 4. 部署和运行合约:在Remix IDE中,你可以使用"Deploy and run(部署并运行)"选项卡来部署和测试合约。确保选择Javascript VM作为环境,这样你就可以在浏览器中运行虚拟区块链环境。然后,你可以编写Solidity合约并进行部署和交互。 通过以上步骤,你就可以搭建Solidity开发环境并开始开发智能合约了。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Solidity 开发环境搭建](https://blog.csdn.net/m0_61243965/article/details/125580416)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Solidity - 环境搭建](https://blog.csdn.net/weixin_43031412/article/details/103157712)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tomggo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值