Michal Zalecki
Michal Zalecki
software development, testing, JavaScript,
TypeScript, Node.js, React, and other stuff

Integration tests and mocking web3 apps

Decentralized applications present a new set of challenges. One of them is testing. Transaction lifecycle is more complex than the old-school POST request/response flow and errors are often less than helpful. Although developer experience is getting better, this puts into perspective how testing is essential.

Error: VM Exception while processing transaction: revert

is a new

Uncaught TypeError: undefined is not a function

The most significant change from the developers perspective when switching from Web 2.0 backend to Ethereum dApps is that you cannot expect "request" return value straight away in the response. Transaction hash is available just after you send the transaction, but it does not mean that transaction will succeed and that miners will include it into the blockchain. This is how handing transactions may look like in your React/Redux app.

this.props.addTransaction({ id });
AwesomeContract.methods.awesomeMethod(web3.utils.asciiToHex(values.awesomeString))
  .send({ from: account })
  .on("transactionHash", txhash => this.props.setTransactionHash({ id, txhash }))
  .on("receipt", receipt => this.props.setTransactionReceipt({ id, receipt }))
  .on("error", error => this.props.setTransactionError({ id, error }))
  .on("confirmation", confirmation => this.props.setTransactionConfirmation({ id, confirmation }));

Testing each function to handle the particular state of the transaction is very easy. That is especially true if you implement the business logic of handling transaction in reducers.

Testing entire flow is quite a challenge and would require a lot of mocking to make it a unit test quick to complete. That is why I prefer to test it using integration tests.

Recently I enjoy testing using Cypress, excellent, hassle-free developer experience. Cypress is running an instance of chrome which is, of course, missing web3 instance on the window object. Not having access to MetaMask or Mist is going to be a common problem no matter which tool for integration tests you use. My solution is to inject web3 instance that does not require any user action to sign the transaction. By attaching to window:before:load event, we can modify window object before the app code runs.

import Web3 from "web3";
import PrivateKeyProvider from "truffle-privatekey-provider";

cy.on("window:before:load", (win) => {
  const provider = new PrivateKeyProvider(Cypress.env("ETH_PRIV_KEY"), Cypress.env("ETH_PROVIDER"));
  win.web3 = new Web3(provider); // eslint-disable-line no-param-reassign
});

There a few ways to use environment variables in Cypress. Do not let the name of truffle-privatekey-provider fool you. It is not a truffle dependent package.

Possibility to use PrivateKeyProvider in tests does not end here. You can also test how your application UI reacts to events triggered by "another user" by making a transaction directly from the test scenario code. I hope that this gives you some insights how to test your dApp.

You can find example on GitHub that tests a simple dApp and changes the smart contract state.

Photo by rawpixel on Unsplash.