Testing Expo Web3 Apps With Wagmi and Anvil

In short

Learn how to configure and test your Expo web3 apps using Wagmi and Anvil, including how to point to a development blockchain instance during tests. Discover best practices on what to test, when to mock, and how to create reliable and independent tests.

Introduction

Probably you already know the benefits of testing your applications: how it can boost your team's confidence, help in refactoring, avoid regressions, make part of documentation and, generally, save you time in the long run.

With that in mind, this article will assume you already understand these advantages and will instead focus on how to effectively test web3 apps built with React Native, highlighting the similarities and differences compared to non-web3 apps.

If, instead, you want to know how to bootstrap and use Viem, Wagmi, WalletConnect Web3Modal, and more web3 libraries in React Native, check out these articles:

What we are testing

The example we'll use resembles a contacts list, where the user connects their wallet to see a list of contacts with their addresses. Pressing a contact brings up a bottom sheet, allowing the user to send a transaction with some ETH to the selected contact.

Though simple, this example covers enough real use cases to help you understand the best approach to test features like connecting a wallet, sending a transaction, and interacting with a blockchain instead of a server.

All our blockchain interactions will be done through Wagmi, the most popular React Hooks library for Ethereum. Wagmi is built on top of Viem, the one that actually implements the RPC calls, transactions, wallet connection, etc. If you want to jump straight to the code, it is available open-source on GitHub.

Here's what it looks like:

Quick demo of the example app
In this example we're using WalletConnect Web3Modal, which is the most popular “Connect Wallet Modal” option with good support for React Native. We're using an alpha version of Web3Modal with support for Wagmi v2. While things might break, we found it stable for the contents of this article.

Configuration

To configure the project for testing, refer to the Expo docs on Unit Testing, and the React Native Testing Library docs.

We will be running our tests against Anvil, a local blockchain server. This allows us to have realistic interactions with the blockchain, but without the cost of the network delay or paying gas fees. Anvil is part of Foundry, a smart contract development toolchain. Start by installing <rte-code>foundryup<rte-code>, the Foundry toolchain installer, and then running itself to install the Anvil binary.

Then, install the project testing dependencies:

In <rte-code>package.json<rte-code>, add the test scripts and Jest settings:

Here's a breakdown of what these settings are changing:

  • First, we added a test script to run the tests. Since we will run our tests against a local blockchain, we need to run it in parallel with the tests. We use <rte-code>concurrently<rte-code> to be able to run both <rte-code>anvil<rte-code> and <rte-code>jest<rte-code> at the same time.
  • In the Jest config, <rte-code>"preset": "jest-expo"<rte-code> will facilitate testing Expo apps by mocking common expo native modules as well as adding utilities for testing universal apps.
  • Also, <rte-code>"transformIgnorePatterns"<rte-code> is specifying which packages inside <rte-code>node_modules<rte-code> shall be transformed by Babel. Many React Native packages don't pre-compile their source code before publishing, so they need to be transformed by Babel before being interpreted by Jest. The default list is provided by Expo as a starter, and should cover the majority of packages in the ecosystem. However, note that we also appended <rte-code>@walletconnect/react-native-compat<rte-code>, <rte-code>wagmi<rte-code> and <rte-code>@wagmi/.*<rte-code> at the end, which will cover these web3 libraries specific to our project.
  • Finally, <rte-code>"setupFilesAfterEnv"<rte-code> is specifying a script to configure the testing framework and will be run before each test. We will create this file next.

Now, create the <rte-code>setup-jest.ts<rte-code> file, in the project root:

This file will be run before all tests, so it's a good place to prepare global settings. We will import the React Native specific Jest Matchers, which will allow us to use matchers that wouldn't be present in <rte-code>jest-dom<rte-code>, such as <rte-code>.toBeOnTheScreen()<rte-code>, and others that behave differently on mobile, such as <rte-code>toBeVisible()<rte-code>.

It's also a good place to mock native modules that won't work outside the native environment. Let's put those mocks in a separate file, <rte-code>src/test/mocks.tsx<rte-code> to keep things organized since we will be consuming those from other places later.

If you're following on your own project, you might not need some of the mocks being used here, such as <rte-code>@gorhom/bottom-sheet<rte-code>, and instead need to mock other of your dependencies.

Remember, though: mocking replaces the code you use in production with something else during tests. While it's useful to mock parts that wouldn't work in the test environment (such as native modules that depend on the device OS), you should strive to mock as little as possible. The less your testing code resembles your production code, the less confidence you can have that it will actually catch bugs a real user might encounter.

Done with the configuration, let's get to actually writing tests.

Connecting to the local blockchain

The primary difference when running tests for our web3 application is that we need to direct all our actions to our local blockchain instance, rather than interacting with deployed blockchains. This is necessary because even testnets like Sepolia require gas fees and involve waiting for network calls.

We need our tests to be fast, reliable and reproducible. Using a local running blockchain such as Anvil we skip the network call (as it is running in localhost) and it provides us with several persistent accounts (which don't change between Anvil runs) with huge amount of tokens that can be used to test transactions. Moreover, we have the ability to reset the blockchain state between each test to make sure they're independent of each other.

Using Wagmi, to change the network we're pointing to, it's just a matter of changing the <rte-code>wagmiConfig<rte-code> passed to the provider.

If you look at our application's Wagmi setup in <rte-code>App.tsx<rte-code>, you’ll notice that we create it using WalletConnect's Web3Modal util functions. Web3Modal provides this to facilitate integrating with their components.

But during tests, what we need to do is recreate this config, using the standard Wagmi configuration, and choose the <rte-code>foundry<rte-code> chain, which will automatically point to the Anvil instance running in localhost.

Let's do this in a new file, <rte-code>src/test/config.tsx<rte-code>:

Here, notice a few things:

  • At the beginning we define a <rte-code>chains<rte-code> constant, and put the <rte-code>foundry<rte-code> chain inside, which will point to Anvil.
  • Then we define a <rte-code>TEST_ACCOUNTS<rte-code> array to hold the static addresses and private keys we will use for testing. These accounts are automatically funded high amounts of ETH when Anvil starts.
  • Then we define the <rte-code>testClient<rte-code>, a Viem client to communicate with the blockchain:
    1. Pass the chain defined earlier.
    2. Pass the account and private key defined earlier, so we can sign for transactions.
    3. Use a low <rte-code>pollingInterval<rte-code> so changes in the blockchain are reflected faster on the client, speeding up tests.
    4. Extend the <rte-code>testClient<rte-code> with public and wallet actions, so it can also act as a <rte-code>publicClient<rte-code> (query info from the blockchain) and <rte-code>walletClient<rte-code> (transact, sign messages, and do other wallet actions).
  • Finally, we define a <rte-code>renderWithProviders()<rte-code> function, that will be used during the actual tests. This function creates a test Wagmi config using everything we defined above, and passes it to the actual component tree, mimicking the same tree we have in the application code, but for tests.

Writing tests

When writing tests, we need to put ourselves in the users shoes. What actions can they take in our system? We need to be able to describe it non-technically, as if we were describing our favorite cake recipe to someone.

Let's go over the main features of the example app, writing the tests in a new file, <rte-code>src/features/home/HomeScreen.test.tsx<rte-code>.

Test: User is disconnected

When the user hasn't connected a wallet yet, we expect him to:

  • See a “Connect Wallet” button.
  • See a message indicating that it needs to connect to view the contacts.

Always start by rendering the app. Usually, you should use the <rte-code>render<rte-code> function from React Native Testing Library, but in our case we created the wrapper <rte-code>renderWithProviders<rte-code>, which will call <rte-code>render<rte-code> internally, wrapping it with the necessary providers for our specific needs.

To get the connect button, use <rte-code>screen.getByRole<rte-code>, which will try to find elements in the render output with the <rte-code>"button"<rte-code> accessibility role and <rte-code>"Connect Wallet"<rte-code> text.

To get the message, use <rte-code>screen.getByText<rte-code>, which will try to find an element with that written text in the render output.

Finally, <rte-code>expect<rte-code> all elements <rte-code>toBeOnTheScreen()<rte-code>.

Going into detail on all React Native Testing Library APIs would require a separate article. If you're not familiar with these, please check out the API docs.

Test: User connects the wallet

When the user connects the wallet, we now expect that a list should appear, showing contact names.

In this example, we're not only querying elements on the screen but also interacting with them using <rte-code>user.press()<rte-code>. This is a React Native Testing Library API called User Events, which simulates realistic user events such as press, long press, type, and scroll.

To be able to use User Events, a one-liner setup is needed. Place it at the top of the test file, outside all tests:

Test: Transaction succeeds

Sending a transaction is the critical feature of our example, so this might be the most important test.

When the user wants to send a transaction, a couple of things must happen:

  • They need to connect the wallet
  • They need to press a contact
  • They need to see the transaction details
  • They need to press the confirm button
  • They expect some kind of loading indicator to appear
  • They expect some kind of success indicator to appear

So let's put those assumptions in our tests:

Notice how every test assumption is based on elements and actions the user can actually see. We're not checking for functions being called or considering that any piece of code must be called with param X, Y or Z. To better understand why testing implementation details is bad, head over to the article by Kent C. Dodds.

Also, notice that in this test we're altering the state of the blockchain as we're settling a transaction and moving ETH from one account to another.

To make sure that tests stay independent of each other, that is, one test does not depend on the result of the previous one, we should set up a mechanism to reset the blockchain state to the same initial state after each test, so all of them rely on the same pristine state.

This is trivial using Jest's <rte-code>beforeAll()<rte-code> and <rte-code>afterEach()<rte-code> hooks, and using the <rte-code>testClient<rte-code> we defined before to manually dump and load the blockchain state:

Test: transaction fails

When testing for a failed transaction, the flow is pretty much the same - except at the end a failure toast should appear instead of a success toast.

To simulate a transaction failing at the blockchain level, such as a transaction being reverted, we can mock Wagmi's <rte-code>useWaitForTransaction<rte-code> hook, which watches the submitted transaction until it completes or fails.

In this case, it's as simple as returning a loading state and after some time mutating it to an error state. Let's create this in the same <rte-code>src/test/mocks.tsx<rte-code> file we used before:

Then, we just need to tell Jest to mock this hook specifically for this test so it doesn't affect others. We can do this by using Jest spies:

Remember to call <rte-code>spy.mockRestore()<rte-code> at the end of the test so the mock is not kept active and interferes with the next tests!

You may also have noticed some repeating patterns at this point. We always need to connect the wallet, and also most of times need to find and press a contact in the list. To avoid repeating the same lines of code all the time, we can extract those in util functions, in a new file called <rte-code>src/test/utils.tsx<rte-code>:

And then use it in all our tests:

Test: Insufficient balance

When a user has insufficient balance to send a transaction, the button to confirm must be disabled from the start.

To test this case, instead of mocking another Wagmi hook to artificially return a zero balance, wouldn't it be better if we could just tell our blockchain to set an account's balance to zero? This way we don't need to mock and diverge from our application code.

That is exactly why <rte-code>testClient.setBalance()<rte-code> exists. This Viem utility will send an RPC call to our local blockchain setting the balance of an account to any amount we want. Of course, this doesn't exist in a real blockchain — it's a feature from Anvil and other development blockchains.

Viem helps us to use this and other development features through Test Actions, like <rte-code>setBalance<rte-code>. Read more in the setBalance docs.

Test: Confirm button should disable while loading

Our last test will be related to testing elements while they are in the loading state. In our example, the confirm button should also be disabled while the transaction is being processed on the blockchain side.

Usually, this is hard to test because we depend on an external system (the blockchain). Luckily, another of Anvil's testing features allows us to programatically stop mining new blocks. That way, the transaction will never be confirmed and our app will be frozen in a loading state — perfect situation to check our elements.

The following must happen:

  • Disable automining new blocks
  • Send the transaction (will be stuck in a loading state)
  • Check if the loading state is correct
  • Enable mining blocks again (the transaction will confirm)
  • Expect success

This feature is also accessible through a Viem Test Action. Read more in the setAutomine docs.

Summary

By this point, you may have noticed that testing a web3 app is mostly like testing all other apps. All testing good practices still apply, such as not testing implementation details and avoid mocking as much as possible.

What differs is that the external system we depend on is the blockchain — and we use a local development instance to simulate our transactions.

In this article, we've covered how to setup your Expo React Native web3 app for testing with popular tools like Anvil, Wagmi, Viem, and Web3Modal. And we've also talked about a few tricks to keep in your toolbelt:

  • Creating a test config and pointing to a development blockchain during tests
  • What to test, and how to avoid testing implementation details
  • Using <rte-code>beforeAll<rte-code> and <rte-code>afterEach<rte-code> to reset the blockchain state and achieve isolated tests
  • Manually changing the blockchain state with Test Actions for more realistic tests
  • Extracting repeated tests into util functions

The final code is available open-source on GitHub.

FAQ

No items found.
React Galaxy City
Get our newsletter

By subscribing to the newsletter, you give us consent to use your email address to deliver curated content. We will process your email address until you unsubscribe or otherwise object to the processing of your personal data for marketing purposes. You can unsubscribe or exercise other privacy rights at any time. For details, visit our Privacy Policy.

Callstack astronaut
Download our ebook

I agree to receive electronic communications By checking any of the boxes, you give us consent to use your email address for our direct marketing purposes, including the latest tech & biz updates. We will process your email address and names (if you have entered them into the above form) until you withdraw your consent to the processing of your names, or unsubscribe, or otherwise object to the processing of your personal data for marketing purposes. You can unsubscribe or exercise other privacy rights at any time. For details, visit our Privacy Policy.

By pressing the “Download” button, you give us consent to use your email address to send you a copy of the Ultimate Guide to React Native Optimization.