In this comprehensive guide, you’ll learn how to implement end-to-end testing for your decentralized application using Synpress. We’ll walk through testing a MetaMask-integrated application, covering everything from setup to execution.
After this article You should be able to test all Metamask featureAfter completing this guide, you’ll be able to:
- Create User Acceptance Testing With Product Manager ☑️
- Test Your Dapp Frontend Components ☑️
- Test Your Smart Contract ☑️
- Test Blockchain Networks ☑️
- Generate Reports And Videos For Marketing And Analysis ☑️
Table Of Content
Project Initialization
- Forking the Ready-to-Use Template
- Project Structure Overview
Initial Setup
- Installing Dependencies
- Setting Up Environment Configuration
- Example Environment Variables
Configuration Setup
- DApp URL Configuration
- Cypress Configuration
- Environment Variable Handling
- Base URL and Video Configuration
Browser Launch Configuration
- Setting Browser-Specific Test Execution Parameters
- Handling Different Browser Families (Chrome, Firefox, Electron)
Plugin Configuration
- Synpress Plugin Setup
- ESBuild and Cucumber Preprocessor Integration
Fail-Fast Configuration
- Optimizing Test Execution with Fail-Fast
Cypress Configuration Export
- Exporting the Complete Cypress Configuration
Writing Test Scenarios with Cucumber
- Structuring Test Scenarios Collaboratively with Product Owners
- Example: Accessing the Application with MetaMask
Understanding Gherkin Syntax
- Using Given, When, Then, And in Test Cases
- Example: Smart Contract Deployment and Transaction Confirmation
Complete Example: MetaMask Interactions
- MetaMask Connection and DApp Operations
- Test Case Examples for Signing and Transaction Approvals
Conclusion
- Next Steps and Further Reading
- Reference to Synpress Example and Documentation
References
- Synpress Example
- Synpress Documentation
✍️ All code mentioned on this article is included on this
GitHub repositoryProject InitializationGetting Started
Begin by forking our ready-to-use template:
git clone [repository-url]Project Structure
The cloned project follows this organized structure:
.
├── Dockerfile
├── cypress
│ ├── downloads
│ ├── report
│ │ ├── cucumber.html
│ │ ├── messages
│ │ ├── screenshots
│ │ └── video
│ │
│ ├── screenshots
│. │
│ ├── support
│ │ ├── commands.js
│ │ ├── common.steps.js
│ │ ├── e2e-synpress.js
│ │ ├── e2e.js
│ │ ├── page-objects
│ │ │ └── home.js
│ │ └── support.js
│ └── tests
│ ├── 01-basic-connection.feature
│ ├── 01-basic-connection.steps.js
│ ├── 02-signatures.feature
│ ├── 02-signatures.steps.js
│ ├── 03-network-management.feature
│ ├── 03-network-management.steps.js
│ ├── 04-token-management.feature
│ ├── 04-token-management.steps.js
│ ├── 05-transactions.feature
│ └── 05-transactions.steps.js
├── cypress.config.js
├── cypress.env.json
├── package-lock.json
├── package.json
├── synpress.config.js
└── tsconfig.jsonInitial Setup
- Install dependency
npm i
2. Set up environment configuration:
cp .env.example .env
The file content should be like
NETWORK_NAME='Sepolia'
CHAIN_ID=11155111
RPC_URL='https://ethereum-sepolia-rpc.publicnode.com'
SYMBOL="ETH"
IS_TESTNET=true
# Add your wallet recovery phrase - ensure the wallet has at least 0.1 ETH on Sepolia
SECRET_WORDS="" # Generate using https://iancoleman.io/bip39/
To generate a seed phrase please use this online generator. All environment variables will be used in the upcoming sections.
Configuration SetupDApp URL Configuration
Update cypress.env.json with your testing parameters:
{
"ENV_NAME": "LOCAL",
"BASE_URL": "https://metamask.github.io/test-dapp/",
"WALLET_ADDRESS": "
"
}Cypress ConfigurationInside cypress.config.js We should have all configurations related to the auto-generated videos and reports.
The code below is standardized you will find it on the template and can use it with all e2e testing projects with slightly different changes.
The setupNodeEvents function is a crucial Cypress configuration component that handles event listeners, plugins, and test environment preparation. Let's break down its implementation step by step:
// Inside the cypress.config.js
async function setupNodeEvents(on, config) {
// Code goes here
};Environment Variable HandlingFirst, create a utility function to manage environment variables:
async function setupNodeEvents(on, config) {
const getEnvValue = (key) => {
if (process.env[key] !== undefined) {
return process.env[key];
} else {
return config.env[key];
}
};Base URL and Video ConfigurationSet up the test URL and configure video recording settings:
// After the getEnvValue function
const baseURL = getEnvValue("CYPRESS_BASE_URL") || config.env.BASE_URL;
config.env.BASE_URL = baseURL;
console.log(`The e2e tests will be executed at the URL: ${baseURL}`);
config.video = Boolean(getEnvValue("CYPRESS_VIDEO_ENABLED"));
config.videoCompression = Number(getEnvValue("CYPRESS_VIDEO_COMPRESSION"));
await addCucumberPreprocessorPlugin(on, config);
// The rest of the code...Browser Launch ConfigurationImplement browser-specific settings for test execution:
// After setting addCucumberPreprocessorPlugin
on("before:browser:launch", (browser = {}, launchOptions, config) => {
if (!browser.isHeadless) return;
// the browser width and height we want to get
// our screenshots and videos will be of that resolution
const width = 1920;
const height = 1080;
launchOptions.args.push("--lang=en-US");
launchOptions.args.push("--force-lang=en-US");
if (browser.name === "chrome" && browser.isHeadless) {
launchOptions.args.push(`--window-size=${width},${height}`);
// force screen to be non-retina and just use our given resolution
launchOptions.args.push("--force-device-scale-factor=1");
}
if (browser.family === "chromium" && browser.name !== "electron") {
// Assicurati di sostituire 'C:\\Path\\To\\Your\\Chrome\\Profile' con il percorso reale
launchOptions.args.push(
"--user-data-dir=C:\\Path\\To\\Your\\Chrome\\Profile"
);
launchOptions.args.push("--profile-directory=Cypress");
launchOptions.args.push("--lang=en-US");
launchOptions.args.push("--no-first-run");
launchOptions.args.push("--no-default-browser-check");
launchOptions.args.push("--disable-features=PromptOnMultipleDownload");
}
if (browser.name === "electron" && browser.isHeadless) {
// might not work on CI for some reason
launchOptions.preferences.width = width;
launchOptions.preferences.height = height;
}
if (browser.name === "firefox" && browser.isHeadless) {
launchOptions.args.push(`--width=${width}`);
launchOptions.args.push(`--height=${height}`);
}
// IMPORTANT: return the updated browser launch options
return launchOptions;
});
Plugin ConfigurationSet up necessary plugins and preprocessor:
synpressPlugins(on, config);
const esbuildPlugin = require("@badeball/cypress-cucumber-preprocessor/esbuild");
const createBundler = require("@bahmutov/cypress-esbuild-preprocessor");
on(
"file:preprocessor",
createBundler({
plugins: [esbuildPlugin.createEsbuildPlugin(config)],
})
);Fail-Fast ConfigurationImplement fail-fast functionality to optimize test execution:
on("task", {
failFastResetSkip() {
setTimeout(() => {
console.log("Timeout reached, reset complete.");
}, 10000);
console.log("Resetting fail-fast skip status");
return null;
},
});
return config;Cypress Configuration ExportFinally, export the complete configuration:
module.exports = defineConfig({
viewportWidth: 1920,
viewportHeight: 1080,
defaultCommandTimeout: 120000,
requestTimeout: 120000,
responseTimeout: 120000,
e2e: {
setupNodeEvents,
video: false,
videoCompression: 32,
videosFolder: "cypress/report/video",
screenshotsFolder: "cypress/report/screenshots",
specPattern: ["**/*.feature", "cypress/tests/**/*.cy.{js,jsx,ts,tsx}"],
scrollBehavior: "nearest",
supportFile: "cypress/support/e2e-synpress.js",
},
env: {
TAGS: "not @ignore",
BASE_URL: "https://metamask.github.io/test-dapp",
CYPRESS_VIDEO_ENABLED: true,
CYPRESS_VIDEO_COMPRESSION: 32,
},
browser: {
name: "chrome",
arguments: ["--lang=en-US"],
},
synpress: {
walletConnect: {
bridge: "https://bridge.walletconnect.org",
qrcodeModal: true,
},
},
});Writing Test Scenarios with CucumberCucumber test scenarios should be written collaboratively with product owners to ensure comprehensive test coverage. Here’s an example structure:
# cypress/tests/01-app-access.feature
Feature: The application works only with the Sepolia network
Scenario: The user accesses the page with Metamask connected to Sepolia network
Given A user with metamask installed connected to sepolia network
When the user accesses the app page
And the user accepts notifications
Then the page shows the account address
And the page shows the input address field
And the page doesn't show a network error messageUnderstanding Gherkin KeywordsGiven- Sets up initial context or preconditions
- Example:
Given('the user clicks on the "Use MetaMask" button', () => {
cy.contains("button", "Use MetaMask").click();
});When- Specifies user or system actions
- Example:
When("the user do a smart contract deployment", () => {
cy.get("#createToken").click();
cy.wait(10000);
cy.confirmMetamaskTransaction({
shouldWaitForPopupClosure: true,
gasConfig: {
gasLimit: 210000,
baseFee: 100,
priorityFee: 10,
},
})
.then((txData) => {
expect(txData.networkName).to.be.not.empty;
expect(txData.customNonce).to.be.not.empty;
expect(txData.confirmed).to.be.true;
})
.wait(15000);
});Then- Describes expected outcomes
- Example:
Then("the transaction should be confirmed successfully", () => {
cy.get("@txConfirmed").should("be.true");
});And- Chains multiple steps
- Example:
Scenario: Connect to dapp and perform various operations
Given A user with metamask installed connected to sepolia network
And the user accesses the app page
Then the user clicks on the "Use MetaMask" button
And the user connects to the dappComplete Example: MetaMask Interactionsimport { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor';
When('the user accesses the app page', () => {
cy.visit('/');
});
Then('the user clicks on the "Use MetaMask" button', () => {
cy.contains("button", "Use MetaMask").click();
});
When("the user connects to the dapp", () => {
cy.get("#connectButton").click();
cy.acceptMetamaskAccess();
});
Then("the user should be able to perform various Metamask operations", () => {
cy.get("#getAccounts").click();
cy.get("#getAccountsResult").should(
"have.text",
Cypress.env("WALLET_ADDRESS")
);
cy.get("#personalSign").click();
cy.confirmMetamaskSignatureRequest();
cy.get("#personalSignVerify").click();
cy.get("#personalSignVerifySigUtilResult").contains(
Cypress.env("WALLET_ADDRESS")
);
});⚠️ Things to take care of when using synpress- Need a fast internet connection or you will face timeout issues.
- If you are using a public test net, not a private Devnet remember to check the blockchain speed as they may cause timeout issues too.
- Synpress now have this issue when submitting a transaction, #140 to fix it downgrade less than version 3.7.2 or edit the custom nonce
For a complete implementation with additional test cases including smart contract deployment and token approvals, please refer to the repository
ReferencesSynpress Example
Synpress Documentation
The Full Guide For End To End Testing With Your Dapp With Synpress was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.