Well written set of tests plays a crucial role in delivering a reliable software. A good test suit ensures that the application works as intended and significantly reduces the number of bugs. It's easier said than done. One of the steps to achieve this goal is adhering to boundaries of different test levels. In this article, I want to focus on the integration testing of decentralized applications that rely on web3. For integration testing, I'm able to sacrifice the speed of execution to gain the confidence that different components of the app seamlessly work together.
Last time, I presented a more E2E solution for mocking web3 during black-box tests where I injected web3 into the window object. This technique required a connection to the Ethereum node and locally running Ganache in that case. It's an elegant solution when you care about the ability to automate testing also against the test network, Ropsten, for example. It's not a silver bullet though.
Recently I was working on the smart contract deployment from the browser. Running robust E2E test cases for components responsible for that functionality is too slow. Luckily, Ganache CLI also provides programmatic access to the provider that automatically mines subsequent blocks. We can use it to spin up ganache instance quickly and prevent sharing blockchain state between test reruns.
Ganache CLI provider
To swap provider for selected test suits, we use Jest's mocking feature. Let's create a separate module for the web3 provider getter.
// web3Provider.ts
import Web3 from "web3";
export function provider() {
return Web3.givenProvider;
}
We can then use this module to instantiate a web3.
// web3.ts
import Web3 from "web3";
import { provider } from "./web3Provider";
export const web3 = new Web3(provider());
The implementation of the provider we use for testing is slightly different.
// someModule.spec.ts
jest.mock("./web3Provider", () => {
function provider() {
return require("ganache-cli").provider();
}
return { provider };
});
In the test cases where we mocked the provider, all transactions are going to run on ganache. This way we have a fresh blockchain instance each time we test the app.
Skipping slow tests
Although automating the Ganache start and making it part of the test case is an improvement over having it running as a separate application, it still takes a few seconds. This is more than I feel comfortable with to run along with my unit tests. I came up with a workaround that mitigates this issue.
We wrap slow-running tests with slowDescribe as opposed to using Jest's describe directly.
describe("BettorAgreement", () => {
slowDescribe("deploy", () => {
it("deploys a contract", async () => {
const [account] = await web3.eth.getAccounts();
const contract = await deploy("1000000", { from: account, gas: 3000000 });
expect(contract.options.address).toContain("0x");
});
});
});
If ALLOW_SLOW
environment variable equals to false, we skip the given set of tests entirely.
function allowSlow() {
return `${process.env.ALLOW_SLOW}`.toLowerCase() !== "false";
}
export function slowDescribe(msg: string, handler: jest.EmptyFunction) {
if (allowSlow()) {
describe(msg, handler);
} else {
describe.skip(msg, handler);
}
}
I'm somewhat against modifying the "production" code just for the sake of making some hacks in tests possible. In a month no one will remember what the intention behind such implementation was. Nonetheless, you could check the result of allowSlow inside the provider and bet on UglifyJS to remove the dead code from the bundle.
// web3Provider.ts
import Web3 from "web3";
export function provider() {
if (allowSlow()) return require("ganache-cli").provider();
return Web3.givenProvider;
}
Conclusion
The dynamic nature of JavaScript gives us the flexibility to mock and patch both test code and actual implementation. Remember that your test suit is also a code you have to maintain and overengineering comes at a cost so try to keep things simple.
Photo by Louis Reed on Unsplash.