Testing Expo Web3 Apps With Wagmi and Anvil

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 foundryup, the Foundry toolchain installer, and then running itself to install the Anvil binary.

# Install foundryup, the toolchain installer
curl -L https://foundry.paradigm.xyz | bash
# Running `foundryup` is what actually installs the `anvil` binary
foundryup

Then, install the project testing dependencies:

# Install Jest and the jest-expo preset
npx expo install -- --save-dev jest-expo jest
# Install React Native Testing Library, Jest types and concurrently
npm install --save-dev @testing-library/react-native @types/jest concurrently

In package.json, add the test scripts and Jest settings:

{
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
// Add test scripts
"test": "concurrently --names 'ANVIL,JEST' -c 'auto' 'anvil' 'jest'",
"test:watch": "concurrently --names 'ANVIL,JEST' -c 'auto' 'anvil' 'jest --watchAll'"
},
"dependencies": {
...
}
"devDependencies": {
...
},
"jest": {
// Add the jest-expo preset
"preset": "jest-expo",
// Specify which files shall be transformed by Babel
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|@walletconnect/react-native-compat|wagmi|@wagmi/.*)"
],
// Call a script to configure the testing framework before each test
"setupFilesAfterEnv": [
"<rootDir>/setup-jest.ts"
]
}
}
view raw package.json hosted with ❤ by GitHub

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 concurrently to be able to run both anvil and jest at the same time.
  • In the Jest config, "preset": "jest-expo" will facilitate testing Expo apps by mocking common expo native modules as well as adding utilities for testing universal apps.
  • Also, "transformIgnorePatterns" specifies which packages inside node_modules 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 @walletconnect/react-native-compat, wagmi and @wagmi/.* at the end, which will cover these web3 libraries specific to our project.
  • Finally, "setupFilesAfterEnv" specifies a script to configure the testing framework and will be run before each test. We will create this file next.

Now, create the setup-jest.ts file, in the project root:

// React Native specific Jest Matchers
import "@testing-library/react-native/extend-expect";
// Project mocks
import "./src/test/mocks";
view raw setup-jest.ts hosted with ❤ by GitHub

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 jest-dom, such as .toBeOnTheScreen(), and others that behave differently on mobile, such as toBeVisible().

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, src/test/mocks.tsx to keep things organized since we will be consuming those from other places later.

import { View as MockView } from "react-native";
// Mock React Native Gesture Handler
import "react-native-gesture-handler/jestSetup.js";
// Mock @gorhom/bottom-sheet
jest.mock("@gorhom/bottom-sheet", () => require("@gorhom/bottom-sheet/mock"));
// Mock React Native Async Storage
jest.mock("@react-native-async-storage/async-storage", () => {
return {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
};
});
// This mock doesn't actually change any implementation, it's just necessary to be able to `spyOn` during tests
jest.mock("wagmi", () => {
const actual = jest.requireActual("wagmi");
return {
__esModule: true,
...actual,
};
});
// Mock WalletConnect Web3Modal, and make it return dummy components during tests
jest.mock("@web3modal/wagmi-react-native", () => {
const actual = jest.requireActual("@web3modal/wagmi-react-native");
return {
...actual,
Web3Modal: () => <MockView />,
W3mButton: () => <MockConnectButton />,
};
});
export function MockConnectButton() {
const { isConnected, address } = useAccount();
const { connect, connectors } = useConnect();
// During tests, mock the Connect Button to automatically connect to the first connector
// instead of showing the WalletConnect Modal
return (
<Pressable
role="button"
onPress={() => connect({ connector: connectors[0] })}
>
<Text>{isConnected ? address : "Connect Wallet"}</Text>
</Pressable>
);
}
view raw mocks.tsx hosted with ❤ by GitHub

If you're following on your own project, you might not need some of the mocks being used here, such as @gorhom/bottom-sheet, 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 wagmiConfig passed to the provider.

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

import {
Web3Modal,
createWeb3Modal,
defaultWagmiConfig,
} from "@web3modal/wagmi-react-native";
import { mainnet, sepolia } from "viem/chains";
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// Setup QueryClient
const queryClient = new QueryClient();
// Web3Modal projectId
const projectId = process.env.EXPO_PUBLIC_WALLETCONNECT_CLOUD_PROJECT_ID ?? "";
// Web3Modal metadata
const metadata = { ... };
// Choose chains to enable
const chains = [mainnet, sepolia] as const;
// Use Web3Modal's utils to create the Wagmi config and attach to Web3Modal
const wagmiConfig = defaultWagmiConfig({ chains, projectId, metadata });
// Init Web3Modal instance
createWeb3Modal({
projectId,
defaultChain: mainnet,
wagmiConfig,
});
export default function App() {
return (
// Pass wagmiConfig to the Wagmi Provider
<WagmiProvider config={wagmiConfig}>
...
</WagmiProvider>
);
}
view raw App.tsx hosted with ❤ by GitHub

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

Let's do this in a new file, src/test/config.tsx:

import { render } from "@testing-library/react-native";
import {
Address,
PublicClient,
createTestClient,
http,
publicActions,
walletActions,
} from "viem";
import { foundry } from "viem/chains";
import { WagmiProvider, createConfig } from "wagmi";
import { mock } from "wagmi/connectors";
// Setup QueryClient
const queryClient = new QueryClient();
// Choose chains to use while testing. Foundry will point to Anvil
const chains = [foundry];
// Account we will use while testing. This is Anvil's first account
export const TEST_ACCOUNTS = [
{
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" as Address,
key: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
},
];
// Create a Viem Test Client, and connect to Anvil
export const testClient = createTestClient({
transport: http(),
chain: chains[0],
mode: "anvil",
// Pass the account address and private key, so we can sign transactions
account: TEST_ACCOUNTS[0].address,
key: TEST_ACCOUNTS[0].key,
// Use a low pollingInterval to speed up tests
pollingInterval: 100,
})
// Extend the client with public and wallet actions, so it can also act as a Public Client and Wallet Client
.extend(publicActions)
.extend(walletActions);
export function renderWithProviders() {
// Create a Wagmi Config specifically for testing
const testConfig = createConfig({
chains: [foundry],
connectors: [
mock({
accounts: [TEST_ACCOUNTS[0].address],
}),
],
client() {
return testClient as Client;
},
// Disable multiInjectedProviderDiscovery because it uses browser APIs
multiInjectedProviderDiscovery: false,
});
return render(
// Pass the test Wagmi Config to the Wagmi Provider
<WagmiProvider config={testConfig}>
<QueryClientProvider client={queryClient}>
<Web3Modal />
<GestureHandlerRootView style={{ flex: 1 }}>
<NavigationContainer>
{/* Use smaller durations for the Toast library to speed up tests */}
<ToastProvider duration={1000} animationDuration={0}>
<RootNavigator />
</ToastProvider>
</NavigationContainer>
</GestureHandlerRootView>
</QueryClientProvider>
</WagmiProvider>
);
}
view raw config.tsx hosted with ❤ by GitHub

Here, notice a few things:

  • At the beginning we define a chains constant, and put the foundry chain inside, which will point to Anvil.
    ‍
  • Then we define a TEST_ACCOUNTS 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 testClient, 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 pollingInterval so changes in the blockchain are reflected faster on the client, speeding up tests.
    4. Extend the testClient with public and wallet actions, so it can also act as a publicClient (query info from the blockchain) and walletClient (transact, sign messages, and do other wallet actions).
  • Finally, we define a renderWithProviders() 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, src/features/home/HomeScreen.test.tsx.

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.
test("user is disconnected", async () => {
// Always start by rendering the app
renderWithProviders();
// Since we just rendered, we won't be connected. The Connect Wallet button should appear.
const connectButton = screen.getByRole("button", { name: "Connect Wallet" });
expect(connectButton).toBeOnTheScreen();
// If wallet is not connected, a message should be displayed
const connectMessage = screen.getByText(
/connect to a wallet to view contacts/i
);
expect(connectMessage).toBeOnTheScreen();
});

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

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

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

Finally, expect all elements toBeOnTheScreen().

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.

test("user connects the wallet", async () => {
// Always start by rendering the app
renderWithProviders();
// Connect Wallet
const connectButton = screen.getByRole("button", { name: "Connect Wallet" });
expect(connectButton).toBeOnTheScreen();
await user.press(connectButton);
// If wallet is connected, a list of contacts should be displayed
// CONTACTS is a hardcoded list being used in the app
const firstContact = screen.getByRole("button", { name: CONTACTS[0].name });
expect(firstContact).toBeOnTheScreen();
});

In this example, we're not only querying elements on the screen but also interacting with them using user.press(). 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:

import { screen, userEvent } from "@testing-library/react-native";
// Setup User Events outside the tests
const user = userEvent.setup();
test("...", () => { ... });
test("...", () => { ... });
test("...", () => { ... });

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:

test("transaction succeeds", async () => {
renderWithProviders();
// Connect Wallet
const connectButton = screen.getByRole("button", { name: "Connect Wallet" });
expect(connectButton).toBeOnTheScreen();
await user.press(connectButton);
// Find and press the first contact
const firstContact = screen.getByRole("button", { name: CONTACTS[0].name });
expect(firstContact).toBeOnTheScreen();
await user.press(firstContact);
// A bottom sheet appears with the contact's details and transaction info
const message = screen.getByText(`Send 0.001 ETH to ${CONTACTS[0].name}`);
expect(message).toBeOnTheScreen();
// Find the confirm button
const confirmButton = screen.getByRole("button", { name: /confirm/i });
// Button should be enabled before sending the transaction
expect(confirmButton).toBeEnabled();
expect(confirmButton).toBeOnTheScreen();
// Press the confirm button
await user.press(confirmButton);
// Loading toast should appear
const loadingToast = await screen.findByText(/transaction processing/i);
expect(loadingToast).toBeOnTheScreen();
// Success toast should appear
const successToast = await screen.findByText(/transaction completed/i);
expect(successToast).toBeOnTheScreen();
});

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 beforeAll() and afterEach() hooks, and using the testClient we defined before to manually dump and load the blockchain state:

import { testClient, ... } from "../../test/config";
// Define the initial state at the top of the file, outside all tests.
// The state is a serialized data blob.
let initialState: Hex = "0x";
beforeAll(async () => {
// Before all tests, dump the pristine state of the blockchain into a variable
const dump = await testClient.dumpState();
initialState = dump;
});
afterEach(async () => {
// After each test, reset the blockchain to the pristine state, so that tests are isolated
await testClient.loadState({ state: initialState });
});

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 useWaitForTransaction 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 src/test/mocks.tsx file we used before:

import * as MockReact from "react";
export function useWaitForTransactionReceiptFailAfter200ms() {
// Start with every state as false
const [state, setState] = MockReact.useState({
isLoading: false,
isSuccess: false,
isError: false,
});
MockReact.useEffect(() => {
// After 100ms, simulate loading state
setTimeout(() => {
setState({
...state,
isLoading: true,
isSuccess: false,
isError: false,
});
}, 100);
// After 200ms, simulate error state
setTimeout(() => {
setState({
...state,
isLoading: false,
isSuccess: false,
isError: true,
});
}, 200);
}, []);
return state;
}
view raw mocks.tsx hosted with ❤ by GitHub

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:

test("transaction fails", async () => {
// Mock the useWaitForTransaction hook to fail after 200ms, simulating a failed transaction
const spy = jest
.spyOn(wagmi, "useWaitForTransactionReceipt")
// @ts-expect-error mockImplementation is not typed
.mockImplementation(useWaitForTransactionReceiptFailAfter200ms);
// render the app ...
// connect the wallet ...
// press a contact ...
// check for elements, press buttons ...
// Restore mock, so it doesn't affect our next tests
spy.mockRestore();
});
Remember to call spy.mockRestore() 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 src/test/utils.tsx:

import { screen } from "@testing-library/react-native";
import { UserEventInstance } from "@testing-library/react-native/build/user-event/setup";
import { CONTACTS } from "../features/home/HomeScreen";
export async function connectWallet(user: UserEventInstance) {
const connectButton = screen.getByRole("button", { name: "Connect Wallet" });
expect(connectButton).toBeOnTheScreen();
await user.press(connectButton);
}
export async function pressFirstContact(user: UserEventInstance) {
const firstContact = screen.getByRole("button", { name: CONTACTS[0].name });
expect(firstContact).toBeOnTheScreen();
await user.press(firstContact);
}
view raw utils.tsx hosted with ❤ by GitHub

And then use it in all our tests:

import * as wagmi from "wagmi";
test("transaction fails", async () => {
// Mock the useWaitForTransaction hook to fail after 200ms, simulating a failed transaction
const spy = jest
.spyOn(wagmi, "useWaitForTransaction")
// @ts-expect-error mockImplementation is not typed
.mockImplementation(useWaitForTransactionFailAfter200ms);
// Render, connect wallet, press first contact
renderWithProviders();
await connectWallet(user);
await pressFirstContact(user);
// A bottom sheet appears with the contact's details and transaction info
const message = screen.getByText(`Send 0.001 ETH to ${CONTACTS[0].name}`);
expect(message).toBeOnTheScreen();
// Find the confirm button
const confirmButton = screen.getByRole("button", { name: /confirm/i });
// Button should be enabled if balance is sufficient
expect(confirmButton).toBeOnTheScreen();
expect(confirmButton).toBeEnabled();
// Press the confirm button
await user.press(confirmButton);
// Loading toast should appear
const loadingToast = await screen.findByText(/transaction processing/i);
expect(loadingToast).toBeOnTheScreen();
// Failure toast should appear
const failureToast = await screen.findByText(/transaction failed/i);
expect(failureToast).toBeOnTheScreen();
// Restore mock, so it doesn't affect our next tests
spy.mockRestore();
});

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 testClient.setBalance() 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 setBalance. Read more in the setBalance docs.

test("insufficient balance", async () => {
// Manually set the balance of the test account to 0, to simulate insufficient balance error
await testClient.setBalance({
address: TEST_ACCOUNTS[0].address,
value: 0n,
});
renderWithProviders();
await connectWallet(user);
await pressFirstContact(user);
// A bottom sheet appears with the contact's details and transaction info
const message = screen.getByText(`Send 0.001 ETH to ${CONTACTS[0].name}`);
expect(message).toBeOnTheScreen();
// Find the confirm button
const confirmButton = screen.getByRole("button", { name: /confirm/i });
// Button should be disabled if balance is insufficient
expect(confirmButton).toBeOnTheScreen();
expect(confirmButton).toBeDisabled();
// Error toast should appear
const insufficientBalanceToast = await screen.findByText(
/insufficient balance/i
);
expect(insufficientBalanceToast).toBeOnTheScreen();
});

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.

test("confirm button should disable while loading", async () => {
// Disable mining blocks, so we can test the loading state
await testClient.setAutomine(false);
renderWithProviders();
await connectWallet(user);
await pressFirstContact(user);
// Find the confirm button
const confirmButton = screen.getByRole("button", { name: /confirm/i });
// Button should be enabled if balance is sufficient
expect(confirmButton).toBeOnTheScreen();
expect(confirmButton).toBeEnabled();
// Press the confirm button
await user.press(confirmButton);
// Button should be disabled while waiting for the transaction
expect(confirmButton).toBeDisabled();
// Enable mining blocks again
await testClient.setAutomine(true);
// Wait for the transaction to succeed
const successToast = await screen.findByText(/transaction completed/i);
expect(successToast).toBeOnTheScreen();
// Button should be enabled again after transaction is completed
expect(confirmButton).toBeEnabled();
});

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 beforeAll and afterEach 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.

Insights

Learn more about Testing

How AI Agents Can Cut Manual QA by 30% — cover

Oct 30Article

How AI Agents Can Cut Manual QA by 30%

Explore an agentic QA workflow targeting 30% less manual effort across mobile, web, and TV, with test selection, failure triage, and validation.

Give QA Agents the Controls They Need With Appduct — cover

Oct 20Article

Give QA Agents the Controls They Need With Appduct

Use Appduct to give AI QA agents access to app functions for setting test conditions, triggering events, and checking results with UI automation.

cover

Oct 13Article

Agentic Mobile Testing With e2e and agent-device

See how e2e uses agent-device for AI-driven iOS and Android tests, combines exact assertions with agent actions, and replays cached steps without model calls.

Who Tests AI-Written Code? Inside TesterArmy With Szymon Rybczak — cover

Oct 12Podcast

Who Tests AI-Written Code? Inside TesterArmy With Szymon Rybczak

Szymon Rybczak explains how TesterArmy tests web and mobile apps with AI agents, creates test plans for pull requests, and fits into QA workflows.

Automating Mobile QA With Cloud Agents

Aug 4Article

Automating Mobile QA With Cloud Agents

Code review and green CI checks do not prove a mobile app still works. Learn how cloud agents, agent-device, and runtime evidence can verify app behavior before merge.

cover

Jul 21Webinar

Behavior Review for AI-Native Delivery

AI can increase code output faster than teams can verify the real app. See why behavior review matters before merge.

cover

Jul 6Article

Introducing Sniffler: Run only the E2E tests your changes can affect

Sniffler maps changed files to affected E2E tests, so teams can avoid running the full suite on every pull request.

cover

Jun 17Talk

Automating Mobile QA with Cloud Agents

Michał Pierzchała will present Automating Mobile QA with Cloud Agents at Ship London on 17 June 2026.

cover

May 13Podcast

How AI Is Changing React Native Development and Testing

Zack Ebenfeld joins React Universe On Air to talk about how AI is changing React Native workflows, testing, QA, and day-to-day engineering work.

cover

Apr 14Article

Agent React DevTools: Give AI Agents Access to React Internals

Agent React DevTools gives AI agents access to the React component tree, state, and performance data, enabling deeper debugging and more reliable QA for React and React Native apps.

cover

Mar 19Live Stream

Agent Device, AI workflows, and mobile testing in practice

A discussion on Agent Device, AI-driven workflows, and testing mobile apps using simulators and accessibility data.

cover

Aug 23Podcast

Introducing E2E Tests to React Native Core

This episode of the React Native Show dives into the recent introduction of End-to-End (E2E) testing in RNTester. Join us as we unpack the thought process, technical considerations, and practical implementation of this new approach to testing in React Native.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

Writing a Swift TurboModule in React Native

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.