区块链全栈以太坊(八)单元测试&gas优化

官方语法文档

https://docs.soliditylang.org/en/v0.8.13/style-guide.html#introduction

一、solidity代码风格

自定义错误

前缀为合约名,然后接着错误类型的名称。

好处:报错时。清除地看到是哪个合约出错。

error  ContractName_Unauthorized();
contract ContractName{

}

NatSpec

一种代码注释的风格。

使用 Doxygen 风格的注释和标签来帮助文档化我们的代码。

用工具生成文档:

solc --userdoc --devdoc ex1.sol

官方文档: https://docs.soliditylang.org/en/v0.8.13/natspec-format.html

可见性

internal、private、external、public区别

https://blog.csdn.net/weixin_42820026/article/details/131477613

二、测试Fundme合约

简述

demo源码hardhat-fund-me-fcc

  • 使用 gas estimator 估算gas,然后调优合约、减少gas。
  • 用hardhat 来自动设置我们的测试。(类似自动执行deploy\下的脚本)。
yarn hardhat test

(一)调试

断点debug(js)

1)左侧 "运行和调试" --》"javascript 调试终端"

2)运行 yarn hardhat test。
会显示 "Debugger attached",停止在断点处的脚本。

console.log

在solidity中使用
类似 js中使用

import "hardhat/console.sol";

console.log("xxxxxx  %s", "error msg");

(二)、单元测试

参考源码hardhat-fund-me-fcc

test\unit
FundMe.test.js

一般在本地,local hoardhat 或者 forked hardhat

1.部署,合约对象获取


beforeEach(async () => {
    //这种方法直接获取  hardhat.config.js 配置的accounts列表
    // const accounts = await ethers.getSigners()
    // deployer = accounts[0]
    //这种方法获取 namedAccounts配置的deployer索引。
    deployer = (await getNamedAccounts()).deployer
    //部署:运行整个 deploy目录下的脚本,
    //并且可以使用任意数量的tag
    await deployments.fixture(["all"])
    //Hardhat-deploy为 ethers 包装了一个名为getContract的函数
    //该函数将获取我们告诉它的任意合约的最新部署。
    fundMe = await ethers.getContract("FundMe", deployer)
    mockV3Aggregator = await ethers.getContract(
        "MockV3Aggregator",
        deployer,
    )
})

2.测试Chainlink注入合约

 describe("constructor", function () {
     it("sets the aggregator addresses correctly", async ()         => {
         const response = await fundMe.getPriceFeed()
         assert.equal(response, mockV3Aggregator.address)
     })
})

3.测试fund 金额不足报错

用来断言 期望错误结果,但是期望单侧 是成功的。
expect( ).to.be.revertedWith(
"You need to spend more ETH!",
)

 it("Fails if you don't send enough ETH", async () => {
     await expect(fundMe.fund()).to.be.revertedWith(
         "You need to spend more ETH!",
     )
})

4.测试fund转入金额正常

it("Updates the amount funded data structure", async () => {
    await fundMe.fund({ value: sendValue })
    const response =
          await fundMe.getAddressToAmountFunded(deployer)
    assert.equal(response.toString(), sendValue.toString())
})

5.测试fund用户记录正常

it("Adds funder to array of funders", async () => {
   await fundMe.fund({ value: sendValue })
   const response = await fundMe.getFunder(0)
   assert.equal(response, deployer)
})

6.测试转账正常

beforeEach(async () => {
                  await fundMe.fund({ value: sendValue })
})
it("withdraws ETH from a single funder", async () => {
    // Arrange
    //获取合约初始余额
    const startingFundMeBalance =
          await fundMe.provider.getBalance(fundMe.address)
    //获取deployer 的初始余额
    const startingDeployerBalance =
          await fundMe.provider.getBalance(deployer)

    // Act
    const transactionResponse = await fundMe.withdraw()
    const transactionReceipt = await transactionResponse.wait()
    const { gasUsed, effectiveGasPrice } = transactionReceipt
    const gasCost = gasUsed.mul(effectiveGasPrice)
    //获取合约当前余额
    const endingFundMeBalance = await fundMe.provider.getBalance(
        fundMe.address,
    )
    //获取deployer 的当前余额
    const endingDeployerBalance =
          await fundMe.provider.getBalance(deployer)

    // Assert
    // Maybe clean up to understand the testing
    assert.equal(endingFundMeBalance, 0)
    //bigNumber 不能用+号,要用 add()
    assert.equal(
        startingFundMeBalance
        .add(startingDeployerBalance)
        .toString(),
        endingDeployerBalance.add(gasCost).toString(),
    )
})

(三)集成测试(staging)

新建test\staging。
FundMe.staging.test.js

一般在测试网络或者真实网络。
开发过程的最后一步。

注意:

1.不会像单元测试中去部署。

而是假设它已经被部署在了测试网络上。

// 单测中部署
await deployments.fixture(["all"]) 

2.编写验证脚本 :捐款,提现,断言。

3.部署

注意配置好 chainlink的pricefeed地址
在这里helper-hardhat-config.js

yarn hardhat deploy --network sepolia

4.执行测试

yarn hardhat test --network sepolia

(四)编写脚本与代码交互

前提:启动本地节点网络 localhost

1) scripts\fund.js

这个脚本将与我们的测试非常相似。|

这样,将来如果我们想快速为其中一个合约提供资金,
我们只需运行这个脚本即可。

# 连接本地节点localhost进行测试。
yarn hardhat run scripts/fund.is --network localhost

2) scripts\withdraw.js

类似。

3) package.json添加脚本

通过添加scripts节点,
可以把这些长测试浓缩为一个yarn script。

{
    "scripts": {
        "test": "hardhat test",
        "test:staging": "hardhat test --network sepolia",
        "lint": "solhint 'contracts/**/*.sol'",
        "lint:fix": "solhint 'contracts/**/*.sol' --fix",
        "format": "prettier --write .",
        "coverage": "hardhat coverage"
    }
}
#执行命令触发脚本:
# 获取scripts>test的脚本"hardhat test"并执行
yarn test
# 集成测试
yarn test:staging
#代码扫描
yarn lint
#格式化
yarn format

三、storage原理

(一)全局变量

当我们保存或存储这些全局变量或者说"storage变量时,到底发生了些什么?

你可以将“storage" 想象为…个包含我们所创建的所有变量的一个巨大的数组或列。
其中的每个变量,每个值。
都被放置在了"Storage"数组中的某个 32 字节长的槽位中。

gas_1.png

(二)动态的变量

对于动态的变量如array,map怎么办?

它们内部的元素实际上是以一种名为“哈希函数“哈希函数“)的形式存储。

gas_2.png

(三)constant`变量和jimmutable

constant`变量和jimmutable。变量并不占用"Storage" 的空间,
这是因为。constant~变量实际上已经成为了合约字节码其本身的一部分了

(四)数组和mapping

数组和.mapping 会点用更多的空间.
所以 Solidity 会想要确认…我们到底在哪里处理它们.是"Storage" 还是"memory" 你必须要告诉我.
我需要知道我是否需要为它们在"Storage"数据结构中分配空间.

(五)private/internal 真的有用?

ethers.provider.getStorageAt(CONTRACT_ADDRESS, index)它能让我们获取任意一个槽位内的数据.
所以:即债你将-个函数设置为"private"或者"internal',其他人也仍然可以读取它。

log("Logging storage...")
    for (let i = 0; i < 10; i++) {
        log(
            `Location ${i}: ${await ethers.provider.getStorageAt(
                funWithStorage.address,
                i
            )}`
        )
    }

四,gas优化

(一)storage

读取或者写入storage存储都会花费大量的gas费用。

gas·费用是通过操作码的 gas 成本来计算的,每个操作码都有一个在以太坊网络中预定义的固定的gas成本。
如下表格:evm-opcodes 可以看到每个操作码的费用。

最常用的操作:
sstore操作码: 在storage中存储,需要支付高达20000gas。
sload操作码: 在storeage中读取数据 成本高达800gas。

所以,全局变量(存储变量)通常用s_前缀来醒目地标注出来。

(二)优化withDraw函数

一次性读入内存,注意mapping类型没法读入内存。

function cheaperWithdraw() public onlyOwner {
    //一次性读入内存,注意mapping类型没法读入内存。
        address[] memory funders = s_funders;
        // mappings can't be in memory, sorry!
        for (
            uint256 funderIndex = 0;
            funderIndex < funders.length;
            funderIndex++
        ) {
            address funder = funders[funderIndex];
            s_addressToAmountFunded[funder] = 0;
        }
        s_funders = new address[](0);
        // payable(msg.sender).transfer(address(this).balance);
        (bool success, ) = i_owner.call{value: address(this).balance}("");
        require(success);
    }

(三)合理设置可见性

设置成 internal 和 private 变量会更省 gas 费。

合约所有者地址不需要对其它人或者其它合约公开,其它信息也是
通过getVarName() 函数公开。

(四)revert代替require()

因为require() ,实际上是把这么多字符串,数组存储在链上。

// 例如,缺点是,没法传递错误信息
revert FundMe__NotOwner();
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容