In my years of developing React applications, testing has always been the backbone of a reliable product. One tool I find myself reaching for constantly is jest.spyOn().
It is incredibly useful when you want to track how a component interacts with its dependencies without completely replacing them.
In this tutorial, I’ll share how I use Jest to spy on React components and functions to ensure my code behaves exactly as expected.
Why You Should Use Jest spyOn in React
When I first started testing, I used to mock everything. However, mocking can sometimes hide bugs because you aren’t testing the actual implementation.
jest.spyOn() allows you to “watch” a function while keeping the original logic intact.
This is perfect for verifying that a specific method was called during a user interaction, like clicking a “Submit” button on a US tax filing form.
Set Up a US-Based Project Example
To make this practical, let’s imagine we are building a simple “New York State Tax Calculator.”
We will have a utility function that calculates the tax and a React component that triggers it.
First, let’s look at our utility file (taxUtils.js):
// taxUtils.js
export const taxUtils = {
calculateStateTax: (income) => {
// Simplified NY State Tax Logic
return income * 0.06;
}
};Now, here is our React component (TaxCalculator.js):
// TaxCalculator.js
import React, { useState } from 'react';
import { taxUtils } from './taxUtils';
const TaxCalculator = () => {
const [income, setIncome] = useState(0);
const [tax, setTax] = useState(null);
const handleCalculate = () => {
const result = taxUtils.calculateStateTax(income);
setTax(result);
};
return (
<div style={{ padding: '20px' }}>
<h1>NY State Tax Estimator</h1>
<label htmlFor="income-input">Enter Annual Income (USD): </label>
<input
id="income-input"
type="number"
value={income}
onChange={(e) => setIncome(e.target.value)}
/>
<button onClick={handleCalculate}>Calculate Tax</button>
{tax !== null && <p id="result-display">Your estimated tax is: ${tax}</p>}
</div>
);
};
export default TaxCalculator;Method 1: Spying on an External Utility Function
This is the most common scenario I encounter. I want to make sure my component calls the tax calculation logic correctly.
In this example, we use jest.spyOn to track the calculateStateTax method.
// TaxCalculator.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { taxUtils } from './taxUtils';
import TaxCalculator from './TaxCalculator';
describe('TaxCalculator Component', () => {
it('should call calculateStateTax when the button is clicked', () => {
// 1. Create the spy
const spy = jest.spyOn(taxUtils, 'calculateStateTax');
render(<TaxCalculator />);
// 2. Interact with the UI
const input = screen.getByLabelText(/Enter Annual Income/i);
const button = screen.getByText(/Calculate Tax/i);
fireEvent.change(input, { target: { value: '50000' } });
fireEvent.click(button);
// 3. Assertions
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith('50000');
// 4. Cleanup
spy.mockRestore();
});
});I executed the code above and added the screenshot below.

I always make sure to call spy.mockRestore() at the end. This ensures that other tests in the suite aren’t affected by the spy we just created.
Method 2: Spying and Providing a Mock Implementation
Sometimes, the original function might perform a heavy API call or a complex calculation that you don’t want to run during the test.
In these cases, I use .mockImplementation().
Let’s modify our test to return a fixed value regardless of the input.
// TaxCalculator.test.js (Mock Implementation)
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { taxUtils } from './taxUtils';
import TaxCalculator from './TaxCalculator';
describe('TaxCalculator with Mock Implementation', () => {
it('displays a custom tax value using a mock implementation', () => {
// Spy on the utility but provide a fake return value
const spy = jest.spyOn(taxUtils, 'calculateStateTax').mockImplementation(() => 3000);
render(<TaxCalculator />);
const input = screen.getByLabelText(/Enter Annual Income/i);
const button = screen.getByText(/Calculate Tax/i);
fireEvent.change(input, { target: { value: '100000' } });
fireEvent.click(button);
// Verify the UI shows our mocked value of 3000
const result = screen.getByText(/Your estimated tax is: \$3000/);
expect(result).toBeInTheDocument();
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});I executed the code above and added the screenshot below.

Using this method helps me isolate the component UI from the underlying business logic.
Method 3: Spying on Child Component Props
Another trick I use frequently involves spying on functions passed as props to child components.
Suppose we have a “Submit” button that is a separate component.
// SubmitButton.js
const SubmitButton = ({ onClick }) => (
<button onClick={onClick} className="btn-primary">
Submit to IRS
</button>
);
export default SubmitButton;When I test the parent component, I want to ensure that clicking this button triggers the parent’s submission handler.
// ParentComponent.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import ParentComponent from './ParentComponent';
describe('Parent Interaction', () => {
it('should trigger the submission handler when the button is clicked', () => {
// If the handler is an internal method, we often spy on the prop
const handleAction = jest.fn();
render(<SubmitButton onClick={handleAction} />);
const btn = screen.getByText(/Submit to IRS/i);
fireEvent.click(btn);
expect(handleAction).toHaveBeenCalled();
});
});Handle “Module not found” Errors with Spies
One issue I often see developers face is trying to spy on a function that is exported directly (not as an object).
Jest can only spy on properties of an object.
If you have export const myFunction = …, you might need to import everything as an object in your test:
import * as utils from ‘./myUtils’;
Then you can use jest.spyOn(utils, ‘myFunction’).
Best Practices for Spying in React
In my experience, following these rules will save you hours of debugging:
- Always Restore: Use afterEach(() => jest.restoreAllMocks()) in your test setup.
- Don’t Spy on Everything: Only spy on things that have side effects or are external to the component.
- Verify Arguments: Use .toHaveBeenCalledWith() to ensure the correct data (like a zip code or SSN format) is being passed.
- Prefer spyOn over jest.fn(): Use
spyOnwhen you want to keep the original functionality; use jest.fn() when you are building a pure mock from scratch.
Testing is more of an art than a science.
The more you use jest.spyOn(), the more you will realize how it provides a safety net for your React applications.
I hope this guide helps you write cleaner, more effective tests for your projects.
If you have any questions about handling complex spies or mocking modules, feel free to dive deeper into the Jest documentation.
Using these methods has made my development process much smoother, and I’m sure it will do the same for you.
You may read:
- How to Reset Component State in React
- Best Accessible React Component Libraries for Modern Web Apps
- How to Create React Components Using CLI
- How to Build a React Stepper Component

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.