Prior to commencing this tutorial, please ensure that you have installed the following RSK workshop pre-requisites on your system: POSIX compliant shell NodeJs OpenZeppelin CLI Java curl Code editor RSKj Project setup Use git to make a copy of this repo, and use npm to install dependencies. git git@github.com:bguiz/workshop-rsk-smart-contract-testing-ozcli.git workshop-rsk-smart-contract-testing-ozcli npm install clone cd Then open up this directory in your code editor. Explore the files If you happen to have tree installed, you can use that to view the directory structure using the following command. $ tree -aI . ├── contracts │ └── Cars.sol ├── networks.js ├── .openzeppelin │ └── project.json ├── scripts │ ├── clean.sh │ └── setup.sh └── └── Cars.spec.js 4 directories, 6 files 'node_modules|*.md|package*.json|.git*' test (Otherwise use your choice of GUI to explore this folder.) Observe that we have the following files: : OZ CLI has already been pre-configured to work with the structure for this project. .openzeppelin/project.json : OZ CLI has already been pre-configured to connect to your choice of RSK Regtest, RSK Testnet, or RSK Mainnet. networks.js and : These are custom scripts which generate keys and configuration that will be used by OZ CLI when connecting to RSK networks. scripts/clean.sh scripts/setup.sh : This is the smart contract. The solidity file is the implementation, and has been completed for you. contracts/Cars.sol If you are familiar will Truffle, you may notice that there is no corresponding deployment script (also known as migration contract) OZ ClI takes a different approach, instead persisting migration status within JSON files within the directory. .openzeppelin : This is the specification, and is only partially complete. This workshop is focused on completing the specification. test/Cars.spec.js Ensure that you have a copy of RSKj running in Regtest locally, and then run the set up script: bash ./scripts/ .sh setup This will set up the RSK specific files for this project which are specific to you at this time. Observe the output in your terminal for more details. Implementation Look at contracts/Cars.sol. We have a smart contract implementation that involves manipulating several car objects. pragma solidity ^ ; contract Cars { enum CarStatus { driving, parked } event CarHonk (uint256 indexed fromCar, uint256 indexed atCar); struct Car { bytes3 colour; uint8 doors; uint256 distance; uint16 lat; uint16 lon; CarStatus status; address owner; } uint256 public numCars = ; mapping( Car) public cars; () public {} { (msg.value > ether, ); carId = ++numCars; Car memory newCar = Car( colour, doors, distance, lat, lon, CarStatus.parked, msg.sender ); cars[carId] = newCar; } modifier onlyCarOwner(uint256 carId) { (cars[carId].owner == msg.sender, ); _; } modifier onlyCarStatus(uint256 carId, CarStatus expectedStatus) { (cars[carId].status == expectedStatus, ); _; } { cars[carId].status = CarStatus.driving; } { cars[carId].status = CarStatus.parked; cars[carId].lat = lat; cars[carId].lon = lon; } { (cars[otherCarId].owner != address( ), ); uint256 timeOfDay = (getTime() % ); (timeOfDay >= , ); emit CarHonk(carId, otherCarId); } { block.timestamp; } } 0.5 .0 0 => uint256 constructor ( ) ( ) function addCar bytes3 colour, uint8 doors, uint256 distance, uint16 lat, uint16 lon public payable returns uint256 carId require 0.1 "You need at least 0.1 ETH to get a car" require "you need to own this car" require "car is not in the required status" ( ) ( ) ( ) function driveCar uint256 carId public onlyCarOwner carId onlyCarStatus carId, CarStatus.parked ( ) ( ) ( ) function parkCar uint256 carId, uint16 lat, uint16 lon public onlyCarOwner carId onlyCarStatus carId, CarStatus.driving ( ) ( ) function honkCar uint256 carId, uint256 otherCarId public onlyCarOwner carId require 0x00 "other car must exist" 86400 require 21600 "cannot honk between midnight and 6am" ( ) ( ) 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 We are not really concerned about how to write this implementation for this workshop, but we do need to know what the implementation does in order to be able to write tests for it. Specification, incomplete Look at . test/Cars.spec.js Here, we have an incomplete specification. We obtain the Cars smart contract defined in our implementation earlier, using . This is OZ CLI's analogue of using NodeJs to obtain the implementation when testing Javascript using Mocha. Those of you familiar with Truffle might recognise this as being the equivalent of . contract.fromArtifact() require() artifacts.require() Unlike Truffle, where we make use of blocks to group tests, in OZ CLI tests, we use blocks to group our tests; exactly as how we would do so when using Mocha. We can do this because OZ CLI's test - - enables us to access the list of accounts up-front. Thus there is no need to obtain the via the block's callback function. contract describe environment @openzeppelin/test-environment accounts describe { accounts, contract } = ( ); assert = ( ); web3 = ( ); BN = web3.utils.BN; Cars = contract.fromArtifact( ); describe( , () => { [owner] = accounts; instance; before( () => { instance = Cars.new({ : owner }); }); it( , () => { initialNumCars = instance.numCars.call(); }); }); describe( , () => { [owner] = accounts; instance; before( () => { instance = Cars.new({ : owner }); }); it( , () => { }); }); describe( , () => { [owner] = accounts; instance; before( () => { instance = Cars.new({ : owner }); }); it( , () => { }); it( , () => { }); }); const require '@openzeppelin/test-environment' const require 'assert' const require 'web3' const const 'Cars' 'Cars - initial state' const let async await from 'Initialised with zero cars' async const await // TODO perform assertions 'Cars - state transitions' const let async await from 'Adds a new car' async // preview the return value without modifying the state // ... (redacted for brevity) ... // TODO perform the assertions 'Cars - events' const let async await from // set up contract with relevant initial state // ... (redacted for brevity) ... // 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 // ... (redacted for brevity) ... 'Honks a car at another car' async // perform the state transition // ... (redacted for brevity) ... // TODO perform assertions 'Honking a car that you do not own is not allowed' async // perform the state transition // ... (redacted for brevity) ... // TODO perform assertions Note that we have several instances of as comments. In these cases, there is test code set up and already available in the demo repo, but it has been omitted here to keep this document short. The intent here is to show the overall structure. These parts indicate code that performs the steps within the test specifications. When writing specifications for your smart contracts, you will need to do this from scratch, but for the sake of demonstration it is already there in full. // ... (redacted for brevity) ... Note that we have four occurrences of in the test code, and in this workshop we will be writing those assertions. // TODO perform assertions Also, note that within the block for , we have a before block. This is used to set up the state of the contract by adding a couple of car objects, because these particular tests only make sense if there already are car objects stored within the smart contract. This has already been done for you, so that you may focus on writing the tests. contract 'Cars - events' Initial test run At this point, we are all set to let Mocha, our test runner, do its thing, which will execute out specification, which in turn will execute our implementation. npm run test You should see output similar to the following: $ npm run > workshop-rsk-smart-contract-testing-ozcli@0.0.0 /home/bguiz/code/rsk/workshop-rsk-smart-contract-testing-ozcli > oz compile && mocha -- --recursive ./ /**/*.spec.js ✓ Compiled contracts with solc 0.5.17 (commit.d19bba13) Cars - initial state ✓ Initialised with zero cars Cars - state transitions ✓ Adds a new car (124ms) Cars - events ✓ Honks a car at another car ✓ Honking a car that you not own is not allowed (44ms) 4 passing (608ms) test test exit test do Great! Our test runner (Mocha) has run successfully! 🎉 🎉 🎉 Our test runner has done the above, listening for which tests have passed or failed, and if there were any errors thrown. However, note that since we have tests in our specification, and they are indeed interacting with the smart contract (implementation), but none of them are performing any assertions. Thus, at this point, we don't know whether the implementation is correct or not. four That means that it is time to write our first ! assertions Writing a test for initial state Edit . test/Cars.spec.js Replace the line that says with an assertion. It should now look like this: // TODO perform assertions it( , () => { initialNumCars = instance.numCars.call(); assert.equal(initialNumCars.toString(), ); }); 'Initialised with zero cars' async const await '0' This test is grouped within a block. When there are multiple tests within the same block, the state of the smart contract is not reset between one test and the next. However, when there are multiple tests in different blocks, the state of the smart contract is indeed reset between one block and the next, as we are doing this explicitly by setting up a new instance variable in each one. contract contract describe describe For those accustomed to working with Truffle, this is analogous to doing within block. In OZ CLI, instead of doing this, we use the method described above. This might take a bit of getting used to, but is indeed exactly how one would do this in "regular" Javascript testing with Mocha. const instance = await Cars.deployed(); each it In this case, this is the first (and only) block within this block, so it is perfect for testing the initial state of the smart contract. it describe The line retrieves the value of the variable in the smart contract. const initialNumCars = await instance.numCars.call(); numCars The line passes the test if this value , and fails the test if this value . assert.equal(initialNumCars.toString(), '0'); is zero is anything other than zero Test run for initial state Now we are going to let Mocha, our test runner, do its thing again. This time we have a test defined in our specification, so when mocha executes our specification, it will indeed execute out implementation in turn. Run Mocha. npm run test You should see some output similar to the following: $ npm run > workshop-rsk-smart-contract-testing-ozcli@0.0.0 /home/bguiz/code/rsk/workshop-rsk-smart-contract-testing-ozcli > oz compile && mocha -- --recursive ./ /**/*.spec.js Nothing to compile, all contracts are up to date. Cars - initial state ✓ Initialised with zero cars (59ms) Cars - state transitions ✓ Adds a new car (122ms) Cars - events ✓ Honks a car at another car ✓ Honking a car that you not own is not allowed (45ms) 4 passing (693ms) test test exit test do Great! 🎉 🎉 🎉 Mocha, our test runner has worked as promised, listening for which tests have passed or failed, and if there were any errors thrown. This time we have verification not only that our implementation has been executed, but also that it is correct, at least according to how we have written our tests. The output is almost identical to the output before, except that it takes a (marginally) longer time to execute. The main thing that we need to look out for here is whether we have gone from having 4 tests passing to less than 4 tests passing. This would indicate that there is either a problem with our specification (a false negative), or a problem with our implementation (a true negative). Testing the initial state of a smart contract is the simplest possible type of test we can write. Now let's move on to for state transitions and events. more complex tests Writing a test for state transition Edit . test/Cars.spec.js Replace the two lines that say with assertions. It should now look like this: // TODO perform assertions it( , () => { returnValue = instance.addCar.call( , BN( ), BN( ), BN( ), BN( ), { : accounts[ ], : web3.utils.toWei( , ), }, ); assert.equal(returnValue.toString(), ); tx = instance.addCar( , BN( ), BN( ), BN( ), BN( ), { : accounts[ ], : web3.utils.toWei( , ), }, ); numCars = instance.numCars.call(); car1 = instance.cars.call( BN( )); assert.equal(numCars.toString(), ); assert.equal(car1.colour, ); assert.equal(car1.doors.toString(), ); assert.equal(car1.distance.toString(), ); assert.equal(car1.lat.toString(), ); assert.equal(car1.lon.toString(), ); assert.equal(car1.status.toString(), ); assert.equal(car1.owner, accounts[ ]); }); 'Adds a new car' async // preview the return value without modifying the state const await '0xff00ff' // colour: purple new 4 // doors: 4 new 0 // distance: 0 new 0 // lat: 0 new 0 // lon: 0 from 1 value '0.11' 'ether' '1' // perform the state transition const await '0xff00ff' // colour: purple new 4 // doors: 4 new 0 // distance: 0 new 0 // lat: 0 new 0 // lon: 0 from 1 value '0.11' 'ether' // retrieve the updated state const await const await new 1 // perform the assertions '1' '0xff00ff' '4' '0' '0' '0' '1' // parked 1 The line retrieves the return value of the function. Some participants in this workshop may have noticed something that is perhaps a little strange: const returnValue = await instance.addCar.call(/* ... */); addCar is a function that causes a state transition, as it the values stored in the smart contract. In fact it has the nor function modifiers. In our smart contract invocation, we are executing and not . addCar updates neither view pure .addCar.call() .addCar() Usually we use when invoking or functions, so why are we using here on a function which explicitly causes a state transition? .call() view pure .call() The answer to that is not exactly straightforward: We are doing so to "emulate" what the return value of this particular call to the smart contract would be, actually creating the state transition. Think of this as "previewing" the function invocation. The reason we need to do this is because if it were a true function invocation that resulted in a state transition on the smart contract, we don't have access to the return value. without The line is the first assertion, and will fail this test if the new is any value other than one. assert.equal(returnValue.toString(), '1'); carId The line is where the actual state transition occurs. This is a "true" invocation of the function, unlike the previous "preview" invocation of the function. When this line has been executed, a transaction has been added to a block, and that block to the blockchain. This test, and any other test that involves a smart contract state transition, will be significantly than tests that do not, such as the one that we wrote earlier for the initial state. const tx = await instance.addCar(/* ... */); addCar addCar slower The lines and retrieve the new/ updated state from the smart contract. const numCars = await instance.numCars.call(); const car1 = await instance.cars.call(new BN(1)); The remaining lines are many statements. These will fail this test if the new/ updated state does not match the expected values. assert.equal() Test run for state transition Now we are going to run our tests again. This time we have two tests. Run Mocha. npm run test You should see output similar to the following $ npm run > workshop-rsk-smart-contract-testing-ozcli@0.0.0 /home/bguiz/code/rsk/workshop-rsk-smart-contract-testing-ozcli > oz compile && mocha -- --recursive ./ /**/*.spec.js Nothing to compile, all contracts are up to date. Cars - initial state ✓ Initialised with zero cars Cars - state transitions ✓ Adds a new car (176ms) Cars - events ✓ Honks a car at another car ✓ Honking a car that you not own is not allowed (45ms) 4 passing (654ms) test test exit test do All four tests continue passing. Great! 🎉 🎉 🎉 Again, the main thing that we are looking out for here is that all of the tests continue passing. If one of the tests began to fail, we know that there is either a problem with the implementation (a true negative), or a problem with our specification (a false negative). Test run with false negative for state transition If you are feeling in an exploratory mood, you can try the following out: Replace - one of the assertions in this test - with . assert.equal(car1.colour, '0xff00ff'); assert.equal(car1.colour, '0xff00aa'); Run the tests again, using . npm run test This time, observe that the output indicates an assertion error: $ npm run > workshop-rsk-smart-contract-testing-ozcli@0.0.0 /home/bguiz/code/rsk/workshop-rsk-smart-contract-testing-ozcli > oz compile && mocha -- --recursive ./ /**/*.spec.js Nothing to compile, all contracts are up to date. Cars - initial state ✓ Initialised with zero cars Cars - state transitions 1) Adds a new car Cars - events ✓ Honks a car at another car (42ms) ✓ Honking a car that you not own is not allowed (46ms) 3 passing (740ms) 1 failing 1) Cars - state transitions Adds a new car: AssertionError [ERR_ASSERTION]: == + expected - actual -0xff00ff +0xff00aa at Context.<anonymous> ( /Cars.spec.js:74:12) at processTicksAndRejections (internal/process/task_queues.js:97:5) npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! workshop-rsk-smart-contract-testing-ozcli@0.0.0 : `oz compile && mocha -- --recursive ./ /**/*.spec.js` npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the workshop-rsk-smart-contract-testing-ozcli@0.0.0 script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. test test exit test do '0xff00ff' '0xff00aa' test test exit test test Of course in this case, we were expecting it, and already know that the problem lies in the specification; in particular, an incorrect assertion. However, in a real (non-demo) scenario, when we encounter this, we would only know that we have encountered a test failure. We would then require an investigation to determine whether this was due to a problem in the implementation, causing a true negative; or conversely whether there was a problem with the specification, causing a false negative. If you have chosen to do this additional step, do before continuing with the rest of this workshop. remember to revert the change Writing a test for events Edit . test/Cars.spec.js As mentioned previously, this block contains a block which sets up the smart contract instance to contain two cars prior to running any tests. This has been done for you, so you may skim over it, and get right to writing some tests. contract before Replace the first line that says with assertions. The it block should now look like this: // TODO perform assertions it( , () => { tx = instance.honkCar( , , { : accounts[ ], }, ); { logs } = tx; assert.ok( .isArray(logs)); assert.equal(logs.length, ); log = logs[ ]; assert.equal(log.event, ); assert.equal(log.args.fromCar.toString(), ); assert.equal(log.args.atCar.toString(), ); }); 'Honks a car at another car' async // perform the state transition const await 2 1 // account #2 owns car #2 from 2 // inspect the transaction & perform assertions on the logs const Array 1 const 0 'CarHonk' '2' '1' In our previous test, where we invoked , we did not use the return value ( ) in the remainder of the test. In this test, we will. addCar tx The line invokes the function, and saves the transaction in . const tx = await instance.honkCar(/* ... */); honkCar tx The next three lines, beginning with , extract . The assert statements will fail this test if there is no array, or if it has a number of logs that is anything other than one. const { logs } = tx; tx.logs tx.logs Note that in RSK, transaction logs are generated when an event is emitted within that transaction. This is equivalent to the behaviour of transaction logs in Ethereum. The next four lines, beginning with , extract the first and only event from this transaction. The assertion statements will fail this test if the event is not of the expected type, or contains unexpected parameters. const log = logs[0]; So far, in each block we have had only one test, but this time we'll be doing something different, with two tests sharing the same block. describe describe Replace the second line that says with assertions. // TODO perform assertions it( , () => { tx; err; { tx = instance.honkCar( , , { : accounts[ ], }, ); } (ex) { err = ex; } assert.ok(err); assert.ok(!tx); }); 'Honking a car that you do not own is not allowed' async // perform the state transition let let try await 2 1 // account #3 does not own any cars, only account #1 and #2 do from 3 catch // should not get a result, but an error should have been thrown The line is similar to the invocation from before. However, if you take a look at the parameters, you will notice that we attempt to operate a car using an account that . const tx = await instance.honkCar(/* ... */); honkCar does not own it Also, unlike the invocation in the previous test, this statement has been surrounded by a block, because we are expecting this invocation to throw an error. try ... catch Note that in the implementation, contracts/Cars.sol, the function has a function modifier for , which contains this statement: . The purpose of this is that only a car's owner is allowed to honk it. honkCar(carId,otherCarId) onlyCarOwner(carId) require(cars[carId].owner == msg.sender, "you need to own this car"); Thus far, all of our tests have been "happy path" cases, where the smart contract functions are always called in the expected way. These tests ensure that the smart contract behaves as it is supposed to, when those interacting with it do the "right thing". However, external behaviour is something that is within the locus of our control, and therefore by definition we need to ensure that our smart contract is able to handle these "failure path" cases too. In this case our implementation appears to have handled it, and we are writing a test within the specification to verify said handling. not The final two lines, , will fail this test if the invocation succeeded, when it was not supposed to. assert.ok(err); and assert.ok(!tx); honkCar Remember: We are testing the "happy path" here. Instead we are testing the "failure path". not Test run for events Now we are going to run our tests again. This time we have four tests. Run Mocha. npm run test You should see output similar to the following $ npm run > workshop-rsk-smart-contract-testing-ozcli@0.0.0 /home/bguiz/code/rsk/workshop-rsk-smart-contract-testing-ozcli > oz compile && mocha -- --recursive ./ /**/*.spec.js Nothing to compile, all contracts are up to date. Cars - initial state ✓ Initialised with zero cars Cars - state transitions ✓ Adds a new car (124ms) Cars - events ✓ Honks a car at another car ✓ Honking a car that you not own is not allowed (87ms) 4 passing (718ms) test test exit test do All four are still passing. Great! 🎉 🎉 🎉 Conclusion We have now created specifications for testing initial state, state transitions, and events in a smart contract written in Solidity. We have also configured the OpenZeppelin CLI to connect to RSK networks, and used Mocha as a test runner to execute our specifications via the OpenZepplin test environment. Going further We have now completed this workshop. Congratulations on making it to the end! There is a lot more to explore with regards to Smart contract testing. For example, you may have noticed that in the implementation for , we have commented out a statement that verifies the value of . Writing a robust specification for this implementation is not possible, as it behaves differently depending on the time of day it is run. Mocking is a testing technique that will enable us to replace one (or sometimes more) functions within a smart contract in order to be able to test it in particular ways, and will help in this case. honkCar() require() getTime() seemingly Check out if you would like to try out as a continuation of this tutorial. (This workshop is a modified and shortened version from that original.) DApps Dev Club's Mocking Solidity for Tests smart contract mocking Previously published at https://github.com/bguiz/workshop-rsk-smart-contract-testing-ozcli/blob/master/walkthru.md