Merge pull request #2720 from ethereum/testing-examples

testing examples added
pull/1/head
yann300 5 years ago committed by GitHub
commit 4abab07272
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. BIN
      docs/images/a-unit-testing-custom-compiler-config.png
  2. 57
      docs/unittesting.md
  3. 329
      docs/unittesting_examples.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

@ -30,7 +30,7 @@ Apart from this, Remix allows usage of some special functions to make testing mo
* `afterEach()` - Runs after each test
* `afterAll()` - Runs after all tests
To get started, see [this](https://github.com/ethereum/remix/blob/master/remix-tests/tests/examples_4/SafeMath_test.sol) for sample implementation.
To get started, see [this simple example](./unittesting_examples.html#simple-example).
Run Tests
------------------
@ -39,10 +39,57 @@ Click the button "Run tests" to executes all tests whose box has been checked be
![](images/a-unit-testing-run-result.png)
Continuous integration
Customization
------------------
Remix facilitates users with various types of customizations to test a contract properly.
**1. Custom Compiler Context**
`Solidity Unit Testing` refers `Solidity Compiler` plugin for compiler configurations. One can provide customized inputs for `Compiler`, `EVM Version` & `Enable Optimization` and these will be the configuration settings used for contract compilation before running unit tests.
![](images/a-unit-testing-custom-compiler-config.png)
**2. Custom Transaction Context**
For a contract method interaction, prime parameters of transaction are `from` address, `value` & `gas`. Usually, we need to test a method's behaviour under different values of these parameters.
Remix provides the functionality of custom `msg.sender` & `msg.value` of transaction using method devdoc like:
```
/// #sender: account-0
/// #value: 10
function checkSenderIs0AndValueis10 () public payable {
Assert.equal(msg.sender, TestsAccounts.getAccount(0), "wrong sender in checkSenderIs0AndValueis10");
Assert.equal(msg.value, 10, "wrong value in checkSenderIs0AndValueis10");
}
```
Things to keep in mind while using custom transaction context:
1. Parameters must be defined in devdoc of related method
2. Each parameter key should be prefixed with a hash (**#**) and end with a colon following a space (**: **) like `#sender: ` & `#value: `
3. For now, customization is available for parameters `sender` & `value` only
4. Sender is `from` address of a transaction which is accessed using `msg.sender` inside a contract method. It should be defined in a fixed format as '**account-**<account_index>'
5. `<account_index>` varies from `0-2` before remix-ide release v0.10.0 and `0-9` afterwards
6. `remix_accounts.sol` must be imported in your test file to use custom `sender`
7. Value is `value` sent along with a transaction in `wei` which is accessed using `msg.value` inside a contract method. It should be a number.
Regarding `gas`, Remix estimates the required gas for each transaction internally. Still if a contract deployment fails with `Out-of-Gas` error, it tries to redeploy it by doubling the gas. Deployment failing with double gas will show error: ```contract deployment failed after trying twice: The contract code couldn't be stored, please check your gas limit```
Various test examples can be seen in [examples](./unittesting_examples) section.
Points to remember
------------------
* A test contract cannot have a method with parameters. Having one such method will show error: `Method 'methodname' can not have parameters inside a test contract`
* Number of test accounts are `3` before remix-ide release v0.10.0 and `10` afterwards
* A test file which imports `remix_accounts.sol` might not compile successfully with `Solidity Compiler` plugin but it will work fine with Solidity Unit Testing plugin.
Remix-tests
----------------------
remix-tests is also a CLI, it can be used in a continuous integration environment which support node.js.
Please find more information in the [remix-tests repository](https://github.com/ethereum/remix/tree/master/remix-tests)
`remix-tests` is the module which works underneath of remix-ide `Solidity Unit Testing` plugin.
`remix-tests` is an [NPM package](https://www.npmjs.com/package/remix-tests). It can also be used as a CLI/CI solution, supporting node.js. Find more information about this type of usage in the [remix-tests repository](https://github.com/ethereum/remix/tree/master/remix-tests#as-command-line-interface)
See also: example [Su Squares contract](https://github.com/su-squares/ethereum-contract/tree/e542f37d4f8f6c7b07d90a6554424268384a4186) and [Travis build](https://travis-ci.org/su-squares/ethereum-contract/builds/446186067) that uses remix-tests for continuous integration testing.
For CI implementation example, see [Su Squares contract](https://github.com/su-squares/ethereum-contract/tree/e542f37d4f8f6c7b07d90a6554424268384a4186) and [Travis build](https://travis-ci.org/su-squares/ethereum-contract/builds/446186067) that uses `remix-tests` for continuous integration testing.

@ -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…
Cancel
Save