Previously …
- Introduction to Solidity
- Introduction to Truffle
- Introduction to Mocha
- Truffle Testing Setup
- Testing Solidity Data Storage
- Testing Solidity State Machines
Recap
Previously, we have set up a truffle project,
which contains a Cars
smart contract (the implementation).
Next we create a specification file that references it,
and wrote some tests which perform assertions on its initial state,
as well as its state after some state transitions have occurred.
Events
In Solidity, we define events
, and then emit
them within a function.
In our Cars
contract the event that we have defined is:
event CarHonk (uint256 indexed fromCar, uint256 indexed atCar);
… and within our honkCar()
function, we emit it:
emit CarHonk(carId, otherCarId);
Solidity events get turned into logs in Ethereum. More details in Mastering Ethereum. You may think of events as structured logs.
Create an event
Let's start by creating a new specification file,
name test/Cars-events.spec.js
.
const Cars = artifacts.require('Cars');
const BN = web3.utils.BN;
contract('Cars - events', (accounts) => {
before(async () => { // set up contract with relevant initial state
});
it('Honks a car at another car', async () => {
const instance = await Cars.deployed();
// perform the state transition
// inspect the transaction & perform assertions on the logs
});
it('Honking a car that you do not own is not allowed', async () => { const instance = await Cars.deployed();
// perform the state transition
// assert that we get an error with the expected reason
});
});
This specification is slightly more complex than the previous two, in two ways:
- We are using a
before
block within thecontract
block to set up the state of the contract instance to be ready for testing - We are going to not only write a "happy path" test, but also an additional test that checks for a failure scenario.
Setting up initial contract state
We set up initial contract state in a before
block.
We need to do this because we wish to test the honkCar
function on the smart
contract, but in order for that to happen, the contract needs to be initialised,
and then have at least two cars added to it,
before we can make a successful call -
there needs to be a valid fromCar
and a valid atCar
to pass in!
So let's fill in the before
block with code that we have previously written
in specification for state machines:
// set up contract with relevant initial state
const instance = await Cars.deployed();
await instance.addCar( '0xff00ff', // colour: purple
new BN(4), // doors: 4
new BN(0), // distance: 0
new BN(0), // lat: 0
new BN(0), // lon: 0
{
from: accounts[1],
value: web3.utils.toWei('0.11', 'ether'),
},
);
await instance.addCar( '0xffff00', // colour: yellow
new BN(2), // doors: 2
new BN(0), // distance: 0
new BN(0), // lat: 0
new BN(0), // lon: 0
{
from: accounts[2],
value: web3.utils.toWei('0.11', 'ether'),
},
);
// just a sanity check, we do not really need to do assertions
// within the set up, as this should be for "known working state"
// only
const numCars =
await instance.numCars.call();
assert.equal(numCars.toString(), '2');
This is pretty much copy-and-paste from the tests we have written earlier, except that we get rid of most of the assertions.
So now, we know that when each test runs, we have an initial state of:
- car #1 owned by account #1 (a purple sedan)
- car #2 owned by account #2 (a yellow coupé)
Add a "happy path" test
In our first test case, enter the following:
const instance = await Cars.deployed();
// perform the state transition
const tx =
await instance.honkCar( 2,
1,
{
// account #2 owns car #2
from: accounts[2],
},
);
This is where account #2, tells the smart contract that it wants to honk from car #2 at car #1. Since account #2 owns car #1, and car #1 exists, this should work - but let's not assume that, and instead verify it using assertions:
// inspect the transaction & perform assertions on the logs
const { logs } = tx;
assert.ok(Array.isArray(logs));
assert.equal(logs.length, 1);
const log = logs[0];
assert.equal(log.event, 'CarHonk'); assert.equal(log.args.fromCar.toString(), '2'); assert.equal(log.args.atCar.toString(), '1');
This part is something we have not done before - we have invoked a function that emits and event, so instead of inspecting the new state of the smart contract, we are instead looking at the transaction object for logs (which are events).
We perform assertion to check that the name, and the arguments of the event matches what we expect.
Tip:
If you're curious, you can always use console
within your test to see what
something contains.
For example console.log(x);
would print something that looks like this:
{ logIndex: 0,
transactionIndex: 0,
transactionHash: '0x9e636fdb1d1193d9d365517682be277a4a67dc2585f7153b6b64b73de3e5aec6',
blockHash: '0xaf66661a274f4c3e846473ea40ef2d76bcf48c7943683583f6744203981f3dab',
blockNumber: 15,
address: '0xefb375423829c10F58aaC5c1f3076DBc161a6FaE',
type: 'mined',
id: 'log_212889c5', event: 'CarHonk',
args:
Result {
'0': <BN: 2>,
'1': <BN: 1>,
__length__: 2,
fromCar: <BN: 2>, atCar: <BN: 1> } }
Add a failure scenario test
In this test, let's try to get an account which does not own any cars to
honk someone else's car.
The purpose of this test would be to ensure that our onlyCarOwner
function modifier is working as expected.
// perform the state transition
let tx;
let err;
try {
tx =
await instance.honkCar(
2,
1,
{
// account #3 does not own any cars, only account #1 and #2 do from: accounts[3], },
);
} catch (ex) {
err = ex;
}
Note that unlike the previous test, with this one, we have put the call to the smart contract within a try-catch block. The purpose of this is to be able to extract the error that occurs, so that we can perform some assertions on it:
// should not get a result, but an error should have been thrown
assert.ok(err);
assert.ok(!tx);
// check that the error reason is what you expect
assert.equal(err.reason, 'you need to own this car');
Here we check that we did not get a result,
and get an error instead.
Then check that the reason
property for the error matches the one that we
expect from the onlyCarOwner
modifier.
Add more tests
The honkCar()
function has another failure scenario,
so write a test for that too!
Hint: Look at the implementation of the smart contract to find it
Congratulations
🎉🎉🎉 You have written a test that checks for events that have been emitted, and you have also written test cases for both the happy path and failure scenarios for the same function.
Next, we will take a look at how to use mocking when writing tests.