remix-project mirror
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.
remix-project/docs/unittesting_examples.md

330 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');
}
}
```