Testing of Web3 Applications: Automation with the Ethers Library
Imagine this scenario: a client comes in and says, "I heard that automated tests help with regression testing, so new features don't break the existing ones. Let's do it! Preferably by tomorrow." No problem, you think you’ve written tests before, so it shouldn't be too hard. But then the client adds, "It probably doesn't matter, but the app is in Web3." Suddenly you feel the blood drain from your head. Web3?! Now that’s a challenge!
Testing a Web3 application is quite challanging. Typically, authentication requires a wallet, an external application that you can't easily add to the project's dependencies.You might consider using solutions known from the Synpress library, where with the help of Puppeteer you can handle MetaMask without major interventions – you would just run the source code and based on that write some tests to simulate real application use, including authentication.
However, MetaMask, like everyone else keeping up with the times, has also implemented security measures that effectively hide the source code. Version 10.25 was the last one where you could easily view the wallet's source code. The world didn't collapse immediately, as a workaround was to include a package with an older version of the application to the project. However, among the numerous arguments to avoid outdated tools, the ultimate one was when Chrome stopped cooperating with such a setup.
Since Chrome stopped supporting Manifest 2, it is likely that MetaMask v10.25 can no longer be run in tests. Downgrading Chrome is almost impossible because they don’t provide old versions. The only thing that comes to mind in this situation is to use the test version of Chrome, which allows running one of the previous versions of the browser. Currently, it’s enough to roll back one release to version 124.0.6367.119, using the command install chrome@124.0.6367.119 when initiating tests. The only question is – how long will such an impermanent method be sufficient?
The numerous factors that could break the tests, combined with the awareness of the makeshift solution, and the difficulty of configuring the test environment naturally led to abandoning thisapproach. At the cost of testing user authentication, during test initiation, one can inject a wallet that tells our application "hey, I'm connected". How to do it? For example, by using the ethers library.
Start by installing two libraries:
- ethers (in my case v6.13)
- @metamask/eth-sig-util (in my case v7.0)
// ./cypress/support/e2e.js
import { Wallet } from "ethers";
import { personalSign } from "@metamask/eth-sig-util";
// inject ethers wallet
Cypress.on("window:before:load", async (win) => {
const wallet = new Wallet(Cypress.env("PRIV_KEY"));
const address = await wallet.getAddress();
win.ethereum = {
on: (eventName, callback) => {
if (eventName === "accountsChanged") {
callback([address]);
}
},
request: async (payload) => {
if (payload.method === "eth_requestAccounts") {
return Promise.resolve([address]);
}
if (payload.method === "eth_chainId") {
return Promise.resolve("0x1");
}
if (payload.method === "personal_sign") {
const signature = personalSign({
privateKey: Buffer.from(wallet.privateKey.slice(2), "hex"),
data: payload.params[0],
});
return signature;
}
return Promise.resolve([address]);
},
isMetaMask: true,
};
});
Next, modify the e2e.js file inside the support folder. This file is automatically loaded before the tests start, there's no need to manually initiate it For our purposes, it is only necessary to log in and sign the session, but if you need other methods, check the documentation for @metamask/eth-sig-util.
For our application, it's not strictly necessary, but for added safety, you can specify the network ID to which you are logging into. Make sure to use the hex code (for example 0x1 for Ethereum Mainnet, 0x89 for Polygon Mainnet, and so on).
The only other thing needed besides modifying the e2e.js file to authenticate within the application is a private key saved in the cypress.env.json:
{
"PRIV_KEY": "privateKeyLongStringTakenFromWallet",
}
And that's it. With these steps, we've simplified the process of testing Web3 applications, making it more reliable. Working with modern technology like Web3 can be challenging, but finding solutions to its unique problems is all the more satisfying. Remember, sometimes all you need to know is where to hit with the hammer – even if it's a Web3 hammer! 😊
If you encounter any issues with Babel or Webpack during the test run create and edit the ./cypress/plugins/index.js file (be sure to install the necessary dependencies for @cypress/webpack-preprocessor).
// ./cypress/plugins/index.js
const webpack = require('@cypress/webpack-preprocessor');
module.exports = async (on, config) => {
const options = {
webpackOptions: {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
},
};
on('file:preprocessor', webpack(options));
// important: return the changed config
return config
}
And then, in the main root, add a babel.config.js file, and specify the configuration there.
{
"presets": ["@babel/preset-env"]
}