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:
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.
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: