Previously …
- Introduction to Solidity
- Introduction to Truffle
- Introduction to Mocha
- Truffle Testing Setup
- Testing Solidity Data Storage
- Testing Solidity State Machines
- Testing Solidity Events
Recap
Previously, we have set up a truffle project,
which contains a Cars
smart contract (the implmentation).
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 and events after some state transitions have occurred.
Modify the smart contract
Our cars contracts as it is now, does not really have anything that could be a target for mocking. So we will need to create a problem for ourselves, before solving it! 😜
Let's implement a new rule that our honkCar()
function needs to check,
which is that one is not allowed to honk between
midnight and six in the morning -
you don't want to wake your neighbours up and get a noise complaint!
Add these two new statements to the the honkCar()
function:
function honkCar(uint256 carId, uint256 otherCarId)
public
onlyCarOwner(carId)
{
require(cars[otherCarId].owner != address(0x00),
"other car must exist");
uint256 timeOfDay = (getTime() % 86400); require(timeOfDay >= 21600, "cannot honk between midnight and 6am"
);
emit CarHonk(carId, otherCarId);
}
The way we work out the time is to get the total number of seconds since epoch, divide that by the number of seconds per day, and take the remainder, which gives us the number of seconds since midnight for the current day.
Reading material: Modular arithmetic
The total number of seconds since epoch is obtained from the timestamp of the latest block in the blockchain. This is not the most accurate time, but it is good enough for this contract.
function getTime() internal view returns (uint256) {
// current block timestamp as seconds since unix epoch
// ref: https://solidity.readthedocs.io/en/v0.5.7/units-and-global-variables.html#block-and-transaction-properties
return block.timestamp; }
Aside: You might observe that this assumes that all the cars are in the UTC time zone - let's just roll with this, and say that that's OK for the purposes of this demonstration.
So let's try it out, and run the tests. If it is past midnight in UTC, your tests should be failing. So these tests will pass or fail, depending on the time of day that you run them - and that is obviously not a good thing.
… and, that's why we need to mock our smart contract!
Now, why did we create a separate function just to return a single value? We are about to see very shortly!
Creating a mocked contract
Create a new file to put our mocked contract in,
named contracts/mocks/MockedCars.sol
:
// mocked version of Cars contract - *only* used in tests
import "../Cars.sol";
contract MockedCars is Cars { // implementation of the contract
}
By saying MockedCars is Cars
, we have told the solidity compiler that
the MockedCars
contract inherits from the Cars
contract.
Since there is zero implementation within this contract,
this means that the MockedCars
contract is now identical to
the Cars
contract.
Let's go ahead and add some implementation:
uint256 public fakeBlockTimeStamp;
// override Cars.getTime()
function getTime() internal view returns (uint256) { return fakeBlockTimeStamp;
}
function _mock_setBlockTimeStamp(uint256 value) public {
fakeBlockTimeStamp = value;
}
We have done three things here:
- create a contract level variable,
fakeBlockTimeStamp
- create a
getTime()
function that overrides thegetTime()
function which was inherited - this is the mocked function - create a
_mock_setBlockTimeStamp()
function
After doing this, our mocked contract becomes useful,
because it does something differently from the original contract
which it inherits from.
In particular, it allows us to control how the contract behaves,
independently of block.timestamp
.
Now we are able to answer the earlier question:
Why did we create a separate function just to return a single value?
Deploying the mocked contract
Writing the mocked contract is not quite enough, we also have to deploy it in order for the specifications to use them.
In truffle, deployments occur through migrations, so create one using this command in the terminal:
truffle create migration mocked_cars
… and enter this into the file that gets generated,
which should be named something similar
to migrations/1554816184_mocked_cars.js
:
const MockedCars = artifacts.require("MockedCars");
module.exports = function(deployer) {
deployer.deploy(MockedCars);};
Now when we run truffle, our new MockedCars
contract will be deployed.
Modify the events specification
Open the specification we created prevfiously,
named test/Cars-events.spec.js
:
Right at the top, replace:
const Cars = artifacts.require('Cars');
… with:
// we require the mocked version of the Cars contract instead of the
// original Cars contract itself
const Cars = artifacts.require('MockedCars');
From this point on, whenever we run const instance = await Cars.deployed();
within this file, it will get us an instance of MockedCars
instead of Cars
.
Finally, within the before
block, let us make sure that we se the time of day
to a value that is on or after six in the morning so that our existing tests
do not fail:
// set a fake block time in the mocked contract
await instance._mock_setBlockTimeStamp((365 * 86400) + 21600);
At this point, please run your tests, and make sure that they all pass.
Write a new test
Now let's write a test where we attempt to honk a car when you are not allowed to!
Create a new test, within the existing contract
block:
it('Honking at car in the wee hours is not allowed', async () => {
const instance = await Cars.deployed();
// set a fake block time in the mocked contract
// perform the state transition
// assert that we get an error with the expected reason
});
For this test, we do not want the default time that was set
in the before
block - let's pick something that is at three in the morning:
// set a fake block time in the mocked contract
await instance._mock_setBlockTimeStamp((9999 * 86400) + 10800);
Now we attempt to run honkCar()
, with values that should otherwise work,
and capture the error:
// perform the state transition
let tx;
let err;
try {
tx =
await instance.honkCar(
2,
1,
{
// account #2 owns car #2
from: accounts[2],
},
);
} catch (ex) {
err = ex;
}
Next, we run some assertions on the error, to check that it has indeed been thrown for the expected reason:
// 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, 'cannot honk between midnight and 6am'); });
Run your tests again to make sure that this one passes as well.
Congratulations
🎉🎉🎉 You have written and deployed a mocked smart contract, and modified your existing tests to make use of it!
Your smart contract is now certifiably neighbour friendly, and noise complaint resistant! 😛