It’s basically like taking your contract out on a test drive before you deploy it to the mainnet and let all those wild crypto wolves loose on it. You write little snippets of code that simulate different scenarios, and then you run them through a fancy machine called a “test runner” to see if everything works as expected.
Now, why is this important for Solidity smart contracts? Well, because they’re notoriously difficult to debug once they’ve been deployed on the blockchain. If there’s an error in your code, it can be incredibly expensive (both financially and reputation-wise) to fix it after the fact. So by testing your contract thoroughly beforehand, you can catch any issues early on and save yourself a whole lot of headaches down the line.
But how do we actually write these tests? Let’s take a look at an example:
pragma solidity ^0.8.4;
contract MyContract {
uint public myVariable = 123; // Declares a public variable named myVariable with a value of 123
function setMyVariable(uint _newValue) external onlyOwner returns (bool success) { // Function to set the value of myVariable, can only be called by the contract owner and returns a boolean value
require(_msgSender() == owner(), "Only the contract owner can call this function"); // Checks if the caller is the contract owner, if not, reverts with an error message
if (_newValue > 999 || _newValue < 100) revert("Invalid value for myVariable"); // Checks if the new value is within a valid range, if not, reverts with an error message
myVariable = _newValue; // Sets the value of myVariable to the new value
emit MyEvent(msg.sender, "Set myVariable to: ", uint(_newValue)); // Emits an event with the caller's address and a message indicating the new value of myVariable
return true; // Returns a boolean value indicating the success of the function
}
event MyEvent(address indexed fromAddress, string memory message); // Declares an event named MyEvent with indexed fromAddress and a string message
}
Now let’s write a test for the `setMyVariable()` function. We want to make sure that if we call this function with an invalid value (i.e., less than 100 or greater than 999), it throws an error and reverts the transaction. Here’s what our test might look like:
pragma solidity ^0.8.4;
contract MyContract {
uint public myVariable; // declaring a public variable to be accessed by other contracts
function setMyVariable(uint _value) public { // declaring a function to set the value of myVariable
require(_value >= 100 && _value <= 999, "Value must be between 100 and 999"); // ensuring the value is within the specified range
myVariable = _value; // setting the value of myVariable to the input value
}
}
contract TestMyContract is DSTest {
MyContract contractUnderTest; // declaring a variable to store the contract being tested
function setUp() public {
contractUnderTest = new MyContract(); // creating a new instance of MyContract to be tested
}
function testSetMyVariableWithInvalidValue() public {
uint invalidValue = 500; // declaring an invalid value to be used as input
(bool success) = contractUnderTest.setMyVariable(invalidValue); // calling the setMyVariable function with the invalid value
assert(!success); // asserting that the transaction should not be successful
emit ShouldNotHappen(); // emitting an event to indicate that this code should not be executed
}
function testSetMyVariableWithValidValue() public {
uint validValue = 105; // declaring a valid value to be used as input
(bool success) = contractUnderTest.setMyVariable(validValue); // calling the setMyVariable function with the valid value
assert(success); // asserting that the transaction should be successful
emit ShouldHappen(); // emitting an event to indicate that this code should be executed
}
}
In this example, we’re using the `DSTest` framework to write our tests. This is just one of many test runners available for Solidity smart contracts others include Truffle and OpenZeppelin Test Suite. The basic idea is that you create a new contract (in this case called `TestMyContract`) that inherits from the test runner’s base class, and then define your tests inside it.
In our example, we have two tests: one for setting myVariable to an invalid value (less than 100), and another for setting it to a valid value (greater than or equal to 100). We use the `assert()` function to check that the expected behavior occurred in this case, whether the transaction succeeded or failed.
Unit testing for Solidity smart contracts isn’t as scary as it might seem at first glance. By writing tests before deploying your contract, you can catch any issues early on and save yourself a whole lot of headaches down the line.