Skip to content

Writing Tests

Here’s a simple step-by-step guide to help you write your first tests for the Hall Pass application.

Open your terminal in the project directory and run:

Terminal window
npm install --save-dev react-test-renderer@18.2.0 @testing-library/react-native@12.3.0 babel-plugin-module-resolver

Create a Jest configuration file called jest.config.js in the root of your project.

  • global.css
  • index.js
  • jest.config.js
  • metro.config
  • nativewind-env.d.ts

This file allows you to customize how tests are run by adding the code below:

jest.config.js
module.exports = {
preset: "react-native",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
testMatch: ["**/tests/**/*.test.js"],
moduleNameMapper: {
"^~/(.*)": "<rootDir>/$1",
},
transformIgnorePatterns: [
"node_modules/(?!(react-native|@react-native|@rn-primitives|react-native-.*)/)",
],
};

Add a test script and Jest configuration to your package.json file:

package.json
{
"name": "hall-pass",
"main": "index.js",
"version": "1.0.0",
"scripts": {
"test": "npx jest --config jest.config.js",
"dev": "expo start -c",
"dev:web": "expo start -c --web",
6 collapsed lines
"dev:android": "expo start -c --android",
"android": "expo start -c --android",
"ios": "expo start -c --ios",
"web": "expo start -c --web",
"clean": "rm -rf .expo node_modules",
"postinstall": "npx tailwindcss -i ./global.css -o ./node_modules/.cache/nativewind/global.css"
},
"dependencies": {
35 collapsed lines
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@rn-primitives/avatar": "~1.1.0",
"@rn-primitives/checkbox": "^1.1.0",
"@rn-primitives/dialog": "^1.1.0",
"@rn-primitives/portal": "~1.1.0",
"@rn-primitives/progress": "~1.1.0",
"@rn-primitives/slot": "~1.1.0",
"@rn-primitives/tooltip": "~1.1.0",
"@rn-primitives/types": "~1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"expo": "~52.0.32",
"expo-linking": "~7.0.4",
"expo-navigation-bar": "~4.0.7",
"expo-router": "~4.0.16",
"expo-splash-screen": "~0.29.20",
"expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.7",
"lucide-react-native": "^0.378.0",
"nativewind": "^4.1.23",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.7",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "^4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-web": "~0.19.13",
"tailwind-merge": "^2.2.1",
"tailwindcss": "3.3.5",
"tailwindcss-animate": "^1.0.7",
"zustand": "^4.4.7"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@testing-library/react-native": "^12.3.0",
"@types/react": "~18.3.12",
"babel-plugin-module-resolver": "^5.0.2",
"react-test-renderer": "^18.2.0",
"typescript": "^5.3.3"
},
"private": true,
"jest": {
"preset": "react-native",
"testsMatch": [
"**/tests/**/*.test.js"
]
}
}

Step 4: Create your first test - the Task component

Section titled “Step 4: Create your first test - the Task component”

Create a new folder called tests at the root of your project, and place a Task.test.js file in here:

  • Directorylib/
  • Directorynode_modules/
  • Directorytests
    • Task.test.js
  • global.css
  • index.js

This will contain all the tests for the Task component

Add your first test code below to Task.test.js:

tests/Task.test.js
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react-native";
import Task from "../components/Task"; // Adjust path as needed
// Mock any components or contexts used by Task
jest.mock("~/lib/TaskContext", () => ({
useTasks: () => ({
updateTask: jest.fn(),
deleteTask: jest.fn(),
}),
}));
describe("Task", () => {
test("renders a task", () => {
const task = {
id: 1,
title: "Test Task",
category: "Test Category",
isChecked: false,
};
render(<Task task={task} />);
// Just check if the title and category are displayed
const titleElement = screen.getByText("Test Task");
const categoryElement = screen.getByText("Test Category");
expect(titleElement).toBeTruthy();
expect(categoryElement).toBeTruthy();
});
});

Run your test with the command below:

Terminal window
npm run test

This will execute Jest with your configuration and run all tests matching the pattern in your testMatch configuration.

When you run the test, you should see a passing test for our Task component:

Tests passing

Step 6: Understanding Component Rendering Tests

Section titled “Step 6: Understanding Component Rendering Tests”

The test we wrote in Step 4 is a basic component rendering test. It verifies that our component correctly displays the data it receives as props. This type of test is essential for ensuring your UI components render correctly.

However, real applications need more than just static rendering - they need to respond to user interactions!

Let’s enhance our test suite by adding an interaction test that verifies the checkbox in our Task component works correctly when pressed.

First, we need to add a testID to our checkbox component to make it easier to find in our tests:

components/Task.tsx
<Checkbox
testID="checkbox"
className="border-foreground checked:bg-foreground"
checked={isChecked}
onCheckedChange={handleSetChecked}
/>

Now, let’s add a new test to our Task.test.js file that verifies the checkbox calls the correct function when pressed:

tests/Task.test.js
12 collapsed lines
import React from "react";
import { render, screen, userEvent } from "@testing-library/react-native";
import Task from "../components/Task"; // Adjust path as needed
// Mock any components or contexts used by Task
jest.mock("~/lib/TaskContext", () => ({
useTasks: () => ({
updateTask: jest.fn(),
deleteTask: jest.fn(),
}),
}));
describe("Task", () => {
16 collapsed lines
test("renders a task", () => {
const task = {
id: 1,
title: "Test Task",
category: "Test Category",
isChecked: false,
};
render(<Task task={task} />);
// Just check if the title is displayed
const titleElement = screen.getByText("Test Task");
const categoryElement = screen.getByText("Test Category");
expect(titleElement).toBeTruthy();
expect(categoryElement).toBeTruthy();
});
test("toggles completion status when pressed", async () => {
const mockToggle = jest.fn(); // Create a mock function
const task = {
id: 1,
title: "Test Task",
category: "Test Category",
isChecked: false,
};
render(<Task task={task} onUpdate={mockToggle} />);
const checkbox = screen.getByTestId("checkbox"); // Find the checkbox element
const user = userEvent.setup();
await user.press(checkbox);
// Check if our mock function was called
expect(mockToggle).toHaveBeenCalled();
});
});

The userEvent function allows you to simulate user interactions with your components.

These must always be placed inside an async function, because they use await internally.

  1. const userEvent = userEvent.setup(); - Sets up the user event simulation
  2. await user.press(checkbox); - Simulates pressing/tapping an element
  3. await user.type(textInput, 'Hello world!'); - Simulates typing in a text input
  4. You can find out more about userEvent in the official documentation

This is powerful because it lets you test not just what your component looks like, but how it responds to user input!

Now that we’ve covered the basics of interaction testing, let’s explore how to test more complex scenarios:

Step 9: Testing with More Specific Assertions

Section titled “Step 9: Testing with More Specific Assertions”

Our current test only verifies that the onUpdate function was called, but we can make our test more specific by checking exactly how it was called:

tests/Task.test.js
import { render, screen, userEvent } from "@testing-library/react-native";
// ... other imports and setup
test("toggles completion status when pressed", async () => {
const mockToggle = jest.fn();
const task = {
id: 1,
title: "Test Task",
category: "Test Category",
isChecked: false,
};
render(<Task task={task} onUpdate={mockToggle} />);
const checkbox = screen.getByTestId("checkbox");
const user = userEvent.setup();
await user.press(checkbox);
// Check if our mock function was called
expect(mockToggle).toHaveBeenCalled();
// Check if our mock function was called with the correct arguments
expect(mockToggle).toHaveBeenCalledWith({
...task,
isChecked: true // The checkbox should toggle from false to true
});
});

It’s also important to test different starting states. Let’s add another test for when the task is already checked:

tests/Task.test.js
test("toggles from checked to unchecked when pressed", async () => {
const mockToggle = jest.fn();
const task = {
id: 1,
title: "Test Task",
category: "Test Category",
isChecked: true, // Starting as checked
};
render(<Task task={task} onUpdate={mockToggle} />);
const checkbox = screen.getByTestId("checkbox");
const user = userEvent.setup();
await user.press(checkbox);
// Check if our mock function was called with the correct arguments
expect(mockToggle).toHaveBeenCalledWith({
...task,
isChecked: false, // The checkbox should toggle from true to false
});
});

When writing interaction tests, keep these principles in mind:

  1. Test user behavior, not implementation details: Focus on what the user does and sees, not on internal component state or methods
  2. Use testIDs strategically: Add testIDs to elements that need to be interacted with in tests
  3. Test edge cases: Consider different starting states and user interactions
  4. Keep tests independent: Each test should be able to run on its own without depending on other tests
  5. Mock external dependencies: Use Jest’s mocking capabilities to isolate the component you’re testing

By following these practices, you’ll create a robust test suite that gives you confidence in your application’s behavior.

Congratulations! You’ve learned how to:

  1. Set up a testing environment for your React Native application
  2. Write basic component rendering tests
  3. Add testIDs to make components easier to find in tests
  4. Create interaction tests that simulate user behavior
  5. Use mock functions to verify component behavior
  6. Apply best practices for writing effective tests

Testing is an essential part of building reliable applications. By investing time in writing good tests, you’ll catch bugs earlier, make changes with confidence, and create a more maintainable codebase.

To continue improving your testing skills:

  1. Add tests for your other components
  2. Learn about the full React Native Testing Library API
  3. See an example of a test using React Native Testing Library in the official documentation

Remember, the goal of testing isn’t 100% code coverage, but rather confidence that your application works as expected.

Focus on testing the most critical parts of your application first, and gradually expand your test suite as your application grows.