Testing is an important aspect of developing smart contracts in Solidity. It helps ensure that your contracts are working as intended and can handle various scenarios and edge cases. In this chapter, we will learn how to write tests for Solidity contracts using Truffle and TestRPC.
What are Truffle and TestRPC?
Truffle is a popular development framework for Ethereum that includes a suite of tools for testing and debugging smart contracts. TestRPC is a Node.js based Ethereum client for testing purposes. It provides an in-memory blockchain that can be used for testing without the need to connect to a live network.
Setting Up a Testing Environment:
To set up a testing environment for Solidity contracts, you will need to install Truffle and TestRPC. To install Truffle, run the following command:
npm install -g truffle
To install TestRPC, run the following command:
npm install -g ethereumjs-testrpc
Once both Truffle and TestRPC are installed, you can create a new Truffle project by running the following command:
truffle init
This will create a new directory with the following structure:
├── contracts
├── migrations
├── test
└── truffle.js
The contracts
directory is where you will place your Solidity contracts. The migrations
directory is where you will place your migration scripts. The test
directory is where you will place your test files. The truffle.js
file is the Truffle configuration file.
To start a TestRPC instance, run the following command:
testrpc
This will start an instance of TestRPC with 10 accounts and a starting balance of 100 ether. You can also specify the number of accounts and starting balance by using the --accounts
and --balance
flags. For example, to start a TestRPC instance with 20 accounts and a starting balance of 1000 ether, run the following command:
testrpc --accounts 20 --balance 1000
Writing Tests:
To write tests for a Solidity contract, you will need to create a test file in the test
directory. The test file should have a .js
extension and should contain your test cases.
Here is an example of a test file for a simple contract called MyContract
:
var MyContract = artifacts.require("MyContract");
contract("MyContract", function(accounts) {
it("should set the value to 5", function() {
return MyContract.deployed().then(function(instance) {
instance.setValue(5);
return instance.getValue.call();
}).then(function(value) {
assert.equal(value, 5, "The value was not set to 5");
});
});
});
This test case tests the setValue
and getValue
functions of the MyContract
contract. It deploys an instance of the contract, calls the setValue
function with the value 5, and then calls the getValue
function to retrieve the value. Finally, it asserts that the value returned by the getValue
function is equal to 5.
Running Tests:
To run the tests, you will need to have a TestRPC instance running in the background. Once the TestRPC instance is running, you can run the tests by using the following command:
truffle test
This will run all the test files in the test
directory and display the results.
Advanced Testing Techniques:
There are several advanced techniques that you can use when writing tests for Solidity contracts. Here are a few examples:
- Mocking external calls: If your contract makes calls to external contracts or APIs, you can use mock libraries like
truffle-contract-mock
to mock these calls and test the contract in isolation. - Testing reverts: You can use the
assert.throws
function to test that a contract function reverts as expected in certain situations. - Testing events: You can use the
MyContract.deployed().then(function(instance) { instance.MyEvent().watch(function(error, result) { /* code */ });});
syntax to watch for events and test that they are emitted as expected.
Conclusion:
In this chapter, we learned how to set up a testing environment for Solidity contracts using Truffle and TestRPC, and how to write test cases for Solidity contracts. Testing is an important aspect of developing smart contracts and can help ensure that your contracts are working as intended.
Exercises:
To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.
Write a test case for a contract that has a function that adds two numbers and returns the result.
it("should add two numbers and return the result", function() {
return MyContract.deployed().then(function(instance) {
return instance.addNumbers(5, 7).then(function(result) {
assert.equal(result, 12, "The result was not correct");
});
});
});
Write a test case for a contract that has a function that reverts if the input is greater than 10.
it("should revert if the input is greater than 10", function() {
return MyContract.deployed().then(function(instance) {
return assert.throws(instance.revertIfGreaterThanTen(11));
});
});
Write a test case for a contract that has an event that is emitted when the value is changed.
it("should emit the ValueChanged event when the value is changed", function() {
return MyContract.deployed().then(function(instance) {
var eventWatcher = instance.ValueChanged();
return instance.changeValue(5).then(function() {
return eventWatcher.get();
}).then(function(events) {
assert.equal(events[0].args.newValue, 5, "The new value was not correct");
});
});
});
Write a test case for a contract that makes a call to an external contract and tests that the call was made correctly.
it("should make a call to an external contract and test that the call was made correctly", function() {
return MyContract.deployed().then(function(instance) {
var mockExternalContract = sinon.mock(ExternalContract.at(instance.address));
mockExternalContract.expects("someFunction").once().withArgs(5).returns(10);
return instance.callExternalContract(5).then(function(result) {
assert.equal(result, 10, "The result was not correct");
mockExternalContract.verify();
});
});
});
Write a test case for a contract that has a function that takes a string as input and reverts if the string is empty.
it("should revert if the input string is empty", function() {
return MyContract.deployed().then(function(instance) {
return assert.throws(instance.revertIfEmpty(""));
});
});