You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
329 lines
12 KiB
329 lines
12 KiB
Testing by Example
|
|
============
|
|
|
|
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');
|
|
}
|
|
}
|
|
```
|
|
|