Credits: Photo by Tima Miroshnichenko
Yes yes I know, testing, I'll be honest, I hate (start) writing tests, but, once I start, I love it, the problem is that then, I want to keep writing tests, and not coding LOL, just joking but is kinda like that, you may suffer from the same mix of feelings (I hope not)
This is a very very interesting toping, since many developers, even seniors, sometimes don't know where to start (starting is the issue as you can see), or how we can use utils or helpers to reduce the boilerplate in our components, especially when I want to test components wrapped in several Context Providers. Do I need to repeat my self on every test file? Hopefully, this will make your life easier from now on, let's get into it!... We will be using react testing library, of course.
The problem
We have an application that has some Context, and our components consume those Context values, now we need to test these components, and we want to definitely pass customs values to our components Providers to try to assert the results in our unit tests
The initial solution
Initially, you may think let's export our Provider and pass the custom values etc and expect some results, well yes and no, this is a problem for the next reasons
- Repeat your self all the time in all files but adding the Context Provider with the values
- If you need to render the component that you need you want to test with more than one Context this may become hard to read and very boilerplate
Let's take a simple Context example
const initialState = {
name: "alex",
age: 39
};
const MyContext = React.createContext(initialState);
export const useMyContext = () => React.useContext(MyContext);
const reducer = (currentState, newState) => ({ ...currentState, ...newState });
export const MyContextProvider = ({ children }) => {
const [state, setState] = React.useReducer(reducer, initialState);
return (
<MyContext.Provider value={{ state, setState }}>
{children}
</MyContext.Provider>
);
};
BTW you can make this cooler but destructuring the Provider from the Context all in one line bla bla, notice the cool useReducer :), but is the same basically, so, you will use this Context like:
export default function App() {
return (
<MyContextProvider>
<Component />
</MyContextProvider>
);
}
And in component you can use you Context by using the custom hook you already declared in the Context file, something like:
function Component() {
const { state, setState } = useMyContext();
return (
<div>
<input
value={state.name}
onChange={(e) => setState({ name: e.target.value })}
/>
Name: {state.name}, Last Name: {state.lastName}
</div>
);
}
Now you want to test this component right?, What do you do? Export the Context to declare again the wrapper in my test and passing custom values, let go to our Context file and export our context
export const MyContext = React.createContext(initialState);
Now in your test you will do something like
import { render } from '@testing-library/react';
const renderComponent() {
return (
render(
<MyContext.Provider value={{ mockState, mockFnc}}>
<Component>
</MyContext.Provider>
)
)
}
// ... test
This is fine if your component uses only one Context, but if you use several? And even if is one, you need to do these in all your tests
Solution: the custom render
Let's build a custom render method that returns our component wrapped in as many Contexts we want with as many Provider values we want!
// /testUtils/index.js
// custom render
import { render as rtlRender } from '@testing-library/react';
// our custom render
export const render = (ui, renderOptions) => {
try {
return rtlRender(setupComponent(ui, renderOptions));
} catch (error: unknown) {
throw new Error('Render rest util error');
}
};
This utility method will expect to params, the component, called ui, and the options, and it will use setupComponent method to render the view as a normal react component, let's finished!
// /testUtils/index.js
// import all the Context you will use in the app
import {MyContext} from './MyContext'
import {MyContext1} from './MyContext'
import {MyContext2} from './MyContext'
const CONTEXT_MAP = {
MyContext,
MyContext1,
MyContext2
}
const setupComponent = (ui, renderOptions) => {
const { withContext } = renderOptions;
if (withContext == null) return ui;
return (
<>
{withContext.reduceRight((acc, { context, contextValue }) => {
const Ctx = CONTEXT_MAP[context];
return <Ctx.Provider value={contextValue}>{acc}</Ctx.Provider>;
}, ui)}
</>
);
};
By reducing right you ensure the first Context you pass, will the at the first to be rendered, nice eh? Final file looks like:
// /testUtils/index.js
// import all the context you will use in the app
import { render as rtlRender } from '@testing-library/react';
import {MyContext} from './MyContext'
import {MyContext1} from './MyContext'
import {MyContext2} from './MyContext'
const CONTEXT_MAP = {
MyContext,
MyContext1,
MyContext2
}
const setupComponent = (ui, renderOptions) => {
const { withContext } = renderOptions;
if (withContext == null) return ui;
return (
<>
{withContext.reduceRight((acc, { context, contextValue }) => {
const Ctx = CONTEXT_MAP[context];
return <Ctx.Provider value={contextValue}>{acc}</Ctx.Provider>;
}, ui)}
</>
);
};
// our custom render
export const render = (ui, renderOptions) => {
try {
return rtlRender(setupComponent(ui, renderOptions));
} catch (error: unknown) {
throw new Error('Render rest util error');
}
};
Now the same test will look like this:
import { render } from './testUtils';
const renderComponent() {
return (
render(
<Component/>,
[{context: "MyContext", contextValue: {name: 'Max', lastName: "Smith"}}]
)
)
}
// test ...
The cool thing is that in the array of Contexts you can pass as many of them as you want, following the format of {context, contextValue}, of course, the recommendation is to use typescript, but it will make the article longer, but now you got the idea if you have any issues turning this into TS let me know I can help. That's it guys, let me know if you use any other trick or if you do it using a different approach. Happy coding!
Top comments (0)