How to manage unit tests snapshots for components containing theme data? - javascript

let's assume this easy example with a component and a unit test
component.jsx
const theme = process.env.THEME
export const Component = ({ title }) => {
return (
<>
<h2>{theme} {title}</h2>
</>
)
}
component.test.jsx
import { Component } from './Component'
describe('<Component />', () => {
it('renders properly', () => {
const wrapper = shallow(<Context {...props} />)
expect(wrapper).toMatchSnapshot()
})
})
The project I'm running depends on the const theme, so in my pipeline I've more builds, more deploys, one per theme.
Of course the snapshots fail due to the fact that every time I run the toMatchSnapshot() functionality, it writes / checks the output in the folder ./__snapshots__, so the test for the first theme runs succesfully, then it fails for the others.
Are there solutions to this problem?
Provide the theme as prop to every component (terrible approach)
Avoid snapshots for these components (not so good approach because I would like to keep this functionality on for my test suite)
Use React.createContext (good approach but it requires a bit of refactoring across the project)
Is there something even better I could try to use?
Thanks everyone in advance

React.createContext with a mocked context data for testing is the best approach indeed.

Related

Unit test redux toolkit queries connected via fixedCacheQuery using react testing library

I have two components in my react app:
Component A
Performs a lazy fetch of users. This looks like:
const ComponentA = () => {
const [trigger, {data}] = useLazyLoadUsers({
fixedCacheKey: fixedLoadUsersKey,
});
useEffect(() => {
trigger();
}, []);
return <div>{data.map(user => user.id)}</div>
}
Component B
Wants to render a loading indicator while useLazyLoadUsers's isLoading property equals true. This component looks like this:
const ComponentB = () => {
const [, {isLoading}] = useLazyLoadUsers({
fixedCacheKey: fixedLoadUsersKey,
});
if (!isLoading) {
return <div>Users loaded</div>
}
return <div>Loading users</div>
}
The issue
While this works well (the states are in sync via the fixedLoadUsersKey), I'm struggling to find documentation or examples on how to test Component B.
Testing Component A is well documented here https://redux.js.org/usage/writing-tests#ui-and-network-testing-tools.
I already have an overwritten react testing library render method that provides a real store (which includes all my auto-generated queries).
What I would like to do is testing that Component B loading indicator renders - or not - based on a mocked isLoading value. I want to keep my current or similar implementation, not duplicating the isLoading state into another slice.
So far, I have tried mocking useLazyLoadUsers without success. I also tried dispatching an initiator before rendering the test, something like
it('should render the loading indicator', async () => {
const store = makeMockedStore();
store.dispatch(myApi.endpoints.loadUsers.initiate());
render(<ComponentB />, {store});
expect(await screen.findByText('Loading users')).toBeVisible();
})
This didn't work either.
Does someone have a hint on how to proceed here or suggestions on best practices?

Loading React hooks using dynamic imports?

I'm using a few third-party React hook libraries that aren't required for the initial render. E.g. react-use-gesture, react-spring, and react-hook-form. They all provide interactivity, which can wait until after the UI is rendered. I want to dynamically load these using Webpack's codesplitting (i.e. import()) after I render my component.
However, I can't stub out a React hook because it's essentially a conditional hook, which React doesn't support.
The 2 solutions that I can think of are:
Somehow extract the hook into a component and use composition
Force React to reconstruct a component after the hook loads
Both solutions seem hacky and it's likely that future engineers will mess it up. Are there better solutions for this?
As you say it, there are two ways to go about using lazy loaded hooks:
Load library in a Parent Component, conditionally render Component using library when available
Something along the lines of
let lib
const loadLib = () => {...}
const Component = () => {
const {...hooks} = lib
...
}
const Parent = () => {
const [loaded, setLoaded] = useState(false)
useEffect(() => loadComponent().then(() => setLoaded(true)), [])
return loaded && <Component/>
}
This method is indeed a little hacky and a lot of manual work for each library
Start loading a component using the hook, fail, reconstruct the component when the hook is loaded
This can be streamlined with the help of React.Suspense
<Suspense fallback={"Loading..."}>
<ComponentWithLazyHook/>
</Suspense>
Suspense works similar to Error Boundary like follows:
Component throws a Promise during rendering (via React.lazy or manually)
Suspense catches that Promise and renders Fallback
Promise resolves
Suspense re-renders the component
This way is likely to get more popular when Suspense for Data Fetching matures from experimental phase.
But for our purposes of loading a library once, and likely caching the result, a simple implementation of data fetching can do the trick
const cache = {}
const errorsCache = {}
// <Suspense> catches the thrown promise
// and rerenders children when promise resolves
export const useSuspense = (importPromise, cacheKey) => {
const cachedModule = cache[cacheKey]
// already loaded previously
if (cachedModule) return cachedModule
//prevents import() loop on failed imports
if (errorsCache[cacheKey]) throw errorsCache[cacheKey]
// gets caught by Suspense
throw importPromise
.then((mod) => (cache[cacheKey] = mod))
.catch((err) => {
errorsCache[cacheKey] = err
})
};
const SuspendedComp = () => {
const { useForm } = useSuspense(import("react-hook-form"), "react-hook-form")
const { register, handleSubmit, watch, errors } = useForm()
...
}
...
<Suspense fallback={null}>
<SuspendedComp/>
</Suspense>
You can see a sample implementation here.
Edit:
As I was writing the example in codesandbox, it completely escaped me that dependency resolution will behave differently than locally in webpack.
Webpack import() can't handle completely dynamic paths like import(importPath). It must have import('react-hook-form') somewhere statically, to create a chunk at build time.
So we must write import('react-hook-form') ourselves and also provide the importPath = 'react-hook-form' to use as a cache key.
I updated the codesanbox example to one that works with webpack, the old example, which won't work locally, can be found here
Have you considered stubbing the hooks? We used something similar to async load a large lib, but it was not a hook, so YMMV.
// init with stub
let _useDrag = () => undefined;
// load the actual implementation asynchronously
import('react-use-gesture').then(({useDrag}) => _useDrag = useDrag);
export asyncUseDrag = (cb) => _useDrag(cb)

How do I find child components without having to specify the HOC wrapping them?

Often when I'm working with Material-ui its extensible nature gets in the way of testing.
This is because even though I am using, say:
const MyEventButton = () => (<IconButton />
<Event />
</IconButton>)
What I get in the test output of a shallow render is:
<WithStyles(ForwardRef(IconButton)) >
<Event />
</WithStyles(ForwardRef(IconButton)) >
This HOC wrapping is not something I overly care about for my tests (i.e. I'm not testing that the component is WithStyles or uses ForwardRef), because it's an implementation detail which could change without me wanting/needing to update tests.
Ultimately, this means I cannot write my tests as:
it('Renders IconButton', () => {
const wrapper = shallow(<MyEventButton />);
expect(wrapper.exists('IconButton')).toEqual(true);
}
I have to do:
it('Renders IconButton', () => {
const wrapper = shallow(<MyEventButton />);
expect(wrapper.exists('WithStyles(ForwardRef(IconButton))')).toEqual(true);
}
Which feels like I've coupled my tests too tightly with materials current inner workings.
Is there a way of the tests peering inside the HOC and getting the base Child component? Preferably without having to dive or guess where in the component it is with children().at(0) an poking around. Using `wrapper.exists() also fails for similar reasons.
I have had a look around Stackoverflow and the internet and not found a working solution.
You don't have to create the component to test it exists
Instead of doing:
expect(wrapper.exists('WithStyles(ForwardRef(IconButton))')).toEqual(true);
or the component
expect(wrapper.exists(<IconButton/>)).toEqual(true);
Just tell enzyme you expect the component's function to exist, and it will understand:
expect(wrapper.exists(IconButton)).toEqual(true); // passes

How to test a React component that dispatches a Redux / Thunk action

I'm writing an integration test for a component that should redirect to a specific path depending on the response from an asynchronous (thunk) redux action.
This is a simplified version of my component:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
redirect: false
}
this.props.dispatch(asyncThunkAction())
.then( () => this.setState({redirec: true}) )
.catch( (err) => console.log('action failed') )
}
...
render() {
if (this.state.redirect) {
return <Redirect to='/whocares' />
}
...
}
}
function mapStateToProps(state) {
return {
...
};
}
export default connect(mapStateToProps)(MyComponent);
I want to write a test that asserts that the component redirected to the expected path.
I am using this technique for inspecting the actual redirection path (It's not perfect but it's not the focus of this question).
The place where I am stuck is the state change in the .then() following the redux/thunk action. Because it's a promise, the redirect happens after my expect statement, so I have not been able to test that.
Here's what my test looks like:
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
test('redirects after thunk action', () => {
const redirectUrl = '/whocares'
const data = {};
jest.mock('../actions');
act(() => {
ReactDOM.render(
<TestRouter
ComponentWithRedirection={<MyComponent store={mockStore(data)} />}
RedirectUrl={redirectUrl}
/>,
container);
});
expect(container.innerHTML).toEqual(
expect.stringContaining(redirectUrl)
)
})
My TestRouter just prints the anticipated redirect URL into the DOM. (Check out the link above for a full explanation of this hack.) So right now instead of hitting the expected route, my test (correctly) identifies the loading screen that appears while the thunk action is in progress.
I think the right way to do this is to mock the response from asyncThunkAction so that it returns a resolved promise with matching data, but so far I have not been able to figure out how to do that. I followed the Jest documentation on manual mocks and created the corresponding mock file:
// __mocks__/actions.js
const asyncThunkAction = function(){
return Promise.resolve({foo: 'bar'});
};
export { asyncThunkAction };
...but my test still "sees" the loading state. I don't even think it's looking at my mocked file/action.
What is the right way to do this?
Here's my "recipe" for how I was able to get this working...
Use testing-library/react...
import { render, fireEvent, waitForElement, act } from '#testing-library/react';
(+1 to #tmahle for this suggestion)
Mock axios (or in my case the API module that wraps it) by creating a "manual mock" which basically entails creating a __mocks__ directory next to the real file containing a file by the same name. Then export an object with a property that replaces the get method (or whichever one your code uses).
//__mocks__/myclient.js
export default {
get: jest.fn(() => Promise.resolve({ data: {} }))
};
Even if you don't call the mocked code in your test, you need to import it in the test file...
import myapi from '../api/myapi';
jest.mock('../api/myai');
You can mock the response from the mocked API call like this:
myapi.get.mockResolvedValueOnce({
data: { foo: "bar" },
});
I'm a little fuzzy on this part...
Even though the mocked API request responds immediately with a resolved promise, you probably need to wait for it to write expects
const { getByText, getByTestId, container } = render(<MyComponent />);
await wait(() => getByText('Some text that appears after the '));
expect(container.innerHTML).toEqual('whatever');
All of this was "out there" in various docs and SO questions... but it took me a long time to cobble it all together. Hopefully this saves you time.
This is a little bit of a sideways answer to your question, admittedly, but I would recommend trying out testing-library and the ideals that it embodies, especially for integration tests.
It is available in both DOM and React flavors, which one to use likely depends on what level of abstraction your redirect is happening at:
https://github.com/testing-library/dom-testing-library
https://github.com/testing-library/react-testing-library
With this paradigm you would not try to assert that the user gets redirected to the correct path, but rather that the correct thing is on the screen after they are redirected. You would also limit your mocking to the absolutely bare necessities (likely nothing or only browser API's that your test environment cannot emulate if you are doing a true integration test).
The overall approach here would probably have you mocking out much less and perhaps rendering a larger portion of the app. A likely-helpful example to draw from can be found here: https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/master/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-router.js&previewwindow=tests
Because there's less mocking in this approach, the specifics for how you can accomplish this would likely come from outside the scope of the example you've given, but the above example link should help a lot with getting started.

How to test rxjs ajax call with jest?

I have a container component where I'm fetching the data via ajax operator from rxjs
const data = ajax(someUrl).pipe(map(r => r.response));
And in my componentDidMount I have
data.subscribe((data) => {
this.setState({ data });
});
// test.js
import React from 'react';
import { mount } from 'enzyme';
import { ajax } from 'rxjs/ajax'
import App from '../src/App';
describe('<App />', () => {
const wrap = mount(<App />);
const data = [{ 1: 'a' }];
const mock = ajax('http://url.com').pipe(map(() => data));
it('renders', () => {
console.log(mock.subscribe(x => x));
expect(wrap.find(App).exists()).toBe(true);
});
});
How do I go about mocking the response so that when I run the test it I can pass that data on to other components and check if they render?
All the testing examples I've found have been redux-Observable ones which I'm not using.
Thanks a lot!
First you need to understand that you should be testing one thing at a time.
Meaning that testing your async method execution should be separated from testing your components rendering proper content.
To test async methods you can mock your data and than mock timers in Jest.
https://jestjs.io/docs/en/tutorial-async
https://jestjs.io/docs/en/asynchronous
https://jestjs.io/docs/en/timer-mocks.html
with jest.useFakeTimers() and techniques mentioned above.
For testing component proper rendering use jest snapshots and e2e testing (can be done with ex. TestCafe)
To connect those approaches you need to design you app in a way that will allow you to:
The API you call in your component, should be external to component and be called from that external source (different file, different class, however you design it), so you can replace it in test.
Whole API should be modular, so you can take one module and test it without initializing whole API just for this case.
If you design your app in such manner, you can initialize part of the API with mock data, than render your component in test and as it will call mocked API, you can check if it renders what you expect it to.

Categories

Resources