diff --git a/docs/images/a-unit-testing-custom-compiler-config.png b/docs/images/a-unit-testing-custom-compiler-config.png new file mode 100644 index 0000000000..8e3719c284 Binary files /dev/null and b/docs/images/a-unit-testing-custom-compiler-config.png differ diff --git a/docs/unittesting.md b/docs/unittesting.md index 431d148a54..0e998301cb 100644 --- a/docs/unittesting.md +++ b/docs/unittesting.md @@ -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,6 +39,45 @@ Click the button "Run tests" to executes all tests whose box has been checked be ![](images/a-unit-testing-run-result.png) +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`. + +![](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 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"); +} +``` + +**Note:** To use custom `msg.sender` functionality, `remix_accounts.sol` should be imported in your test file. + +Complete example can be seen in [examples](./unittesting_examples) section. + +Regarding `gas`, Remix estimate 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` + + +Points to remember +------------------ + +* A test contract can not have a method with parameters. Having one such method will show error: `Method 'methodname' can not have parameters inside a test contract` +* No. 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 + Continuous integration ---------------------- diff --git a/docs/unittesting_examples.md b/docs/unittesting_examples.md index 964533cc9d..264ca61d7a 100644 --- a/docs/unittesting_examples.md +++ b/docs/unittesting_examples.md @@ -1,4 +1,318 @@ -Examples +Testing by Example ============ -coming soon \ No newline at end of file +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 { + address account0; + address account1; + address account2; + + /// Initiate accounts variable + function beforeAll() public { + account0 = TestsAccounts.getAccount(0); + account1 = TestsAccounts.getAccount(1); + account2 = TestsAccounts.getAccount(2); + } + + /// Test if initial owner is set correctly + function testInitialOwner() public { + // account-0 is default account, so current owner should be account0 + Assert.equal(getOwner(), account0, 'owner should be account-0'); + } + + /// Update owner. This method will be called by account0 as there is not custom sender appointed + function updateOwnerOnce() public { + // check method call is as expected + Assert.ok(msg.sender == account0, 'caller should default account i.e. account0'); + // update owner address to account1 + updateOwner(account1); + // check if owner is set to expected account + Assert.equal(getOwner(), account1, 'owner should be updated to account1'); + } + + /// Update owner again by defining custom sender + /// #sender: account-1 + function updateOwnerOnceAgain() public { + // check if caller is custom and is as expected + Assert.ok(msg.sender == account1, 'caller should be custom account i.e. account1'); + // update owner address to account2. This will be successful because account1 is current owner & caller both + updateOwner(account2); + // check if owner is set to expected account i.e. account2 + Assert.equal(getOwner(), account2, 'owner should be updated to account2'); + } +} +``` + +### 3. Testing method execution + +With Solidity, one can verify the changes made by a method in storage by accessing those variables out of a contract. But while testing whether method execution was successful and if execution failed, what was the reason behind it, can also be an importnat case. +Solidity introduced `try-catch` statement in version 0.6.0 which helps a lot to solve this purpose. Previously, this could be achieved using low-level call to some extent. Here is the example to test such a scenario: + +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'); + } +} +``` \ No newline at end of file