# 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}]"}