# Test-first approach to writing solidity contracts workshop <!-- Put the link to this slide here so people can follow --> slide: https://hackmd.io/ts57kdjGRXSG383GIV9Gag --- ## What is TDD - everyone knows, but - most people defect to code-first approach :raising_hand: - because: --- <style> blockquote.small * { font-size: 0.8em; } </style> <blockquote class="small"> - write a “single” unit test describing an aspect of the program - run the test, which should fail because the program lacks that feature - write “just enough” code, the simplest possible, to make the test pass - “refactor” the code until it conforms to the simplicity criteria - repeat, “accumulating” unit tests over time </blockquote> sounds tedious, doesn't it. Let's have a go: --- ## Greeter user stories (semi-BDD) 1. as contract OWNER I want to deploy a Greeter contract so that I can provide greeting service 2. as contract OWNER I want to be able to change the greeting message alone so that it changes acording to my wishes 3. as contract USER I want to call the contract so that I can see the greeting --- ### :a: ```shell $ mkdir tdd $ npm init $ npm i hardhat --save-dev $ npx hardhat # create sample project with TS tests $ # add 'test": "hardhat test" to scripts in package.json $ npm run test $ rm -rf contracts/* test/* typechain/* scripts/* artifacts/* $ code . # or whatever editor you use ``` --- ### Create contract test-driven #### create test <style> code.typescript * { font-size: 0.8em; line-height: initial; } </style> ```typescript= // test/Greeter.spec.ts import chai, { expect } from "chai"; import chaiAsPromised from "chai-as-promised"; import { Contract, ContractFactory } from "ethers/lib/ethers"; import { ethers } from "hardhat"; chai.use(chaiAsPromised); describe("Greeter", () => { it("should deploy contract", async () => { const greeterFactory: ContractFactory = await ethers.getContractFactory( "Greeter" ); const promiseOfContract: Promise<Contract> = greeterFactory.deploy(); await expect(promiseOfContract).to.be.eventually.fulfilled; }); }); ``` --- #### run test ```shell $ npm run test $ # should fail with "HardhatError: HH700: Artifact for contract "Greeter" not found." ``` --- #### make test pass ```solidity= // contracts/Greeter.ts // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.4; contract Greeter { } ``` ```shell $ npm run test # should pass now ``` # :sports_medal: --- #### Let's make life easier ```shell= $ npm i hardhat-watcher --save-dev ``` ```typescript= // hardhat.config.ts ... import "hardhat-watcher"; ... const config: HardhatUserConfig = { ... watcher: { compilation: { tasks: ["compile"], files: ["./contracts"], verbose: true, }, ci: { tasks: [ "clean", { command: "compile", params: { quiet: true } }, { command: "test", params: { noCompile: true }, }, ], }, }, ... } ... ``` ```shell= $ npx hardhat watch ci ``` --- #### Move deployment into the beforeEach block ```typescript= // test/Greeter.spec.ts import chai, { expect } from "chai"; import chaiAsPromised from "chai-as-promised"; import { Contract, ContractFactory } from "ethers/lib/ethers"; import { ethers } from "hardhat"; chai.use(chaiAsPromised); describe("Greeter", () => { let greeter: Contract; beforeEach(async () => { const contractFactory = await ethers.getContractFactory("Greeter"); greeter = await contractFactory.deploy(); }); }); ``` --- #### Add test for US2 (greeting message) ```typescript= // test/Greeter.spec.ts import chai, { expect } from "chai"; import chaiAsPromised from "chai-as-promised"; import { Contract, ContractFactory } from "ethers/lib/ethers"; import { ethers } from "hardhat"; chai.use(chaiAsPromised); describe("Greeter", () => { ... it("should contain greeting message", async () => { const prommiseOfMessage = await greeter.message(); expect(message).to.equal(""); }); }); ``` go to `Greeter.sol` and save to rerun the test --- #### make test pass ```solidity= // contracts/Greeter.ts // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.4; contract Greeter { string public message; } ``` :::warning But let's say we're told the US implies the `message` has to be private ::: --- #### Make message private ```typescript= // test/Greeter.spec.ts import chai, { expect } from "chai"; import chaiAsPromised from "chai-as-promised"; import { Contract, ContractFactory } from "ethers/lib/ethers"; import { ethers } from "hardhat"; chai.use(chaiAsPromised); describe("Greeter", () => { ... describe("getMessage", () => { it("should return the greeting message", async () => { const prommiseOfMessage = await greeter.getMessage(); expect(message).to.equal(""); }); }); }); ``` --- #### Make test pass ```solidity= // contracts/Greeter.ts // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.4; contract Greeter { string private _message; function getMessage() public view returns (string memory) { return _message; } } ``` :::warning But here we want this to be only callable by the owner as this does not return personalised greeting. ::: --- #### Make getMessage callable by owner only ```typescript= // test/Greeter.spec.ts (...imports) describe("Greeter", () => { let greeter: Contract; let signer1: SignerWithAddress; beforeEach(async () => { const contractFactory = await ethers.getContractFactory("Greeter"); greeter = await contractFactory.deploy(); [, signer1] = await ethers.getSigners(); }); describe("getMessage", () => { it("should return the greeting message", async () => {...}); it("should be callable only by owner", async () => { const prommiseOfMessage = greeter.getMessage(); await expect(prommiseOfMessage).to.be.eventually.fulfilled; const greeterAsSig1 = greeter.connect(signer1.address); await expect(greeterAsSig1.getMessage()).to.be.eventually.rejected; }); }); }); ``` --- #### make test pass ```shell $ npm i @openzeppelin/contracts ``` ```solidity= // contracts/Greeter.ts // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.4; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract Greeter { string public message; } ``` > re-run the watcher :::danger We should really mock the Ownable. :unamused: ::: --- # OK, let's make the user happy (US2) --- # :hand: :computer: :hand: --- # :100: :muscle: :tada: --- ### Wrap up - TDD is not too bad, just need to get use to it AND - bosses (and you) understand it makes the development slower but more reliable - For the sake of the community! ;) :hammer_and_wrench: --- ### Thank you! :sheep:
{"metaMigratedAt":"2023-06-16T12:19:41.092Z","metaMigratedFrom":"YAML","title":"TDD in solidity","breaks":true,"description":".","contributors":"[{\"id\":\"82f16c31-17c2-4338-96ad-3b5b7461eab6\",\"add\":12066,\"del\":4981}]"}
    248 views
   Owned this note