简体   繁体   中英

How to spy on method inside a React Functional Component?

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.

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 accordling.


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');
        });
    });
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM