How to spy on method inside a React Functional Component?

Viewed 10496

I have this functional component.

Search.js


function Search() {
  const [term, setTerm] = useState('sun');

  function handleOnChange(e) {
    if (!e.target.value) {
      return false;
    }
    setTerm(e.target.value);
    return true;
  }

  return <input type="text" onChange={handleOnChange} placeholder="Search" />
}

Search.test.js

import { render, fireEvent } from '@testing-library/react';
import Search from '.';

describe('when type a valid term', () => {
  it('update the state', () => {
    const { getByPlaceholderText } = render(<Search />;

    // this doesn't work. The handleOnChange method is private. How to deal with this?
    const handlerSpy = jest.spyOn(Search, 'handleOnChange');

    fireEvent.click(getByPlaceholderText(/search/i), { target: { value: 'moon' } });

    expect(handlerSpy).toHaveReturnedWith(true);
  });
});


I don't know if I'm trying the wrong approach. I just need to test what happens if the user type an empty term. Appreciate any suggestion.

3 Answers

Please, if you have a better answer left it here. After a search for different approaches, I realized another way to test it.

First, I attached the current state to the value attribute of my search field.

This way, I can check if the attribute value of my search field changes accordingly


import { render, fireEvent } from '@testing-library/react';
import Search from '.';

describe('when type a valid term', () => {
  it('update the state', () => {
    const { getByPlaceholderText } = render(<Search />);
    const inputField = getByPlaceholderText(/search/i);
    fireEvent.change(inputField, { target: { value: 'moon' } });

    expect(inputField).toHaveValue('moon');
  });
});

It's possible to write a snapshot test as well.

Your current approach is closer to how tests were done in enzyme (test implementation details). I'd recommend checking the documentation of testing-library https://testing-library.com/docs/intro#the-problem

You should be testing as if your final user was interacting with your app. A more appropriate test might be:

describe('when no search is present', () => {
  it('displays placeholder text', () => {
    const { getByPlaceholderText } = render(<Search />;
    const searchInput = getByPlaceholderText(/search/i)
    expect(searchInput).toBeInTheDocument()
  });
});

testing this way will give you the confidence you need and also code coverage

You struggle to test it because you're testing to many things at the same time.

Here's a solution where I decouple your state from your rendering.
I converted your component to a class component so now you can have access to state and methods using enzyme.

Note: your example is very simple and these tests might sound overkill. But it's a nice example to understand what you are testing and what tool you can use for each test case.

As a general advice, when you struggle to test something, it's usually because you're testing too many things.
And if you're only testing one thing, then maybe you have too many thing at the same place.

import React from "react";

export default class Search extends React.Component {

    constructor(props) {
        super(props);
        this.handleOnChange = this.handleOnChange.bind(this);
        this.state = {
            term: ''
        }
    }

    handleOnChange(value) {
        if (!value) {
            return false;
        }
        this.setState({term: value});
        return true;
    }

    render() {
        return <SearchInput handleOnChange={this.handleOnChange} />
    }
}

export function SearchInput(props) {
    return <input data-testid="search-input" type="text" placeholder="Search"
           onChange={e => props.handleOnChange(e.target.value)} />
}

Now we can test each feature independently.

import React from "react";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {shallow} from "enzyme";
import Search, {SearchInput} from "./App";

describe('Search section', () => {
    describe("Search", () => {
        const searchWrapper = shallow(<Search />);

        it('should update the term on change', () => {
            searchWrapper.instance().handleOnChange("test-input");
            expect(searchWrapper.instance().state.term).toBe("test-input");
        });

        it("should render the search input with handleOnChange", () => {
            const searchInput = searchWrapper.find(SearchInput);
            expect(searchInput.props().handleOnChange).toBe(searchWrapper.instance().handleOnChange);
        });
    });

    describe("Search input", () => {
        it("should call handleOnChange callback when typing", () => {
            const onChangeMock = jest.fn();
            const searchInput = render(<SearchInput handleOnChange={onChangeMock} />);

            userEvent.type(searchInput.getByTestId('search-input'), 'test input');

            expect(onChangeMock).toBeCalledWith('test input');
        });
    });
});
Related