Testing smart contracts is a must. Here's a look at some of the most important tests and popular tools used for this vital function.
What you will learn
Reentrancy and front-running are common smart contract problems.
Unit tests and integration tests are the two types of smart contract testing.
Truffle, Waffle, Chai, and Mocha are popular testing tools.
If smart contracts were people, they might be called stubborn or inflexible. But since they are computer code, we call them immutable. Once they are deployed they don't change — they can't be changed. Their immutable nature keeps developers from altering their code once they have been deployed. It's a valuable feature, but it means that testing smart contracts is essential before releasing them as a mainnet contract.
Testing can be an arduous task. In fact, the immutable nature of the smart contract demands that testing be rigorous and thorough. Luckily, numerous resources make it easier to test your smart contracts.
Many smart contracts handle high-value transactions. Even if you create a smart contract as a small passion project, you may have people use it to trade millions of dollars worth of assets. Minor flaws in your code could result in large sums of cryptocurrency or valuable NFTs being lost or stolen. Smart-contract testing can highlight vulnerabilities and help you avoid significant losses.
Because smart contracts are immutable, you won't be able to fix them after releasing them. Developers commonly must disable a smart contract and build and deploy a new one to fix a mistake. For example, Rubixi, a popular ponzi game, was released with a naming error that allowed anyone to set themselves as the contract owner. The funds stored in the Rubixi contract could be withdrawn at any time by the new "owner," leading to large sums of stolen funds. Since the contract was deployed to mainnet before the error was noticed, there was no way for the Rubixi devs to fix their mistake.
Reentrancy: This is one of the most common smart contract issues. It typically lets exploiters withdraw balances multiple times and can drain your smart contract's funds. In 2020, the popular decentralized exchange Uniswap lost $25 million to a hacker using a reentrancy exploit. Reentrancy vulnerabilities can be overlooked easily, so it's important to test your contract thoroughly before deploying it. All state changes need to be completed before calling external contracts to prevent reentrancy attacks. You'll also want to use function modifiers that prevent reentrancy attacks, such as "modifier noReentrant()."
Frontrunning: Transactions become public when they're submitted as pending transactions. Malicious traders can submit transactions with higher fees and guarantee their transaction is processed first. This practice can be troublesome if your goal is to write arbitrage contracts, as traders can copy your contract and set the fee slightly higher to front run your arbitrage opportunities. These issues can be difficult to avoid, but you can at least revert your transactions by adding a "transactionCount" value to your code. This value indicates the preferred transaction counter value and reverts your transaction if the counter's value isn't equal to the specified value. Of course, you will want to test this method to ensure it works for your contract.
In many cases, you'll want to test your smart contract using both Solidity and JavaScript. Solidity is the programming language used for smart contracts run on Ethereum, the dominant blockchain. Naturally, you would use it for testing the internal attributes of Solidity smart contracts. JavaScript is used to test your contract's external behavior.
Before writing tests, you'll want to consider your contract's purpose and business logic. Think about the problem it solves, how users will interact with it, how it interacts with other contracts, and what its limitations are. Mapping your contract's workflow before you write tests ensures you're considering the big picture while testing.
Unit testing is ideal for isolating and testing your contract's specific functions. Unit tests are often straightforward and run quickly. You should conduct unit tests early to save yourself time and trouble down the line. Before the first test, it's a good idea to create assertions: simple statements that detail your smart contract's intended behaviors. Your tests are then used to determine whether the assertions are true or false.
Unit testing should be completed before integration testing. It's essential to test valid and invalid inputs to ensure everything is in working order.
Integration testing lets you examine how your smart contract's components interact. Integration tests can also test a contract's interactions with other contracts. These tests are an ideal way to test inheritance and similar features.
Truffle is a test suite designed for blockchains that use the Ethereum Virtual Machine. It uses the Mocha framework, a JavaScript test framework using node.js. The primary difference between Truffle and the traditional Mocha framework is that Truffle uses "contract()" rather than "describe()."
Truffle includes a framework for straightforward automated tests using JavaScript, TypeScript, or Solidity. You can execute multiple test files using the "$ truffle test" command. When running a Truffle test, you'll need to create a test directory first.
Truffle makes testing easy with its Ganache tool, a browser-based, personal Ethereum blockchain that's used for safe contract testing. Ganache includes a built-in block explorer, allowing you to track test transactions easily. Ganache also features a "Deploy and Run Transactions" pane that lets you easily deploy your contract to mainnet after running your test code.
Chai is an assertion library that provides functions for comparing the output of a certain test with the expected value. Chai's straightforward syntax reads similarly to English, making it easy to understand.
Mocha, a JavaScript framework, is often used alongside Chai. Mocha is ideal for functions executed in a specific order and for ensuring that test cases run independently.
Promises are used for network operations, such as making HTTP requests. Writing a single promise can be quick and easy, but things get more complicated when combining multiple promises. Async/await is a particular syntax in JavaScript ES7 that makes coordinating asynchronous promises more straightforward.
The async function is a shortcut used for defining functions that use promises. The await function is often combined with async functions to allow them to wait for each other before executing.
Waffle is a smart contract testing tool created by ETHWorks. It lets you create new smart contracts and test them using Mocha and Chai. Many people choose Waffle over Truffle, as it is simpler and easier to learn.
The Hedera Smart Contract Service is EVM (Ethereum Virtual Machine) compatible and runs Solidity, a programming language used by 30% of all Web3 developers. Hedera’s Smart Contracts 2.0 equips Solidity smart contracts an EVM compatible smart contracts with the versatility of Hedera’s tokenization infrastructure by supporting native Hedera tokens and NFTs with the Hedera Token Service