[Spike] Testing / release validation
Overview
Currently our process for validating changes to the code base heavily relies on manual testing against production blockchains. This is slow, error prone, and can be expensive.
testnet solutions don't fully resolve any of these pain points due to the cross chain nature (EVM <> BTC) of some of our most critical products.
Additionally the number or protocols x number of wallets x number of chains will continue to grow simply making the problem worse.
We should evaluate if other solutions or architectures might prove effective to better validate our code or the ability to implement more automated testing. Another option might be to establish more formal and documented test plans that engineering sticks to when validating changes.
References and additional details
Lets timebox this to 2 days of effort for the moment, if we need to invest more time we can but lets a tleast check in at that point.
Acceptance Criteria
possible sources of information:
- review other similiar open source react apps to see how they are handling the same problem
- do some reading (blog posts, etc)
at the end, generally summarize findings and proposed next steps either in this ticket or another doc.
Need By Date
No response
Screenshots/Mockups
No response
Estimated effort
2
Summary of findings
Similar dApps
| App | Unit tests | Integration tests | Blockchain simulation | |
|---|---|---|---|---|
| 1 | Uniswap | Jest | Cypress | Hardhat |
| 2 | Pancake | Jest | Cypress | None |
| 3 | Jumper | None | None | None |
| 4 | Spectrum | None | None | None |
| 5 | Bancor | vitest | Playwrite | Tenderly |
Learnings
I've reviewed a number of open source DEX repositories that have similar functionality to us, namely those that include a swapper.
Uniswap and Pancake used Cypress for their end to end integration tests. A standout was Uniswap, which seems the most extensive, as it includes the ability to simulate and mock on-chain behaviour via Hardhat.
Additionally, Uniswap:
- Configure Hardhat to use both Ethereum Mainnet and Polygon chains
- Uses Infura endpoints with private API keys to run tests
- Store mock user portfolios as JSON in a format that match their data structure, which are imported as Fixtures into Cypress
- Assert that expected wallet methods (e.g.
eth_sendRawTransaction) are called at the expected times - Use the package
cypress-hardhat, which they also maintain, to make Cypress play with Hardhat
Recommendation / possible paths forward
Context
We have had a Cypress implementation before, though it never got past the PoC phase. For this reason, the maintenance burden and CI cost made it difficult to justify keeping.
It did basic checks, including:
- Does each page load?
- Does the trade widget load, and can I interact with it (input only, no mock trades could be performed)
- Can I load a wallet and view my balance?
It did not do any checks surrounding wallet methods or on-chain activity.
Re-add Cypress
Implement Cypress for end-to-end integration testing, configured to use Hardhat forks across a 2-3 chains.
For this to be useful and worth doing we'll want to ensure that we include sufficient test cases for our revenue-driving feature-set. For example, the below checks should be feasible with this architecture:
Swapper
- Check that we can get trade quotes by inputting values, changing assets, switch to fiat etc. We'd ensure that we requested a quote from the expected upstream swap provider, and mock the response.
- Check that we can proceed through the approval and trade signing steps. Ensure the expected wallet methods are called, and use the Hardhat fork to confirm that the transaction is executed on chain.
LP
- Confirm we can interact with the LP input component (input values, change assets, switch to fiat)
- Confirm we can proceed through the Add and Remove flows (likely EVM only if we are only using Hardhat to mock wallet state), and that the appropriate wallet methods are called. We'll also be able to check EVM transactions against Hardhat.
- Confirm balance checks working (Hardhat EVM)
Sends
- Check that the end-to-end send flow works - the correct wallet methods are called, the transaction is executed on the Hardhat fork, and the balances are updated as expected, as that we show the updated balance to the user.
- Confirm balance checks working (Hardhat EVM)
Savers
- Confirm balance checks working (Hardhat EVM)
- Confirm the end-to-end Deposit and Withdraw flows work - check wallet methods are called and an EVM transaction is executed on the Hardhat fork.
Performance (network requests)
Use integration tests to check for network performance regressions (an increase in the number of calls to expensive endpoints caused by unoptimised reactivity, for example)
Technical considerations
- So that the Cypress CI run is not prohibitively resource intensive we'd need to ensure network calls reduced as much as possible.
- Previously we used an actual wallet seed to get asset balances, but if we are using Hardhat this time we should be able to set up an account with balances on a fork before the tests are run.
- With Hardhat we'll be limited to EVM functionality (no UTXOs)
- Using the Native wallet for end-to-end testing is a somewhat solved problem - we can import the appropriate seed for the desired account on the Hardhat fork. Using other wallets is less straightforward/known, and is likely out of scope.
- We'll only be able to test that transactions sign and are executed (via monitoring the wallet methods called, and checking the Hardhat fork state). We won't be be to test cross-chain functionality that relies on third parties, such as THORChain and LiFi.
- We'll need to mock responses from third parties so that our tests are deterministic. This does mean we'll miss things such as breaking API changes upstream, or these services being down (but that's likely ok/desired for a set of CI tests).
Additional information and best practices
Excerpt from Uniswap README:
Our tests use a local hardhat node to simulate blockchain transactions. This can be accessed with `cy.hardhat().then((hardhat) => ...)`.
By default, automining is turned on, so that any transaction that you send to the blockchain is mined immediately. If you want to assert on intermediate states (between sending a transaction and mining it), you can turn off automining: `cy.hardhat({ automine: false })`.
The hardhat integration has built-in utilities to let you modify and assert on balances, approvals, and permits, and should be fully typed. Check it out at [Uniswap/cypress-hardhat](https://github.com/Uniswap/cypress-hardhat).
Asserting on wallet methods:
// Asserts that `eth_sendRawTransaction` was sent to the wallet.
cy.wait('@eth_sendRawTransaction')
Sometimes, you may want a method to fail. In this case, you can stub it, but you should disable logging to avoid spamming the test:
// Stub calls to eth_signTypedData_v4 and fail them
cy.hardhat().then((hardhat) => {
// Note the closure to keep signTypedDataStub in scope. Using closures instead of variables (eg let) helps prevent misuse of chaining.
const signTypedDataStub = cy.stub(hardhat.provider, 'send').log(false)
signTypedDataStub.withArgs('eth_signTypedData_v4).rejects(USER_REJECTION)
signTypedDataStub.callThrough() // allws other methods to call through to hardhat
cy.contains('Confirm swap').click()
// Verify the call occured
// Note the call to cy.wrap to correctly queue the chained command. Without this, the test would occur before the stub is called.
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
// Restore the stub
// note the call to cy.then to correctly queue the chained command. Without this, the stub would be restored immediately.
cy.then(() => permitApprovalStub.restore())
})