vendredi 29 janvier 2021

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.

Aucun commentaire:

Enregistrer un commentaire