简体   繁体   中英

How to test a function passed as props to child in React?

I need some advice on how to write tests for React component. I have 2 approaches in my mind, feel free to suggest more.

I have a component App which renders a ComplexSearchBox . I pass this ComplexSearchBox a prop onSearch , the value of this prop is a function. This function is invoked by ComplexSearchBox at various user interactions like when the user hits enter on the input box or clicks the search button.

const App = () => {
    return <ComplexSearchBox onSearch={query => console.log(query)}/>;
};

I want to write tests for App component. I'm using Enzyme to write tests. I'm following some principles of React Testing Library. I'm mounting the component. So this will render all children as well. I don't want to render ComplexSearchBox so I'll mock it.

This is where my problem is. I have 2 approaches in my mind.

  1. I can get the props of ComplexSearchBox and invoke the method directly with the required parameters.
jest.mock('Path To ComplexSearchBox', function ComplexSearchBox() {
    return null;
});

describe('App', () => {
    describe('when search button is clicked', () => {
        it('should log to console by invoking prop method', () => {
            const wrapper = mount(<App/>);
            wrapper.find('ComplexSearchBox').props().onSearch('My random test query');
            //expect something
        });
    });
});
  1. I can mock the ComplexSearchBox and return a simplified version of it. Now I can type the query in an input box and then click a button to submit.
jest.mock('Path To ComplexSearchBox', function ComplexSearchBox({onSearch}) {
    const [query, setQuery] = useState('Sample Search Query');
    return <div>
        <input value={query} onChange={setQuery}/>
        <button onClick={() => onSearch(query)}/>
    </div>;
});

describe('App', () => {
    describe('when search button is clicked', () => {
        it('should log to console by clicking', () => {
            const wrapper = mount(<App/>);
            wrapper.find('input').simulate('change', {target: {value: 'My random test query'}});
            wrapper.find('button').simulate('click');
            //expect something
        });
    });
});

I see value in the second approach. However, I'm not sure if it is worth the effort of creating a simplified version every time I have to interact with a child component.

The benefit of the second approach is that

  1. It decouples the code from tests. My tests don't have to know which method to invoke with what parameter when the user wants to execute a search. Mock knows which method to invoke but that is at one place and not spread across all the tests.
  2. I find tests to be more readable and behaviour oriented when written this way.
  3. This mock can be extracted out and used at multiple places. Making writing tests easier.
  4. Any method sequencing can be abstracted in the mock component. Like if I modify the ComplexSearchBox as below.
const App = () => {
    return <ComplexSearchBox preProcess={()=>{}} onSearch={query => console.log(query)} postProcess={()=>{}}/>;
};

jest.mock('Path To ComplexSearchBox', function ComplexSearchBox({preProcess, onSearch, postProcess}) {
    const [query, setQuery] = useState('Sample Search Query');
    const search = (query) => {
        const preProcessedQuery = preProcess(query);
        const searchedQuery = onSearch(preProcessedQuery);
        postProcess(searchedQuery);
    };
    return <div>
        <input value={query} onChange={setQuery}/>
        <button onClick={() => search(query)}/>
    </div>;
});

Though I'm not very sure if the last benefit is really a benefit. As now my mock is aware of the lifecycle of ComplexSearchBox . But on the other hand, this mock will be written only once and will save me from calling those 3 methods one after the other in a lot of tests. I could also argue that a component test written with approach one should not really care about the method sequencing as that is ComplexSearchBox responsibility. Those 3 methods do have a tight coupling as one's output is next one's input. And now I'm borderline integration testing these 2 components.

I could also have 3 buttons which have onClick to run those 3 methods and now I can test them individually.

I'm not really sure which approach is better. I'm leaning a bit towards approach 2 because it makes my tests less dependent on implementation.

I'd appreciate any advice on this and if you have another way to test this scenario then please share.

I don't have an exact answer for you, but I highly suggest reading this article:

https://kentcdodds.com/blog/why-i-never-use-shallow-rendering

In it, you will find arguments against shallow rendering, and in favour or integration tests in React.

The points you made are all valid, if you choose the route of unit testing, therefore mocking all subcomponents of the <App /> component, you will be coupling your tests to the current implementation of those subcomponents, and anytime you change those subcomponents, even if the refactor doesn't change the behaviour, you run the risk of breaking the <App /> s test.

I don't believe there is a way around it when you are dealing with mocks, so I would suggest considering the article above and not mocking anything, which might run the tests a little slower, but it probably won't be a big deal most of the time!

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