Truffle development framework provides two methods for testing Ethereum smart contract: solid testing at blockchain level and JavaScript testing at DApp level. In this tutorial, we will introduce the purpose, difference and application scenario of these two testing methods of Ethereum smart contract, and learn how to use the solid test case and JavaScript test case to integrate Ethereum intelligence through a specific example Contract for unit testing and integration testing.
Ethereum tutorials in seven development languages: Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart
1. Ethereum smart contract Testing Overview
As software developers, we all know that testing is a very important part to make the code run normally. The decentralized software based on blockchain is no exception, and due to the immutability of blockchain, testing is more important for blockchain software.
Generally speaking, there are two types of software testing: unit testing and integration testing. Unit testing focuses on the testing of a single function, while the goal of integration testing is to ensure that all parts of the code can be combined and run as expected.
Truffle is the most widely used Ethereum smart contract and DApp development framework. It provides two methods for testing Ethereum smart contract: Solidity test and JavaScript test. The question is, which method should we choose?
The answer is both.
Writing test cases of smart contracts with Solidity allows us to test at the blockchain level. This kind of test case can call the contract method just as the case is deployed in the blockchain. To test the internal behavior of smart contracts, we can:
- Write a solid unit test to check the return value of the smart contract function and the value of the status variable
- Write a solid integration test to check the interaction between smart contracts. These integration tests ensure that mechanisms like inheritance or dependency injection work as expected
We also need to make sure that smart contracts show the right external behavior. In order to test the smart contract from outside the blockchain, we use web3.js in JavaScript test cases, just like when developing DApp. We need to build confidence that the DApp front end can call the smart contract correctly. This aspect of testing belongs to integration testing.
Therefore, in short, the Solidity test case is mainly used to verify the internal implementation logic of the smart contract, which can be used for unit test and integration test; while the JavaScript test case is mainly used to verify the external behavior of the smart contract, which is usually used for integration test.
2. Sample project of Ethereum smart contract test
Suppose we have two Ethereum smart contracts to test: Background and Entrypoint.
Background is an internal contract, and our DApp front end will not directly interact with it. EntryPoint is a smart contract for DApp interaction, and the background contract will be accessed inside the EntryPoint contract.
The following is the solidity code of the Background contract:
pragma solidity >=0.5.0; contract Background { uint[] private values; function storeValue(uint value) public { values.push(value); } function getValue(uint initial) public view returns(uint) { return values[initial]; } function getNumberOfValues() public view returns(uint) { return values.length; } }
Above, we see that the Background contract provides three functions:
- storeValue(uint): write value
- getValue(uint): read value
- getNumberOfValues(): get the number of values
All three contract functions are simple, so unit testing is easy.
Here is the entity code for the EntryPoint contract:
pragma solidity >=0.5.0; import "./Background.sol"; contract EntryPoint { address public backgroundAddress; constructor(address _background) public{ backgroundAddress = _background; } function getBackgroundAddress() public view returns (address) { return backgroundAddress; } function storeTwoValues(uint first, uint second) public { Background(backgroundAddress).storeValue(first); Background(backgroundAddress).storeValue(second); } function getNumberOfValues() public view returns (uint) { return Background(backgroundAddress).getNumberOfValues(); } }
In the constructor of the EntryPoint contract, we inject the deployment address of the Background contract and store it in a state variable backgroundAddress. The EntryPoint contract exposes three functions:
- getBackgroundAddress(): returns the deployment address of the Background contract
- storeTwoValues(uint, uint): save two values
- getNumberOfValues(): number of returned values
Because the storeTwoValues(uint, uint) function calls a function in the Background contract twice, it is difficult to unit test this function. getNumberOfValues() has the same problem, so these two functions are more suitable for integration testing.
3. Solid case of Ethereum smart contract test
In this section, we learn how to write solid unit test cases and integration test cases for smart contracts. Let's start with a simpler unit test.
Here is the code for the TestBackground test:
pragma solidity >=0.5.0; import "truffle/Assert.sol"; import "truffle/DeployedAddresses.sol"; import "../../../contracts/Background.sol"; contract TestBackground { Background public background; // Run before every test function function beforeEach() public { background = new Background(); } // Test that it stores a value correctly function testItStoresAValue() public { uint value = 5; background.storeValue(value); uint result = background.getValue(0); Assert.equal(result, value, "It should store the correct value"); } // Test that it gets the correct number of values function testItGetsCorrectNumberOfValues() public { background.storeValue(99); uint newSize = background.getNumberOfValues(); Assert.equal(newSize, 1, "It should increase the size"); } // Test that it stores multiple values correctly function testItStoresMultipleValues() public { for (uint8 i = 0; i < 10; i++) { uint value = i; background.storeValue(value); uint result = background.getValue(i); Assert.equal(result, value, "It should store the correct value for multiple values"); } } }
The purpose of this unit test is to ensure that the Background contract can:
- Save the new value in the values array
- Return values by index
- Save multiple values in the values array
- Returns the size of the values array
The following TestEntryPoint test contains a unit test testItHasCorrectBackground() to verify that the EntryPoint contract functions as expected:
pragma solidity >=0.5.0; import "truffle/Assert.sol"; import "truffle/DeployedAddresses.sol"; import "../../../contracts/Background.sol"; import "../../../contracts/EntryPoint.sol"; contract TestEntryPoint { // Ensure that dependency injection working correctly function testItHasCorrectBackground() public { Background backgroundTest = new Background(); EntryPoint entryPoint = new EntryPoint(address(backgroundTest)); address expected = address(backgroundTest); address target = entryPoint.getBackgroundAddress(); Assert.equal(target, expected, "It should set the correct background"); } }
This function tests dependency injection. As mentioned earlier, other functions in the EntryPoint contract need to interact with the Background contract, so we have no way to test these functions separately and need to verify them in the integration test. Here is the code for the integration test:
pragma solidity >=0.5.0; import "truffle/Assert.sol"; import "truffle/DeployedAddresses.sol"; import "../../../contracts/Background.sol"; import "../../../contracts/EntryPoint.sol"; contract TestIntegrationEntryPoint { BackgroundTest public backgroundTest; EntryPoint public entryPoint; // Run before every test function function beforeEach() public { backgroundTest = new BackgroundTest(); entryPoint = new EntryPoint(address(backgroundTest)); } // Check that storeTwoValues() works correctly. // EntryPoint contract should call background.storeValue() // so we use our mock extension BackgroundTest contract to // check that the integration workds function testItStoresTwoValues() public { uint value1 = 5; uint value2 = 20; entryPoint.storeTwoValues(value1, value2); uint result1 = backgroundTest.values(0); uint result2 = backgroundTest.values(1); Assert.equal(result1, value1, "Value 1 should be correct"); Assert.equal(result2, value2, "Value 2 should be correct"); } // Check that entry point calls our mock extension correctly // indicating that the integration between contracts is working function testItCallsGetNumberOfValuesFromBackground() public { uint result = entryPoint.getNumberOfValues(); Assert.equal(result, 999, "It should call getNumberOfValues"); } } // Extended from Background because values is private in actual Background // but we're not testing background in this unit test contract BackgroundTest is Background { uint[] public values; function storeValue(uint value) public { values.push(value); } function getNumberOfValues() public view returns(uint) { return 999; } }
We can see that TestIntegrationEntryPoint uses an extension of Background, that is, BackgroundTest defined at line 43 as our simulation contract, which allows our test case to check whether EntryPoint calls the correct function of the contract deployed at backgroundAddress.
4. JavaScript case of Ethereum smart contract test
We write integration tests in JavaScript to ensure that the external behavior of the contract meets the expected requirements, so we have information to develop dapps based on these smart contracts.
Here is our JavaScript test file entryPoint.test.js:
const EntryPoint = artifacts.require("./EntryPoint.sol"); require('chai') .use(require('chai-as-promised')) .should(); contract("EntryPoint", accounts => { describe("Storing Values", () => { it("Stores correctly", async () => { const entryPoint = await EntryPoint.deployed(); let numberOfValues = await entryPoint.getNumberOfValues(); numberOfValues.toString().should.equal("0"); await entryPoint.storeTwoValues(2,4); numberOfValues = await entryPoint.getNumberOfValues(); numberOfValues.toString().should.equal("2"); }); }); });
Using the functions in the EntryPoint contract, JavaScript testing can ensure that we can pass the value outside the blockchain into the smart contract by using the transaction, which is realized by calling the storeTwoValues(uint,uint) function (line 15) of the contract.
5. Test course section of Ethereum smart contract
When it comes to testing smart contracts, it can be said that the more, the better, you should cover all possible execution paths to return the expected results. Truffle provides two methods: the solid unit test and integration test of blockchain layer, and the JavaScript integration test of DApp level. We need to comprehensively use these two test methods according to the code implementation of the smart contract in our actual work to ensure that the operation of the smart contract meets the expectations.
Original link: Two methods of testing Ethereum smart contract Huizhi network