Smart Contract Testing: Javascript vs Solidity
Smart contracts, the self-executing code on blockchains, require rigorous testing to ensure their security and functionality. Two primary approaches emerge: testing in Javascript and testing directly in Solidity. This blog post delves into the pros and cons of each method, along with popular frameworks like Hardhat and Foundry.
Javascript Testing:
- Pros:
- Familiarity: Developers with Javascript experience can easily transition to writing tests.
- Rich Ecosystem: Javascript boasts a mature testing ecosystem with established frameworks like Mocha and Chai.
- Higher-Level Abstractions: Javascript frameworks facilitate mocking and manipulating blockchain environments, allowing for broader test coverage.
- Cons:
- Indirectness: Tests interact with the deployed smart contract through emulated environments, potentially missing edge cases specific to the blockchain’s execution.
- Dependency on Frameworks: Javascript tests rely on frameworks for blockchain interaction, adding an extra layer of complexity.
Solidity Testing:
- Pros:
- Directness: Tests directly interact with the deployed contract on a real blockchain, capturing the exact execution environment.
- Language Consistency: Writing tests in the same language as the contract promotes code readability and reduces context switching.
- Cons:
- Learning Curve: Developers unfamiliar with Solidity need to acquire new skills for writing tests.
- Limited Ecosystem: The Solidity testing ecosystem is still evolving, with fewer established frameworks compared to Javascript.
Hardhat vs. Foundry:
Both Hardhat and Foundry are popular frameworks for smart contract development, each offering functionalities for testing:
- Javascript with Hardhat: Provides a comprehensive environment for development, testing, and deployment, including a built-in testing framework with assertions and mocks. Ideal for developers familiar with Javascript and for situations where rapid test development and a rich ecosystem are crucial.
- Solidity with Foundry: Emphasizes developer experience with a focus on ease of use and rapid testing. It utilizes Forge, a custom language based on Solidity, for writing tests and interacting with the blockchain. Preferred for projects requiring the highest level of test accuracy and for teams comfortable with Solidity development.
Ultimately, a hybrid approach combining both Javascript and Solidity testing might be optimal for certain scenarios, leveraging the strengths of each method. Continuously evaluate your project’s needs and adapt your testing strategy accordingly.
Javascript with Hardhat
Project Setup:
- Initialize a Node.js project:
npm init -y
- Install Hardhat:
npm install --save-dev hardhat
- Create a Hardhat project:
npx hardhat init
(Choose an emptyhardhat.config.js
)
Sample Smart Contract (Greeter.sol):
pragma solidity ^0.8.9;
contract Greeter {
string private greeting;
constructor(string memory _greeting) {
greeting = _greeting;
}
function greet() public view returns (string memory) {
return greeting;
}
function setGreeting(string memory _newGreeting) public {
greeting = _newGreeting;
}
}
Test File (test/greeter.js):
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Greeter", function () {
it("Should deploy with a greeting", async function () {
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, world!");
await greeter.deployed();
expect(await greeter.greet()).to.equal("Hello, world!");
});
it("Should be able to change the greeting", async function () {
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hola");
await greeter.deployed();
await greeter.setGreeting("Bonjour");
expect(await greeter.greet()).to.equal("Bonjour");
});
});
Explanation:
- Imports:
chai
: Testing assertion library.hardhat
: Provides the Hardhat Runtime Environment for interacting with the Ethereum network.
describe
block:- Defines a test suite for the
Greeter
contract.
- Defines a test suite for the
it
blocks:- Individual test cases.
- The first test checks if the contract deploys with the correct initial greeting.
- The second test verifies the ability to update the greeting.
ethers.getContractFactory
:- Compiles the
Greeter
contract and creates a factory to deploy instances.
- Compiles the
greeter.deployed()
:- Deploys the contract and waits for its deployment to complete.
expect(...).to.equal(...)
:- Chai assertions to verify if contract state matches expectations.
Running Tests:
Use the following command in your project directory:
npx hardhat test
Solidity with Foundry
Project Setup
- Install Foundry: Follow the instructions at https://getfoundry.sh/
- Initialize Project: Run
forge init
in an empty directory.
Sample Smart Contract (Counter.sol)
Solidity
pragma solidity ^0.8.9;
contract Counter {
uint256 public count;
function increment() public {
count++;
}
function decrement() public {
require(count > 0, "Cannot decrement below zero");
count--;
}
}
Test File (test/Counter.t.sol)
pragma solidity ^0.8.9;
import "forge-std/Test.sol";
contract CounterTest is Test {
Counter public counter;
function setUp() public {
counter = new Counter();
}
function testIncrement() public {
counter.increment();
assertEq(counter.count(), 1);
}
function testDecrement() public {
counter.decrement();
assertEq(counter.count(), 0);
}
function testDecrementFail() public {
vm.expectRevert(bytes("Cannot decrement below zero"));
counter.decrement();
}
}
Explanation
- Import:
forge-std/Test.sol
: Foundry’s standard testing library, providing assertion functions, cheatcodes (for simulating conditions), etc.
CounterTest
contract:- Inherits from the
Test
contract. Test contracts must inherit fromTest
.
- Inherits from the
setUp()
function:- Deploys a fresh instance of the
Counter
contract before each test case.
- Deploys a fresh instance of the
test...
functions:- Individual test cases.
testIncrement
andtestDecrement
test the basic functionality of the counter.testDecrementFail
demonstrates how to test for expected reverts usingvm.expectRevert
.
assertEq()
:- A built-in assertion function to verify if the expected and actual values match.
vm
:- A global object in Foundry tests, providing cheatcodes to interact with the blockchain environment (e.g.,
vm.expectRevert
for checking reverts).
- A global object in Foundry tests, providing cheatcodes to interact with the blockchain environment (e.g.,
Running the Tests
In your project directory, use the following command:
forge test
Conclusion
Both Javascript and Solidity offer valuable approaches for testing smart contracts, each with distinct advantages and disadvantages. Javascript testing shines with familiarity, a rich ecosystem of tools, and higher-level abstractions. However, it can lack directness and introduce dependencies on frameworks. In contrast, Solidity testing boasts direct interaction with the deployed contract, ensuring accurate test execution, and maintaining code consistency. However, it comes with a steeper learning curve and a less mature ecosystem.
Ultimately, the choice depends on your project’s specific needs and your team’s expertise. For rapid prototyping and projects with a strong Javascript focus, Javascript testing might be the ideal starting point. However, for projects demanding the highest level of test accuracy and dealing primarily with Solidity development, Solidity testing is a compelling choice. In some scenarios, a hybrid approach incorporating elements of both methods could offer the best of both worlds.