Home

Lesson One: Testing Smart Contracts with Truffle

Lesson link here.

By the end of this lesson, you will be able to:

  1. Test your smart contracts with Truffle against Ganache
  2. Use Chai to write more expressive assertions
  3. Test against Loom

Example project layout

This is for the course:

├── build ├── contracts ├── Migrations.json ├── CryptoZombies.json ├── erc721.json ├── ownable.json ├── safemath.json ├── zombieattack.json ├── zombiefactory.json ├── zombiefeeding.json ├── zombiehelper.json ├── zombieownership.json ├── contracts ├── Migrations.sol ├── CryptoZombies.sol ├── erc721.sol ├── ownable.sol ├── safemath.sol ├── zombieattack.sol ├── zombiefactory.sol ├── zombiefeeding.sol ├── zombiehelper.sol ├── zombieownership.sol ├── migrations └── test . package-lock.json . truffle-config.js . truffle.js

Build artefacts

Every time you compile a smart contract, the Solidity compiler generates a JSON file (referred to as build artifacts) which contains the binary representation of that contract and saves it in the build/contracts folder.

Next, when you run a migration, Truffle updates this file with the information related to that network.

The first thing you'll need to do every time you start writing a new test suite is to load the build artifacts of the contract you want to interact with. This way, Truffle will know how to format our function calls in a way the contract will understand.

Say there was a contract called myAwesomeContract. We could do something like the following to load the build artifacts:

const myAwesomeContract = artifacts.require("myAwesomeContract")

The function returns something called a contract abstraction. In a nutshell, a contract abstraction hides the complexity of interacting with Ethereum and provides a convenient JavaScript interface to our Solidity smart contract. We'll be using it in the next chapters.

The contract() function

Behind the scenes, Truffle adds a thin wrapper around Mocha in order to make testing simpler.

Here is a barebones example of the implementation:

contract("MyAwesomeContract", (accounts) => { it("should be able to receive Ethers", () => {}) })

An example implementation of a test without asserting anything:

const CryptoZombies = artifacts.require("CryptoZombies") contract("CryptoZombies", (accounts) => { it("should be able to create a new zombie", () => {}) })

Testing locally

Before deploying to Ethereum, it is best to test your smart contracts locally.

You can do so by using a tool called Ganache, which sets up a local Ethereum network.

Every time Ganache starts, it creates 10 test accounts and gives them 100 Ethers to make testing easier. Since Ganache and Truffle are tightly integrated we can access these accounts through the accounts array we've mentioned in the previous chapter.

We can access those accounts using let [alice, bob] = accounts;.

We can then go through the setup/act/assert approach to testing for our contract.

Setup

const contractInstance = await myAwesomeContract.new() const zombieNames = ["Zombie #1", "Zombie #2"] contractInstance.createRandomZombie(zombieNames[0])

Act

One of the features of Truffle is that it wraps the original Solidity implementation and lets us specify the address that makes the function call by passing that address as an argument.

The following calls createRandomZombie and makes sure msg.sender is set to Alice's address:

const result = await contractInstance.createRandomZombie(zombieNames[0], { from: alice, })

Logs and Events

Once we specified the contract we wanted to test using artifacts.require, Truffle automatically provides the logs generated by our smart contract. What this means is that we can now retrieve the name of Alice's newly created zombie using something like this: result.logs[0].args.name. In a similar fashion, we can get the id and the _dna.

Taking all this knowledge together, we can then write out the test (and assert against the result):

const CryptoZombies = artifacts.require("CryptoZombies") const zombieNames = ["Zombie 1", "Zombie 2"] contract("CryptoZombies", (accounts) => { let [alice, bob] = accounts it("should be able to create a new zombie", async () => { const contractInstance = await CryptoZombies.new() const result = await contractInstance.createRandomZombie(zombieNames[0], { from: alice, }) assert.equal(result.receipt.status, true) assert.equal(result.logs[0].args.name, zombieNames[0]) }) })

Hooks

These are our test hooks like beforeEach, afterEach, etc.

An example of updating our code to use the hook for the contract instance:

const CryptoZombies = artifacts.require("CryptoZombies") const zombieNames = ["Zombie 1", "Zombie 2"] contract("CryptoZombies", (accounts) => { let [alice, bob] = accounts let contractInstance beforeEach(async () => { contractInstance = await CryptoZombies.new() }) it("should be able to create a new zombie", async () => { const result = await contractInstance.createRandomZombie(zombieNames[0], { from: alice, }) assert.equal(result.receipt.status, true) assert.equal(result.logs[0].args.name, zombieNames[0]) }) it("should not allow two zombies", async () => {}) })

Contract.new

Now, let's circle back to how contract.new works. Basically, every time we call this function, Truffle makes it so that a new contract gets deployed.

On one side, this is helpful because it lets us start each test with a clean sheet.

On the other side, if everybody would create countless contracts the blockchain will become bloated. We want you to hang around, but not your old test contracts!

We would want to prevent this from happening, right?

Happily, the solution is pretty straightforward... our contract should selfdestruct once it's no longer needed.

function kill() public onlyOwner { selfdestruct(owner()); }

Note: If you want to learn more about selfdestruct(), you can read the Solidity docs here. The most important thing to bear in mind is that selfdestruct is the only way for code at a certain address to be removed from the blockchain. This makes it a pretty important feature!

We could then update our afterEach hook to make sure that the contract is selfdestructed before each test:

afterEach(async () => { await contractInstance.kill() })

Adding a section test to ensure two zombies cannot be created

const CryptoZombies = artifacts.require("CryptoZombies") const utils = require("./helpers/utils") const zombieNames = ["Zombie 1", "Zombie 2"] contract("CryptoZombies", (accounts) => { let [alice, bob] = accounts let contractInstance beforeEach(async () => { contractInstance = await CryptoZombies.new() }) it("should be able to create a new zombie", async () => { const result = await contractInstance.createRandomZombie(zombieNames[0], { from: alice, }) assert.equal(result.receipt.status, true) assert.equal(result.logs[0].args.name, zombieNames[0]) }) it("should not allow two zombies", async () => { await contractInstance.createRandomZombie(zombieNames[0], { from: alice }) await utils.shouldThrow( contractInstance.createRandomZombie(zombieNames[1], { from: alice }) ) }) })

Repository

https://github.com/okeeffed/developer-notes-nextjs/content/ethereum/Crypto-Zombies/Advanced-Solidity-Path/Lesson-1-Testing-Smart-Contracts-With-Truffle

Sections


Related