Sitemap
Coinmonks

Coinmonks is a non-profit Crypto Educational Publication. Other Project — https://coincodecap.com/ & Email — gaurav@coincodecap.com

Guideline to be QA Web3: Complete E2E DeFi Project with Synpress (Playwright) and Hardhat/Anvil

13 min readMar 5, 2025

--

In my previous blog, QA Blockchain Testing: Smart Contract & Network Performance with Hardhat, we explored the fundamentals of node testing for smart contracts. Today, we’re taking the next step by diving into comprehensive testing for Web3 decentralized applications (DApps).

Blockchain testing presents unique challenges for QA professionals. Many struggle with three key obstacles: limited practical experience with blockchain technology and business logic, a scarcity of specialized testing tools, and difficulty designing effective test cases for immutable, public transactions that directly involve financial assets.

Press enter or click to view image in full size

To address these challenges, I’ve created a comprehensive learning project that allows QA testers to practice end-to-end testing on their own localhost environment. This approach eliminates the restrictions of public testnets, where free tokens are often limited by daily faucet allowances.

I’ll share the complete GitHub repository at the end of this blog as a resource for the QA community to enhance their blockchain testing skills.

Table of Contents:

Essential Knowledge for Web3 QA

1. Blockchain Fundamentals

Core Concepts:

  • Transactions and Gas: Understand how transactions work, gas fees, and transaction finality
  • Wallets and Accounts: Know how private/public keys work and how wallets connect to dApps
  • Smart Contracts: Understand basic smart contract functionality and limitations
  • Consensus Mechanisms: Basic understanding of Proof of Work vs. Proof of Stake
  • Networks and Forks: Differentiate between mainnet, testnet, and local development networks

Practical Knowledge:

  • Set up MetaMask and other popular wallets
  • Experience making transactions on testnets
  • Use block explorers (Etherscan, Arbiscan, etc.) to verify transactions

2. Web3 Development Stack

Familiarity with:

  • Frontend Libraries: React, ethers.js, web3.js, wagmi
  • Development Frameworks: Hardhat, Foundry, Truffle, Brownie
  • Testing Frameworks: Playwright, Cypress, Selenium with MetaMask extensions
  • Local Blockchain Environments: Anvil, Hardhat Network, Ganache

3. DeFi and Web3 Application Patterns

Common Patterns:

  • Connect Wallet Flow: Standard wallet connection patterns
  • Transaction Approval Flow: Understanding approvals vs. transactions
  • Asynchronous State Updates: How blockchain confirmations affect UI updates
  • Network Switching: How applications handle network changes

Understanding DeFi Lending Protocols

Lending protocols in smart contracts represent decentralized financial systems that eliminate the need for traditional banks as intermediaries. These protocols operate transparently with clearly defined rules governing borrowing, interest rates, and liquidation processes.

To understand how these protocols work, it’s important to familiarize yourself with these key concepts:

  1. Collateral: Assets deposited by borrowers as security for their loans. This collateral serves as insurance for the protocol, demonstrating that borrowers have sufficient assets to back their borrowed funds.
  2. Health Factor: A numerical representation of a borrower’s position safety (risk level). If the collateral value decreases significantly or interest accumulates substantially, the health factor drops, potentially leading to liquidation.
  3. Liquidation: The process triggered when a borrower’s health factor falls below a critical threshold. In this state, the borrower’s collateral can be forcibly sold at a discount (liquidation bonus) to liquidators who help repay the outstanding debt.
  4. Interest Rate: The cost of borrowing assets over time. Borrowers pay interest when they hold borrowed assets for extended periods, while lenders earn this interest as compensation for providing liquidity to the protocol.
Press enter or click to view image in full size

The liquidator sees the liquidation status of the borrower and pays part of her debt. As a result, the borrower’s health factor decreases from 0.59 to 0.47, making it easier for them to repay the remaining debt later.

Press enter or click to view image in full size
Press enter or click to view image in full size

How to Test End-to-End DeFi Applications

End-to-End testing simulates real user behavior by testing the complete flow of an application from start to finish. In the context of DeFi applications, E2E testing means:

  1. Starting with a wallet connection — just like a real user would
  2. Performing complete financial transactions — deposits, borrows, withdraws, etc.
  3. Verifying results across all layers — UI changes, blockchain state, database records
  4. Testing entire user journeys — not just individual functions

The diagram below illustrates the complex interactions in a DeFi lending protocol during deposit and borrow flows. Understanding these interactions is crucial for designing comprehensive test cases:

Borrower flow:

Press enter or click to view image in full size

Liquidator flow:

Press enter or click to view image in full size

Core Lending Protocol Functions

  • Deposit: Supply collateral to the protocol
  • Borrow: Take out a loan against your collateral
  • Repay: Pay back borrowed assets plus interest
  • Withdraw: Remove your collateral from the protocol
  • Liquidate: Repay part of an unhealthy loan in exchange for a discount on the collateral

As you can see, a simple user action like depositing ETH triggers multiple interactions:

  • Price checks with an oracle service
  • Token conversions (ETH to WETH)
  • Smart contract state updates
  • User position tracking
  • Activity logging in the database

Our E2E tests must verify that each of these steps completes successfully, just as they would in a production environment.

Synpress and MetaMask Integration with Playwright

You can use the Synpress framework, an end-to-end (E2E) testing framework designed for decentralized applications (DApps). Synpress supports both Playwright and Cypress as plugins and provides built-in features for handling MetaMask authentication, transactions, and wallet interactions.

Install the Synpress framework command:

npm install --save-dev @synthetixio/synpress @synthetixio/synpress-tsconfig

Install Playwright framework command:

npm install --save-dev @playwright/test

Setup metamask wallet:

For manual UI testing, you can copy the private key from the Hardhat local network while running npx hardhat node and import the account into MetaMask like this:

Press enter or click to view image in full size

Integrating Playwright with MetaMask:

First, we initialize our test framework by combining Playwright and Synpress:

// tests/e2e/lending_test.spec.ts
import { testWithSynpress } from '@synthetixio/synpress';
import { MetaMask, metaMaskFixtures } from '@synthetixio/synpress/playwright';
import basicSetup from '../../wallet-setup/metamask.setup';

// Initialize test with Synpress and MetaMask fixtures
const test = testWithSynpress(metaMaskFixtures(basicSetup));
let metamask: MetaMask;
const { expect } = test;

test.describe('Lending Protocol Tests', () => {
// Test suite content will go here
});

Here is how you can setup a MetaMask wallet with Synpress:

The basicSetup file configures the MetaMask wallet that will be used in tests:

Press enter or click to view image in full size

During wallet setup with Synpress, it will create a new wallet for you. If you’re using a Hardhat local network, you can simply use MetaMask’s default seed phrase and any password for the wallet setup.

The Synpress framework will look for a specific filename and folder for the wallet setup. You should name the file metamask.setup.ts, place it in the wallet-setup folder, and then run this command:

npx synpress wallet-setup

In the video below, you will see that it opens a browser to create a MetaMask wallet and caches the wallet setup in the .cache-synpress folder.

Press enter or click to view image in full size

Test Setup with MetaMask Initialization:

In our test file, we initialize MetaMask before each test:

Press enter or click to view image in full size

Page Object With MetaMask Integration:

Our page objects accept the MetaMask controller to interact with wallet popups:

// pages/lending.page.ts
import { type Page, type Locator, expect } from "@playwright/test";
import { MetaMask } from "@synthetixio/synpress/playwright";
import { TestData } from '../config/test-data.config';
import BasePage from "./base.page";

export default class LendingPage extends BasePage {
readonly connectWalletButton: Locator;
readonly depositButton: Locator;
readonly liquidationSuccessMessage: Locator;
// Other locators...

constructor(page: Page, metamask: MetaMask) {
super(page, metamask);

// Initialize locators
this.connectWalletButton = page.getByRole('button', { name: /Connect Wallet/i });
this.depositButton = page.getByTestId('deposit-button');
this.liquidationSuccessMessage = page.getByTestId('success-message');
// Other locator initializations...
}

// Connect wallet with MetaMask
async connectWallet(): Promise<void> {
await this.connectWalletButton.click();
await this.metamask.connectToDapp();
await this.waitForTimeout(2000);
}

/**
* Confirm the liquidation transaction in MetaMask
*/
async confirmLiquidation(): Promise<void> {
await this.metamask.confirmTransaction();
await this.waitForTimeout(TestData.TIMEOUTS.LONG);
}

/**
* Verify liquidation success message
*/
async verifyLiquidationSuccess(): Promise<void> {
await expect(this.liquidationSuccessMessage).toBeVisible();
}

// Other methods...
}

After you have the wallet cache folder, you can simply run the Playwright command for your test case.

npx playwright test

In the video below, you can see that Synpress helps manage the wallet plugin on the Chrome extension, simulating real user activities on the UI. You can create a script to automate confirming transactions on the MetaMask popup as desired.

Press enter or click to view image in full size

Playwright report result:

Press enter or click to view image in full size

Tips: Screenshot result report in Playwright

You may already know that to capture test results in Playwright, you need to use await page.screenshot(). However, whether you want to record a screenshot or a video, Playwright records per context.

For example, during test execution, Playwright opens the Chrome extension (MetaMask) in the first tab and your DApp in the second tab. However, in our test reports, what we actually want to capture is the MetaMask popup showing the transaction details and gas fees.

How to Capture Both Main Page and Popup in Playwright?

I created a custom function to capture screenshots of both the main page and the popup page.

  • Playwright can detect a popup session using context.waitForEvent('page')
  • After capturing both images, I merge them into a single picture using the Sharp library.

You can see the implementation in the code below.

Press enter or click to view image in full size

Final Step:

Simply call the screenshot function and use testInfo.attach() to add it to the HTML report.

Press enter or click to view image in full size
Press enter or click to view image in full size

Understand MetaMask functions in Synpress:

metamask.connectToDapp()
Press enter or click to view image in full size
Press enter or click to view image in full size

When we use metamask.connectToDapp(), it automatically selects the default account in our MetaMask wallet and connects it to the DApp when we click the Connect button in the UI.

metamask.switchAccount('Account 2')
Press enter or click to view image in full size

This function switches the active account in MetaMask by specifying the account name. However, simply switching accounts in MetaMask does not automatically update the connected account in the DApp. The DApp must listen for MetaMask’s account change events to detect and handle the switch properly.

When clicking the “Connect Wallet” button, I check if an account is already connected. If it is, I first disconnect it and then reconnect.

Press enter or click to view image in full size

During automation, after switching accounts, MetaMask will automatically select Account 2, as shown below.

Press enter or click to view image in full size

Similarly, when switching networks, the frontend must set up an event listener for MetaMask events, just like when switching accounts.

this code is part of frontend/utils/web3.ts in repo
metamask.switchNetwork(Local Optimism, true);
Press enter or click to view image in full size
metamask.confirmTransaction()
Press enter or click to view image in full size

This function is used when interacting with a DApp and performing a transaction that modifies the blockchain state (e.g., transferring tokens, updating balances, or writing data to storage). When this happens, MetaMask will require the user to confirm the transaction before it is executed.

Using Anvil for Network Simulation

Anvil is a local Ethereum development environment that’s part of the Foundry suite of blockchain development tools. It’s similar to Hardhat’s network but with some key differences that make it particularly valuable for our testing scenario.

Anvil provides:

  • A customizable local Ethereum node
  • Precise control over chain ID, gas parameters, and block time
  • The ability to run multiple independent networks simultaneously
  • Advanced forking capabilities

Setting Up Multiple Networks:

// Start L1 Anvil node with realistic gas prices
const l1Node = spawn('anvil', [
'--chain-id', '31337',
'--base-fee', gasPrices.l1.baseFee.toString(),
'--host', '0.0.0.0',
'--port', '8545',
'--block-time', '12', // 12-second block times for Ethereum
'--fork-url', `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`,
mainnetBlockNumber ? `--fork-block-number=${mainnetBlockNumber}` : ''
]);

// Start L2 Anvil node with lower gas prices
const l2Node = spawn('anvil', [
'--chain-id', '420',
'--base-fee', forcedL2BaseFee.toString(),
'--host', '0.0.0.0',
'--port', '8546',
'--block-time', '2', // 2-second block times for Optimism
'--fork-url', `https://opt-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`,
optimismBlockNumber ? `--fork-block-number=${optimismBlockNumber}` : ''
]);

Real-time Gas Updates:

Press enter or click to view image in full size

This allows us to have:

  1. Two separate networks running concurrently on localhost
  2. Different chain IDs for L1 (31337) and L2 (420)
  3. Different gas pricing to simulate L1/L2 cost differences
  4. Independent state for each network

Time Manipulation

Our tests rely heavily on time manipulation to test interest accrual and liquidations.

Press enter or click to view image in full size

Look at this function that checks interest value by advance time on blockchain:

Press enter or click to view image in full size

Note on Local Testing Environment:

The lending smart contract calculates interest at 5% per year, which requires time progression to test properly. Unlike real networks, Hardhat’s local blockchain doesn’t automatically advance block timestamps. To simulate the passage of time for interest accumulation, we need to manually advance the blockchain.

Press enter or click to view image in full size

Similarly, health factor calculations depend on collateral price fluctuations. In a local environment, we need to implement a mock price oracle that can either:

  1. Fetch real prices from external APIs like CoinMarketCap (useful for realistic scenarios)
  2. Allow manual price adjustments (essential for testing edge cases like liquidation events)

These two simulation techniques enable comprehensive testing of all protocol states, including the critical liquidation process when a user’s health factor falls below the threshold.

Testing Across Layer 1 and Layer 2 Networks

Our framework tests the same protocol deployed on different network types:

Network Differences:

  • L1 (Ethereum): Higher gas costs, slower block times
  • L2 (Optimism): Lower gas costs, faster block times, different transaction format

Cross-Network Test Scenarios

We’ve implemented tests that verify:

  1. Network isolation: Positions on one network don’t affect the other
  2. Gas cost comparison: Compare transaction costs between L1 and L2
  3. UI consistency: Verify the UI handles both networks correctly

Here’s a test step that verifies network isolation and gas costs:

Press enter or click to view image in full size

During transactions on the DApp, we record the event type along with all relevant blockchain data, such as the chain ID, account, and gas cost, into the data column below:

Press enter or click to view image in full size

Network Layer 2 Data:

{
"user": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"token": "0x1343248Cbd4e291C6979e70a138f4c774e902561",
"amount": "1",
"chainId": 420,
"gasMetrics": {
"baseFee": "9826117",
"gasUsed": "79381",
"gasPrice": "1019652234",
"blockTime": "2025-03-04T19:57:11.000Z",
"totalGasCost": "80941013987154"
}
}

Network Layer 1 Data:

{
"user": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"token": "0xF01f4567586c3A707EBEC87651320b2dd9F4A287",
"amount": "1",
"chainId": 31337,
"gasMetrics": {
"baseFee": "982611638",
"gasUsed": "79381",
"gasPrice": "2965223276",
"blockTime": "2025-03-04T20:03:34.000Z",
"totalGasCost": "235382388872156"
}
}

As seen in the data, Network Layer 2 has a lower gas cost compared to Network Layer 1. The total gas cost on Layer 2 is 80,941,013,987,154, whereas on Layer 1, it is 235,382,388,872,156.

This means Layer 2 saves approximately 154,441,374,885,002 in gas costs, making it significantly more cost-efficient.

Why Test Across Networks?

While many users might interact with DeFi protocols on a single network, there are compelling reasons why testing across multiple networks is essential:

Real-World Cross-Network Usage:

Many DeFi applications today actually operate across multiple networks:

  1. Bridge Protocols: Applications like Stargate, Hop Protocol, and Across connect assets between networks.
  2. Multi-Chain Deployments: Major DeFi protocols (Aave, Uniswap, etc.) deploy on multiple networks, and users often move between them based on gas costs.
  3. L2 Migration Strategies: Users frequently deposit on L1 and then migrate to L2 for cheaper transactions as protocols expand.
  4. Cross-Chain Collateralization: Some advanced DeFi applications allow users to use collateral on one network to borrow on another.

Besides gas tracking in database testing from different network, as shown in the example above, we can also track transaction success rates and failures to analyze user behavior. This helps in conducting financial correctness tests and improving error handling.

For example, if a user attempts to borrow 0.8 ETH (which exceeds the 75% collateral factor):

  • Verify that an error is logged in the database.
  • Check that the user’s position remains unchanged.
Press enter or click to view image in full size
Press enter or click to view image in full size

Ready to Try It Yourself?

Now that you’ve seen the flow, set up the project, and explored how to interact with MetaMask, it’s time to get hands-on! This repo includes an E2E liquidation flow as an example, but feel free to extend it, experiment, and design your own test cases.

🔗 Check out the repo and start testing: [Repo Link]

Whether you’re refining automation skills or exploring DeFi testing, this is your sandbox — have fun and break things (safely)! 🚀

Thanks for reading, and I hope you found this article helpful.
If you found this article helpful, give it a 👏 (or a few!) to help others discover it!

--

--

Coinmonks
Coinmonks

Coinmonks is a non-profit Crypto Educational Publication. Other Project — https://coincodecap.com/ & Email — gaurav@coincodecap.com

Ploy Thanasornsawan
Ploy Thanasornsawan

Sharing knowledge about security and automation techniques.

No responses yet