commit
4abab07272
After Width: | Height: | Size: 55 KiB |
@ -1,4 +1,329 @@ |
||||
Examples |
||||
Testing by Example |
||||
============ |
||||
|
||||
coming soon |
||||
Here are some examples which can give you better understanding to plan your tests. |
||||
|
||||
**Note:** Examples in this section are intended to give you a push for development. We don't recommend to rely on them without verifying at your end. |
||||
|
||||
### 1. Simple example |
||||
In this example, we test setting & getting variables. |
||||
|
||||
Contract/Program to be tested: `Simple_storage.sol` |
||||
|
||||
``` |
||||
pragma solidity >=0.4.22 <0.7.0; |
||||
|
||||
contract SimpleStorage { |
||||
uint public storedData; |
||||
|
||||
constructor() public { |
||||
storedData = 100; |
||||
} |
||||
|
||||
function set(uint x) public { |
||||
storedData = x; |
||||
} |
||||
|
||||
function get() public view returns (uint retVal) { |
||||
return storedData; |
||||
} |
||||
} |
||||
``` |
||||
Test contract/program: `simple_storage_test.sol` |
||||
|
||||
``` |
||||
pragma solidity >=0.4.22 <0.7.0; |
||||
import "remix_tests.sol"; |
||||
import "./Simple_storage.sol"; |
||||
|
||||
contract MyTest { |
||||
SimpleStorage foo; |
||||
|
||||
// beforeEach works before running each test |
||||
function beforeEach() public { |
||||
foo = new SimpleStorage(); |
||||
} |
||||
|
||||
/// Test if initial value is set correctly |
||||
function initialValueShouldBe100() public returns (bool) { |
||||
return Assert.equal(foo.get(), 100, "initial value is not correct"); |
||||
} |
||||
|
||||
/// Test if value is set as expected |
||||
function valueIsSet200() public returns (bool) { |
||||
foo.set(200); |
||||
return Assert.equal(foo.get(), 200, "value is not 200"); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### 2. Testing a method involving `msg.sender` |
||||
In Solidity, `msg.sender` plays a great role in access management of a smart contract methods interaction. Different `msg.sender` can help to test a contract involving multiple accounts with different roles. Here is an example for testing such case: |
||||
|
||||
Contract/Program to be tested: `Sender.sol` |
||||
|
||||
``` |
||||
pragma solidity >=0.4.22 <0.7.0; |
||||
contract Sender { |
||||
address private owner; |
||||
|
||||
constructor() public { |
||||
owner = msg.sender; |
||||
} |
||||
|
||||
function updateOwner(address newOwner) public { |
||||
require(msg.sender == owner, "only current owner can update owner"); |
||||
owner = newOwner; |
||||
} |
||||
|
||||
function getOwner() public view returns (address) { |
||||
return owner; |
||||
} |
||||
} |
||||
``` |
||||
|
||||
Test contract/program: `Sender_test.sol` |
||||
|
||||
``` |
||||
pragma solidity >=0.4.22 <0.7.0; |
||||
import "remix_tests.sol"; // this import is automatically injected by Remix |
||||
import "remix_accounts.sol"; |
||||
import "./Sender.sol"; |
||||
|
||||
// Inherit 'Sender' contract |
||||
contract SenderTest is Sender { |
||||
/// Define variables referring to different accounts |
||||
address acc0; |
||||
address acc1; |
||||
address acc2; |
||||
|
||||
/// Initiate accounts variable |
||||
function beforeAll() public { |
||||
acc0 = TestsAccounts.getAccount(0); |
||||
acc1 = TestsAccounts.getAccount(1); |
||||
acc2 = TestsAccounts.getAccount(2); |
||||
} |
||||
|
||||
/// Test if initial owner is set correctly |
||||
function testInitialOwner() public { |
||||
// account at zero index (account-0) is default account, so current owner should be acc0 |
||||
Assert.equal(getOwner(), acc0, 'owner should be acc0'); |
||||
} |
||||
|
||||
/// Update owner first time |
||||
/// This method will be called by default account(account-0) as there is no custom sender defined |
||||
function updateOwnerOnce() public { |
||||
// check method caller is as expected |
||||
Assert.ok(msg.sender == acc0, 'caller should be default account i.e. acc0'); |
||||
// update owner address to acc1 |
||||
updateOwner(acc1); |
||||
// check if owner is set to expected account |
||||
Assert.equal(getOwner(), acc1, 'owner should be updated to acc1'); |
||||
} |
||||
|
||||
/// Update owner again by defining custom sender |
||||
/// #sender: account-1 (sender is account at index '1') |
||||
function updateOwnerOnceAgain() public { |
||||
// check if caller is custom and is as expected |
||||
Assert.ok(msg.sender == acc1, 'caller should be custom account i.e. acc1'); |
||||
// update owner address to acc2. This will be successful because acc1 is current owner & caller both |
||||
updateOwner(acc2); |
||||
// check if owner is set to expected account i.e. account2 |
||||
Assert.equal(getOwner(), acc2, 'owner should be updated to acc2'); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### 3. Testing method execution |
||||
|
||||
With Solidity, one can directly verify the changes made by a method in storage by retrieving those variables from a contract. But testing for a successful method execution takes some strategy. Well that is not entirely true, when a test is successful - it is usually obvious why it passed. However, when a test fails, it is essential to understand why it failed. |
||||
|
||||
To help in such cases, Solidity introduced the `try-catch` statement in version `0.6.0`. Previously, we had to use low-level calls to track down what was going on. |
||||
|
||||
Here is an example test file that use both **try-catch** blocks and **low level calls**: |
||||
|
||||
Contract/Program to be tested: `AttendanceRegister.sol` |
||||
|
||||
``` |
||||
pragma solidity >=0.4.22 <0.7.0; |
||||
contract AttendanceRegister { |
||||
struct Student{ |
||||
string name; |
||||
uint class; |
||||
} |
||||
|
||||
event Added(string name, uint class, uint time); |
||||
|
||||
mapping(uint => Student) public register; // roll number => student details |
||||
|
||||
function add(uint rollNumber, string memory name, uint class) public returns (uint256){ |
||||
require(class > 0 && class <= 12, "Invalid class"); |
||||
require(register[rollNumber].class == 0, "Roll number not available"); |
||||
Student memory s = Student(name, class); |
||||
register[rollNumber] = s; |
||||
emit Added(name, class, now); |
||||
return rollNumber; |
||||
} |
||||
|
||||
function getStudentName(uint rollNumber) public view returns (string memory) { |
||||
return register[rollNumber].name; |
||||
} |
||||
} |
||||
``` |
||||
|
||||
Test contract/program: `AttendanceRegister_test.sol` |
||||
|
||||
``` |
||||
pragma solidity >=0.4.22 <0.7.0; |
||||
import "remix_tests.sol"; // this import is automatically injected by Remix. |
||||
import "./AttendanceRegister.sol"; |
||||
|
||||
contract AttendanceRegisterTest { |
||||
|
||||
AttendanceRegister ar; |
||||
|
||||
/// 'beforeAll' runs before all other tests |
||||
function beforeAll () public { |
||||
// Create an instance of contract to be tested |
||||
ar = new AttendanceRegister(); |
||||
} |
||||
|
||||
/// For solidity version greater or equal to 0.6.0, |
||||
/// See: https://solidity.readthedocs.io/en/v0.6.0/control-structures.html#try-catch |
||||
/// Test 'add' using try-catch |
||||
function testAddSuccessUsingTryCatch() public { |
||||
// This will pass |
||||
try ar.add(101, 'secondStudent', 11) returns (uint256 r) { |
||||
Assert.equal(r, 101, 'wrong rollNumber'); |
||||
} catch Error(string memory /*reason*/) { |
||||
// This is executed in case |
||||
// revert was called inside getData |
||||
// and a reason string was provided. |
||||
Assert.ok(false, 'failed with reason'); |
||||
} catch (bytes memory /*lowLevelData*/) { |
||||
// This is executed in case revert() was used |
||||
// or there was a failing assertion, division |
||||
// by zero, etc. inside getData. |
||||
Assert.ok(false, 'failed unexpected'); |
||||
} |
||||
} |
||||
|
||||
/// Test failure case of 'add' using try-catch |
||||
function testAddFailureUsingTryCatch1() public { |
||||
// This will revert on 'require(class > 0 && class <= 12, "Invalid class");' for class '13' |
||||
try ar.add(101, 'secondStudent', 13) returns (uint256 r) { |
||||
Assert.ok(false, 'method execution should fail'); |
||||
} catch Error(string memory reason) { |
||||
// Compare failure reason, check if it is as expected |
||||
Assert.equal(reason, 'Invalid class', 'failed with unexpected reason'); |
||||
} catch (bytes memory /*lowLevelData*/) { |
||||
Assert.ok(false, 'failed unexpected'); |
||||
} |
||||
} |
||||
|
||||
/// Test another failure case of 'add' using try-catch |
||||
function testAddFailureUsingTryCatch2() public { |
||||
// This will revert on 'require(register[rollNumber].class == 0, "Roll number not available");' for rollNumber '101' |
||||
try ar.add(101, 'secondStudent', 11) returns (uint256 r) { |
||||
Assert.ok(false, 'method execution should fail'); |
||||
} catch Error(string memory reason) { |
||||
// Compare failure reason, check if it is as expected |
||||
Assert.equal(reason, 'Roll number not available', 'failed with unexpected reason'); |
||||
} catch (bytes memory /*lowLevelData*/) { |
||||
Assert.ok(false, 'failed unexpected'); |
||||
} |
||||
} |
||||
|
||||
/// For solidity version less than 0.6.0, low level call can be used |
||||
/// See: https://solidity.readthedocs.io/en/v0.6.0/units-and-global-variables.html#members-of-address-types |
||||
/// Test success case of 'add' using low level call |
||||
function testAddSuccessUsingCall() public { |
||||
bytes memory methodSign = abi.encodeWithSignature('add(uint256,string,uint256)', 102, 'firstStudent', 10); |
||||
(bool success, bytes memory data) = address(ar).call(methodSign); |
||||
// 'success' stores the result in bool, this can be used to check whether method call was successful |
||||
Assert.equal(success, true, 'execution should be successful'); |
||||
// 'data' stores the returned data which can be decoded to get the actual result |
||||
uint rollNumber = abi.decode(data, (uint256)); |
||||
// check if result is as expected |
||||
Assert.equal(rollNumber, 102, 'wrong rollNumber'); |
||||
} |
||||
|
||||
/// Test failure case of 'add' using low level call |
||||
function testAddFailureUsingCall() public { |
||||
bytes memory methodSign = abi.encodeWithSignature('add(uint256,string,uint256)', 102, 'duplicate', 10); |
||||
(bool success, bytes memory data) = address(ar).call(methodSign); |
||||
// 'success' will be false if method execution is not successful |
||||
Assert.equal(success, false, 'execution should be successful'); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
|
||||
### 4. Testing a method involving `msg.value` |
||||
In Solidity, ether can be passed along with a method call which is accessed inside contract as `msg.value`. Sometimes, multiple calculations in a method are performed based on `msg.value` which can be tested with various values using Remix's Custom transaction context. See the example: |
||||
|
||||
Contract/Program to be tested: `Value.sol` |
||||
|
||||
``` |
||||
pragma solidity >=0.4.22 <0.7.0; |
||||
contract Value { |
||||
uint256 public tokenBalance; |
||||
|
||||
constructor() public { |
||||
tokenBalance = 0; |
||||
} |
||||
|
||||
function addValue() payable public { |
||||
tokenBalance = tokenBalance + (msg.value/10); |
||||
} |
||||
|
||||
function getTokenBalance() view public returns (uint256) { |
||||
return tokenBalance; |
||||
} |
||||
} |
||||
``` |
||||
Test contract/program: `Value_test.sol` |
||||
|
||||
``` |
||||
pragma solidity >=0.4.22 <0.7.0; |
||||
import "remix_tests.sol"; |
||||
import "./Value.sol"; |
||||
|
||||
contract ValueTest{ |
||||
Value v; |
||||
|
||||
function beforeAll() public { |
||||
// create a new instance of Value contract |
||||
v = new Value(); |
||||
} |
||||
|
||||
/// Test initial balance |
||||
function testInitialBalance() public { |
||||
// initially token balance should be 0 |
||||
Assert.equal(v.getTokenBalance(), 0, 'token balance should be 0 initially'); |
||||
} |
||||
|
||||
/// For Solidity version greater than 0.6.1 |
||||
/// Test 'addValue' execution by passing custom ether amount |
||||
/// #value: 200 |
||||
function addValueOnce() public payable { |
||||
// check if value is same as provided through devdoc |
||||
Assert.equal(msg.value, 200, 'value should be 200'); |
||||
// execute 'addValue' |
||||
v.addValue{gas: 40000, value: 200}(); // introduced in Solidity version 0.6.2 |
||||
// As per the calculation, check the total balance |
||||
Assert.equal(v.getTokenBalance(), 20, 'token balance should be 20'); |
||||
} |
||||
|
||||
/// For Solidity version less than 0.6.2 |
||||
/// Test 'addValue' execution by passing custom ether amount again using low level call |
||||
/// #value: 100 |
||||
function addValueAgain() public payable { |
||||
Assert.equal(msg.value, 100, 'value should be 100'); |
||||
bytes memory methodSign = abi.encodeWithSignature('addValue()'); |
||||
(bool success, bytes memory data) = address(v).call.gas(40000).value(100)(methodSign); |
||||
Assert.equal(success, true, 'execution should be successful'); |
||||
Assert.equal(v.getTokenBalance(), 30, 'token balance should be 30'); |
||||
} |
||||
} |
||||
``` |
||||
|
Loading…
Reference in new issue