• 基于Hardhat编写合约测试用例


    基于Hardhat编写合约测试用例

    为智能合约编写自动化测试至关重要,毕竟写智能合约多多少少都会跟用户资金挂钩。

    场景

    这里假设自己正在开发一个NFT交易平台,这个平台可以让用户售卖自己的NFT,包括ERC721和ERC1155,并且用户可以指定购买者需要支付指定的ERC20 Token购买。
    我们先确定自己的测试功能和目标,为了文章篇幅不要太长,我们就以卖家用户调用sell,创建售卖订单功能为目标做测试。

    合约代码

    我们需要4个合约文件:

    1. ERC20
    2. ERC721
    3. ERC1155
    4. NFTSwap(交易平台)

    前三种合约最简单的,我们不需要自己再去实现,直接引用Openzeppelin的合约代码即可。
    contracts目录下创建一个新的文件TestDependency.sol,并且把下面的代码粘贴进去

    // SPDX-License-Identifier: MIT
    pragma solidity >=0.8.0;
    
    import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";
    import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol";
    import "@openzeppelin/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser.sol";
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样需要用到的ERC20,ERC721,ERC1155合约就会被编译到项目中

    NFTSwap合约代码我只展示sell相关部分,足够测试即可
    contracts目录下新建一个NFTSwap.sol合约,并粘贴下面的代码

    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.0;
    
    import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
    
    contract NFTSwap is Initializable {
        enum AssetType {
            ERC721,
            ERC1155
        }
    
        struct Asset {
            address Contract; // NFT Token地址
            uint256 TokenId; // Token id
            uint256 TokenValue; // Token Value, ERC721 为1
            AssetType Type; // NFT 类型
        }
    
        function __NFTSwap_init() public initializer {}
    
        function sell(
            Asset[] calldata assets, // 要售卖的NFT,可以同时售卖多个
            address paymentToken, // 指定接受购买支付的 ERC20 代币
            uint256 price // 售卖价格
        ) public virtual returns (uint256 goodsId) {
            // 创建售卖订单逻辑
            //.......
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    编译合约

    ➜ npx hardhat compile
    Compiled 36 Solidity files successfully
    
    • 1
    • 2

    合约编译通过,下一步

    引用测试工具包

    修改项目根目录下的hardhat.config.js,添加对工具包的引用

    require("@nomiclabs/hardhat-waffle");
    require('@openzeppelin/hardhat-upgrades');
    
    task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
      const accounts = await hre.ethers.getSigners();
    
      for (const account of accounts) {
        console.log(account.address);
      }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    编写测试代码

    这一部分是重点,我会把整个测试脚本文件先拆分讲解,并在文章最后附上完成的代码

    引用

    test目录下新建sell-test.js文件,我们将在这里编辑测试用例代码
    先添加引用

    const { expect, use } = require('chai'); //引入断言库
    const { BigNumber } = require('ethers'); // bignumber一会儿要用到
    const { deployContract, MockProvider, solidity } = require('ethereum-waffle'); 
    const { ethers, upgrades } = require("hardhat");
    
    use(solidity); // 这里是跟 chai 声明使用在solidity合约测试
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    定义测试套件和全局变量

    因为我会在这个套件内定义多个测试用例,模拟多种场景,所以可以定义全局变量,减少代码重复

    describe("Test NFTSwap.sell Interface", function () {
        var ERC20; 	// 存放要用到的ERC20
        var ERC721;	// 同上
        var ERC1155; // 同上
        var OWNER; // 这里是为了演示模拟多用户操作 
        var ADDR1; // 同上
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    定义beforeEach

    beforeEach会在每个测试用例运行前先运行。可以通过定义beforeEach在每次测试前初始化环境,这样可以做到多个测试用例的数据不会相互影响,因为每次运行用例前,beforeEach都会重置环境

    beforeEach(async () => {
        // 模拟不同的两个用户,比如测试完成的买卖流程就应该用 两个用户地址
        [OWNER, ADDR1] = await ethers.getSigners();
    
        // Owner 用户创建多个合约
        const ERC20PresetMinterPauser = await ethers.getContractFactory("ERC20PresetMinterPauser", OWNER);
        ERC20 = await ERC20PresetMinterPauser.deploy("TestERC20", "T20");
    
        const ERC721PresetMinterPauserAutoId = await ethers.getContractFactory("ERC721PresetMinterPauserAutoId", OWNER);
        ERC721 = await ERC721PresetMinterPauserAutoId.deploy("TestERC721", "T721", "https://t721.com");
    
        const ERC1155PresetMinterPauser = await ethers.getContractFactory("ERC1155PresetMinterPauser", OWNER);
        ERC1155 = await ERC1155PresetMinterPauser.deploy("https://t1155.com");
    
        const NFTSwap = await ethers.getContractFactory("NFTSwap");
        NFT_SWAP = await upgrades.deployProxy(NFTSwap, [], {
            initializer: '__NFTSwap_init'
        });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    定义测试用例

    这里我会定义三个测试用例,模拟售卖不同种类NFT,和同时售卖两种NFT的情况

    第一个测试用例

    创建售卖1个ERC721 Token订单成功

    it("Should be sale an ERC721 token successful", async function () {
        // 确定 NFTSwap合约 部署完成
        await NFT_SWAP.deployed();
    
        // 确定 ERC721合约 部署完成
        await ERC721.deployed();
    
        // 增发 id=0 的token,并approve 给 NFTSwap
        var mintERC721Tx = await ERC721.connect(OWNER).mint(OWNER.address);
        await mintERC721Tx.wait();
        var approveERC721Tx = await ERC721.connect(OWNER).setApprovalForAll(NFT_SWAP.address, true);
        await approveERC721Tx.wait();
    
        // 定义assets, assetType.ERC721 = 1
        var assets = [{ Contract: ERC721.address, TokenId: BigNumber.from(0), TokenValue: BigNumber.from(1), Type: 1 }]
    
        await ERC20.deployed();
        // 发起交易
        const sellTx = await NFT_SWAP.sell(assets, ERC20.address, BigNumber.from(1000000));
        await sellTx.wait()
    
        // 获取交易结果
        var receipt = await ethers.provider.getTransactionReceipt(sellTx.hash);
        // 判断交易最终状态,必须为1,1表示合约执行成功
        expect(receipt.status).to.equal(1);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    第二个测试用例

    创建售卖1个ERC1155T oken订单成功

    it("Should be sale an ERC1155 token successful", async function () {
        // 确定 NFTSwap合约 部署完成
        await NFT_SWAP.deployed();
    
        // 确定 ERC1155合约 部署完成
        await ERC1155.deployed();
    
        // 增发 id=0 的token,并approve 给 NFTSwap
        var mintERC1155Tx = await ERC1155.connect(OWNER).mint(OWNER.address, 1, 10, "0x");
        await mintERC1155Tx.wait();
        var approveERC1155Tx = await ERC1155.connect(OWNER).setApprovalForAll(NFT_SWAP.address, true);
        await approveERC1155Tx.wait();
    
        // 定义assets, assetType.ERC1155 = 2
        var assets = [{ Contract: ERC1155.address, TokenId: BigNumber.from(1), TokenValue: BigNumber.from(1), Type: 2 }]
    
        await ERC20.deployed();
        const sellTx = await NFT_SWAP.sell(assets, ERC20.address, BigNumber.from(1000000));
        await sellTx.wait()
    
        var receipt = await ethers.provider.getTransactionReceipt(sellTx.hash);
        expect(receipt.status).to.equal(1);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    第三个测试用例

    创建售卖 1个ERC721 Token + 1个ERC1155T oken 订单成功

    it("Should be packet sale an ERC721 token and an ERC1155 token successful", async function () {
        // 确定 NFTSwap合约 部署完成
        await NFT_SWAP.deployed();
    
        // 确定 ERC721合约 部署完成
        await ERC721.deployed();
    
        // 增发 id=0 的ERC721 token,并approve 给 NFTSwap
        var mintERC721Tx = await ERC721.connect(OWNER).mint(OWNER.address);
        await mintERC721Tx.wait();
        var approveERC721Tx = await ERC721.connect(OWNER).setApprovalForAll(NFT_SWAP.address, true);
        await approveERC721Tx.wait();
    
        // 确定 ERC1155合约 部署完成
        await ERC1155.deployed();
    
        // 增发 id=0 的ERC1155 token,并approve 给 NFTSwap
        var mintERC1155Tx = await ERC1155.connect(OWNER).mint(OWNER.address, 1, 10, "0x");
        await mintERC1155Tx.wait();
        var approveERC1155Tx = await ERC1155.connect(OWNER).setApprovalForAll(NFT_SWAP.address, true);
        await approveERC1155Tx.wait();
    
        // 定义assets,这里是用两个 NFT Token的
        var assets = [{ Contract: ERC721.address, TokenId: BigNumber.from(0), TokenValue: BigNumber.from(1), Type: 1 },
        { Contract: ERC1155.address, TokenId: BigNumber.from(1), TokenValue: BigNumber.from(10), Type: 2 }]
    
        await ERC20.deployed();
        const sellTx = await NFT_SWAP.sell(assets, ERC20.address, BigNumber.from(200000));
        await sellTx.wait()
    
        var receipt = await ethers.provider.getTransactionReceipt(sellTx.hash);
        expect(receipt.status).to.equal(1);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    到这里,我们的测试脚本文件已经完成了,接下来直接运行测试脚本,查看测试结果就可以了

    运行测试脚本

    ➜  npx hardhat test test/sell-test.js
    
    
      Test NFTSwap.sell Interface
        ✔ Should be sale an ERC721 token successful (120ms)
        ✔ Should be sale an ERC1155 token successful (99ms)
        ✔ Should be packet sale an ERC721 token and an ERC1155 token successful (177ms)
    
    
      3 passing (4s)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这里可以看到测试都通过

    完整测试脚本代码

    const { expect, use } = require('chai');
    const { BigNumber } = require('ethers');
    const { deployContract, MockProvider, solidity } = require('ethereum-waffle');
    const { ethers, upgrades } = require("hardhat");
    
    use(solidity);
    
    describe("Test NFTSwap.sell Interface", function () {
        var ERC20;
        var ERC721;
        var ERC1155;
        var OWNER;
        var ADDR1;
        var NFT_SWAP;
    
        beforeEach(async () => {
            [OWNER, ADDR1] = await ethers.getSigners();
    
            const ERC20PresetMinterPauser = await ethers.getContractFactory("ERC20PresetMinterPauser", OWNER);
            ERC20 = await ERC20PresetMinterPauser.deploy("TestERC20", "T20");
    
            const ERC721PresetMinterPauserAutoId = await ethers.getContractFactory("ERC721PresetMinterPauserAutoId", OWNER);
            ERC721 = await ERC721PresetMinterPauserAutoId.deploy("TestERC721", "T721", "https://t721.com");
    
            const ERC1155PresetMinterPauser = await ethers.getContractFactory("ERC1155PresetMinterPauser", OWNER);
            ERC1155 = await ERC1155PresetMinterPauser.deploy("https://t1155.com");
    
            const NFTSwap = await ethers.getContractFactory("NFTSwap");
            NFT_SWAP = await upgrades.deployProxy(NFTSwap, {
                initializer: '__NFTSwap_init'
            });
        });
    
    
        it("Should be sale an ERC721 token successful", async function () {
            await NFT_SWAP.deployed();
    
            await ERC721.deployed();
    
            var mintERC721Tx = await ERC721.connect(OWNER).mint(OWNER.address);
            await mintERC721Tx.wait();
            var approveERC721Tx = await ERC721.connect(OWNER).setApprovalForAll(NFT_SWAP.address, true);
            await approveERC721Tx.wait();
    
            var assets = [{ Contract: ERC721.address, TokenId: BigNumber.from(0), TokenValue: BigNumber.from(1), Type: 1 }]
    
            await ERC20.deployed();
    
            const sellTx = await NFT_SWAP.sell(assets, ERC20.address, BigNumber.from(1000000));
            await sellTx.wait()
    
            var receipt = await ethers.provider.getTransactionReceipt(sellTx.hash);
            expect(receipt.status).to.equal(1);
        });
    
        it("Should be sale an ERC1155 token successful", async function () {
            await NFT_SWAP.deployed();
    
            await ERC1155.deployed();
    
            var mintERC1155Tx = await ERC1155.connect(OWNER).mint(OWNER.address, 1, 10, "0x");
            await mintERC1155Tx.wait();
            var approveERC1155Tx = await ERC1155.connect(OWNER).setApprovalForAll(NFT_SWAP.address, true);
            await approveERC1155Tx.wait();
    
            var assets = [{ Contract: ERC1155.address, TokenId: BigNumber.from(1), TokenValue: BigNumber.from(1), Type: 2 }]
    
            await ERC20.deployed();
            const sellTx = await NFT_SWAP.sell(assets, ERC20.address, BigNumber.from(1000000));
            await sellTx.wait()
    
            var receipt = await ethers.provider.getTransactionReceipt(sellTx.hash);
            expect(receipt.status).to.equal(1);
        });
    
        it("Should be packet sale an ERC721 token and an ERC1155 token successful", async function () {
            await NFT_SWAP.deployed();
    
            await ERC721.deployed();
    
            var mintERC721Tx = await ERC721.connect(OWNER).mint(OWNER.address);
            await mintERC721Tx.wait();
            var approveERC721Tx = await ERC721.connect(OWNER).setApprovalForAll(NFT_SWAP.address, true);
            await approveERC721Tx.wait();
    
            await ERC1155.deployed();
    
            var mintERC1155Tx = await ERC1155.connect(OWNER).mint(OWNER.address, 1, 10, "0x");
            await mintERC1155Tx.wait();
            var approveERC1155Tx = await ERC1155.connect(OWNER).setApprovalForAll(NFT_SWAP.address, true);
            await approveERC1155Tx.wait();
    
            var assets = [{ Contract: ERC721.address, TokenId: BigNumber.from(0), TokenValue: BigNumber.from(1), Type: 1 },
            { Contract: ERC1155.address, TokenId: BigNumber.from(1), TokenValue: BigNumber.from(10), Type: 2 }]
    
            await ERC20.deployed();
            const sellTx = await NFT_SWAP.sell(assets, ERC20.address, BigNumber.from(200000));
            await sellTx.wait()
    
            var receipt = await ethers.provider.getTransactionReceipt(sellTx.hash);
            expect(receipt.status).to.equal(1);
        });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103

    有问题,或者建议请留言,谢谢。

  • 相关阅读:
    IDEA配置Maven
    MySQL数据库触发器
    C语言求矩阵的逆(初等行变换法)
    JVM基础:初识JVM
    vue3.0父子组件方法调用(setup语法糖)
    系统架构设计师-第11章-未来信息综合技术-软考学习笔记
    智能井盖传感器:提升城市安全与便利的利器
    Java 面试题:如何保证集合是线程安全的? ConcurrentHashMap 如何实现高效地线程安全?
    vim以16进制打开和编辑文件
    Linux程序设计shell程序学习
  • 原文地址:https://blog.csdn.net/Lyon_Nee/article/details/125559468