區(qū)塊鏈全棧以太坊(八)單元測試&gas優(yōu)化

官方語法文檔

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

一、solidity代碼風(fēng)格

自定義錯誤

前綴為合約名,然后接著錯誤類型的名稱。

好處:報錯時巫员。清除地看到是哪個合約出錯。

error  ContractName_Unauthorized();
contract ContractName{

}

NatSpec

一種代碼注釋的風(fēng)格捎琐。

使用 Doxygen 風(fēng)格的注釋和標(biāo)簽來幫助文檔化我們的代碼虱咧。

用工具生成文檔:

solc --userdoc --devdoc ex1.sol

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

可見性

internal耕拷、private汹族、external萧求、public區(qū)別

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

二、測試Fundme合約

簡述

demo源碼hardhat-fund-me-fcc

  • 使用 gas estimator 估算gas顶瞒,然后調(diào)優(yōu)合約夸政、減少gas。
  • 用hardhat 來自動設(shè)置我們的測試榴徐。(類似自動執(zhí)行deploy\下的腳本)秒梳。
yarn hardhat test

(一)調(diào)試

斷點debug(js)

1)左側(cè) "運行和調(diào)試" --》"javascript 調(diào)試終端"

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目錄下的腳本疚顷,
    //并且可以使用任意數(shù)量的tag
    await deployments.fixture(["all"])
    //Hardhat-deploy為 ethers 包裝了一個名為getContract的函數(shù)
    //該函數(shù)將獲取我們告訴它的任意合約的最新部署虐拓。
    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 金額不足報錯

用來斷言 期望錯誤結(jié)果,但是期望單側(cè) 是成功的。
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轉(zhuǎn)入金額正常

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.測試轉(zhuǎn)賬正常

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)
    //獲取合約當(dāng)前余額
    const endingFundMeBalance = await fundMe.provider.getBalance(
        fundMe.address,
    )
    //獲取deployer 的當(dāng)前余額
    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

一般在測試網(wǎng)絡(luò)或者真實網(wǎng)絡(luò)杨耙。
開發(fā)過程的最后一步衬廷。

注意:

1.不會像單元測試中去部署。

而是假設(shè)它已經(jīng)被部署在了測試網(wǎng)絡(luò)上。

// 單測中部署
await deployments.fixture(["all"]) 

2.編寫驗證腳本 :捐款西潘,提現(xiàn)卷玉,斷言。

3.部署

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

yarn hardhat deploy --network sepolia

4.執(zhí)行測試

yarn hardhat test --network sepolia

(四)編寫腳本與代碼交互

前提:啟動本地節(jié)點網(wǎng)絡(luò) localhost

1) scripts\fund.js

這個腳本將與我們的測試非常相似肛捍。|

這樣铃肯,將來如果我們想快速為其中一個合約提供資金,
我們只需運行這個腳本即可遣妥。

# 連接本地節(jié)點localhost進(jìn)行測試。
yarn hardhat run scripts/fund.is --network localhost

2) scripts\withdraw.js

類似寝并。

3) package.json添加腳本

通過添加scripts節(jié)點,
可以把這些長測試濃縮為一個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"
    }
}
#執(zhí)行命令觸發(fā)腳本:
# 獲取scripts>test的腳本"hardhat test"并執(zhí)行
yarn test
# 集成測試
yarn test:staging
#代碼掃描
yarn lint
#格式化
yarn format

三衬潦、storage原理

(一)全局變量

當(dāng)我們保存或存儲這些全局變量或者說"storage變量時,到底發(fā)生了些什么?

你可以將“storage" 想象為…個包含我們所創(chuàng)建的所有變量的一個巨大的數(shù)組或列。
其中的每個變量馏谨,每個值别渔。
都被放置在了"Storage"數(shù)組中的某個 32 字節(jié)長的槽位中。

gas_1.png

(二)動態(tài)的變量

對于動態(tài)的變量如array惧互,map怎么辦?

它們內(nèi)部的元素實際上是以一種名為“哈希函數(shù)“哈希函數(shù)“)的形式存儲哎媚。

gas_2.png

(三)constant`變量和jimmutable

constant`變量和jimmutable。變量并不占用"Storage" 的空間,
這是因為喊儡。constant~變量實際上已經(jīng)成為了合約字節(jié)碼其本身的一部分了

(四)數(shù)組和mapping

數(shù)組和.mapping 會點用更多的空間.
所以 Solidity 會想要確認(rèn)…我們到底在哪里處理它們.是"Storage" 還是"memory" 你必須要告訴我.
我需要知道我是否需要為它們在"Storage"數(shù)據(jù)結(jié)構(gòu)中分配空間.

(五)private/internal 真的有用拨与?

ethers.provider.getStorageAt(CONTRACT_ADDRESS, index)它能讓我們獲取任意一個槽位內(nèi)的數(shù)據(jù).
所以:即債你將-個函數(shù)設(shè)置為"private"或者"internal',其他人也仍然可以讀取它艾猜。

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

四买喧,gas優(yōu)化

(一)storage

讀取或者寫入storage存儲都會花費大量的gas費用。

gas·費用是通過操作碼的 gas 成本來計算的匆赃,每個操作碼都有一個在以太坊網(wǎng)絡(luò)中預(yù)定義的固定的gas成本淤毛。
如下表格:evm-opcodes 可以看到每個操作碼的費用。

最常用的操作:
sstore操作碼: 在storage中存儲算柳,需要支付高達(dá)20000gas低淡。
sload操作碼: 在storeage中讀取數(shù)據(jù) 成本高達(dá)800gas。

所以瞬项,全局變量(存儲變量)通常用s_前綴來醒目地標(biāo)注出來蔗蹋。

(二)優(yōu)化withDraw函數(shù)

一次性讀入內(nèi)存,注意mapping類型沒法讀入內(nèi)存囱淋。

function cheaperWithdraw() public onlyOwner {
    //一次性讀入內(nèi)存猪杭,注意mapping類型沒法讀入內(nèi)存。
        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);
    }

(三)合理設(shè)置可見性

設(shè)置成 internal 和 private 變量會更省 gas 費妥衣。

合約所有者地址不需要對其它人或者其它合約公開皂吮,其它信息也是
通過getVarName() 函數(shù)公開戒傻。

(四)revert代替require()

因為require() ,實際上是把這么多字符串涮较,數(shù)組存儲在鏈上稠鼻。

// 例如,缺點是狂票,沒法傳遞錯誤信息
revert FundMe__NotOwner();
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末候齿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子闺属,更是在濱河造成了極大的恐慌慌盯,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,332評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掂器,死亡現(xiàn)場離奇詭異亚皂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)国瓮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,508評論 3 385
  • 文/潘曉璐 我一進(jìn)店門灭必,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人乃摹,你說我怎么就攤上這事禁漓。” “怎么了孵睬?”我有些...
    開封第一講書人閱讀 157,812評論 0 348
  • 文/不壞的土叔 我叫張陵播歼,是天一觀的道長。 經(jīng)常有香客問我掰读,道長秘狞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,607評論 1 284
  • 正文 為了忘掉前任蹈集,我火速辦了婚禮烁试,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拢肆。我一直安慰自己减响,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,728評論 6 386
  • 文/花漫 我一把揭開白布善榛。 她就那樣靜靜地躺著,像睡著了一般呻畸。 火紅的嫁衣襯著肌膚如雪移盆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,919評論 1 290
  • 那天伤为,我揣著相機(jī)與錄音咒循,去河邊找鬼据途。 笑死,一個胖子當(dāng)著我的面吹牛叙甸,可吹牛的內(nèi)容都是我干的颖医。 我是一名探鬼主播,決...
    沈念sama閱讀 39,071評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼裆蒸,長吁一口氣:“原來是場噩夢啊……” “哼熔萧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起僚祷,我...
    開封第一講書人閱讀 37,802評論 0 268
  • 序言:老撾萬榮一對情侶失蹤佛致,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辙谜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俺榆,經(jīng)...
    沈念sama閱讀 44,256評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,576評論 2 327
  • 正文 我和宋清朗相戀三年装哆,在試婚紗的時候發(fā)現(xiàn)自己被綠了罐脊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,712評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡蜕琴,死狀恐怖萍桌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奸绷,我是刑警寧澤梗夸,帶...
    沈念sama閱讀 34,389評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站号醉,受9級特大地震影響反症,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜畔派,卻給世界環(huán)境...
    茶點故事閱讀 40,032評論 3 316
  • 文/蒙蒙 一铅碍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧线椰,春花似錦胞谈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至配紫,卻和暖如春径密,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背躺孝。 一陣腳步聲響...
    開封第一講書人閱讀 32,026評論 1 266
  • 我被黑心中介騙來泰國打工享扔, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留底桂,地道東北人。 一個月前我還...
    沈念sama閱讀 46,473評論 2 360
  • 正文 我出身青樓惧眠,卻偏偏與公主長得像籽懦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子氛魁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,606評論 2 350

推薦閱讀更多精彩內(nèi)容