Unit Tests for async code (indexedDB) inside React Component - reactjs

I am trying to write unit tests for a React component - That looks something broadly as follows -
a handle to a db is passed to a component
The component also receives data as props (that will change) - perhaps this is something that could come through a Redux store.
Now what I do in my useEffect for this data change is - Write into indexDB (actually using the idb library) and then on success, I read some of this data and setup a local state using setData and use this state to render
Basically all of the above works - so there is no problem with that (It's perhaps not right, but works!). My challenge is how to write unit test for this code. I have looked at using async inside jest.
The code looks something like as follows -
const Component = ({db, data}) => {
useEffect(() => {
const saveDataToDB = (data, successCB) => {
const tx = db.transaction("store", "readwrite");
const transactions = [tx.store.add(data)];
transactions.push(tx.done);
Promise.all(transactions).then((res) => {
successCB();
});
const cb = () => {
// Do somethingl
};
saveDataToDb(data, cb);
};
}, [data]);
// Rendering part.
return (
<div id="component-div">
</div>
);
};
export default Component;
and I am trying to write the tests for this component using something like following
describe('Component Tests', () => {
it("Component Renders with Data", () => {
const wrapper = mount(<Component data={data} db={db}/>);
expect(wrapper.find("#component-div").toEqual(true);
});
});
May be - there's something wrong the way component is implemented, that makes unit testing challenging. But it works, so not sure in general about what's the strategy for unit testing components that could have a few promise chains to resolve?

react testing library has support for async APIs. They can be used to achieve to solve the problem mentioned above.
This would require converting the test cases to async test cases and then awaiting using right the async APIs as described above.
The original test cases mention enzyme APIs, instead of that react-testing-library APIs are to be used.

Related

Is there a way in React to test whether or not a Functional Component calls a particular custom hook?

describe("UI Logic", () => {
it("Uses useEmailMultiSelectCustomHook", () => {
const component = render(<EmailMultiSelectCombobox/>);
const hook = jest.mock("./useEmailMultiSelectCombobox", () => ({
useEmailMuliSelect: () => {
return {
initialConditions: ["test", "test1", "test2"]
}
}
}));
const hookTest = renderHook(useEmailMultiSelectCombobox);
expect(hookTest).toHaveBeenCalledTimes(1);
});
})
export const EmailMultiSelectCombobox = () => {
const { initialConditions } = useEmailMultiSelectCombobox();
return (
<>
{/*<MultiSelectCombobox initialConditions={} handleSelectionChange={}/>*/}
</>
)
};
export const useEmailMultiSelectCombobox = () => {
const [initialConditions, setInitialConditions] = useState<Array<string>>([]);
return { initialConditions };
}
So, I have a superrrr basic setup here. All I'm trying to do is test whether or not the hook was called/initialized from the component that I want to use the custom hook. The idea here is to be able to truly unit test a custom hook apart from unit testing a view. Most things I've seen really involve an integration test with a component view layer and a component logic layer being tested together, using the user-events library that comes with the testing library.
I only have questionable answers for you which aren't really great in production, but for testing - could be acceptable?
Here's some arcane knowledge that might help. Keep in mind this is a fairly cursed abuse of power, but it might work for your purposes.
Option 1
All functions in JavaScript have a secret way to extract the code that was used to make them. Here's an example:
function MyComponent({ id, ...props }) {
const [clicked, setClicked] = useState(false);
return <div onClick={setClicked.bind(this,true)} id={id} {...props}>Hello, world</div>;
}
console.log({ usesState: MyComponent.toString().includes("useState"));
There are some nuances here. For example: if your component uses styled-components or really any kind of composition (like connect() with redux) this might not work. But if the component is pure, it should meet your needs.
I want to emphasize this is a fairly questionable use of function.toString().
Option 2
Also a bad idea, but you could have your hook in question set some test flag somewhere and then check for the presence of it.
function useMyHook() {
window.useMyHookTriggered = true;
}
if (window.useMyHookTriggered) {
// Hook was used
}
This is not great because it pollutes the public namespace. But again, it'd work.
Option 3
Your hook could emit an event. This solution, I don't hate. Still kinda pollutes things, but in a sane way.
function useMyHook() {
window.dispatchEvent(new Event("useMyHookInvoked"));
}
// The test
window.addeventlistener("useMyHookInvoked", () => {
assert(true);
})

Is it possible to mock functions outside of the test file for multiple tests to use?

Thanks in advance
Issue
I have some functions that need to be mocked in every test file. However, only in some of those files, do I actually care to evaluate or test those mock functions.
For example, let's say, that when my app renders, it immediately fetches some data In A.test.tsx I want to mock that fetch and verify that it was called.
But, in B.test.tsx, I still need to mock that fetch, but I don't care to verify that it was called.
Goal
I would like the setup to look something like this:
setup.ts
import * as SomeAPI from 'api';
const setup = () => {
jest.spyOn(SomeAPI, 'fetchData');
return { SomeAPI };
};
export default setup;
A.test.tsx
const { SomeAPI } = setup();
beforeEach(() => {
jest.clearAllMocks();
});
test('data should be fetched when app renders', async () => {
RenderWithProviders(<App />);
expect(SomeAPI.fetchData).toHaveBeenCalled();
});
B.test.tsx
setup(); // Don't destructure because I don't care to test the mock
beforeEach(() => {
jest.clearAllMocks();
});
test('test some other stuff', async () => {
// In this case, the fetchData still needs to be mocked<br>
// because the rest of the app depends on it
RenderWithProviders(<App />);
expect(someElement).toBeInTheDocument();
});
My Current Problem
My problem is that while I'm trying to attempt this way of returning mocked functions... If that mocked function is used more than once in the same test file, the jest.clearAllMocks() seems not to have an effect. I assume because the setup is happening outside of the test?
Is this possible to setup mocks outside of the test and only destructure them when needed?

Test custom hook with multiple async requests with jest and renderhook

I've been working on an application that uses react-query for handling and caching responses to our backend. Got a lot of inspiration from the creator behind that framework to use custom hooks for handling backend states. In the beginning it was quite easy but later on the hooks started to be dependent to one and another. This resulted in some wierd behavior in the CI were some testcases failed due to heavy load on the servers. (note that backend is mocked with msw)
Custom hook containing multiple custom hooks:
const useFooMutation = () => {
const cache = useQueryClient();
const { types } = useTypes(); // async call to backend
const { user } = useAuth(); // async call to backend
return useMutation(['key'], mutationFunction, {
onSuccess: async (data, variables) => {
// Do cache handling by using values types and user
},
});
};
Test case:
test('useFooMutation should foo', async () => {
const { wrapper } = setupCache();
const { result, waitFor, waitForNextUpdate } = renderHook(() => useFooMutation(), { wrapper });
await waitForNextUpdate();
act(() => {
result.current.mutate({
values
});
});
await waitFor(() => result.current.isSuccess && !result.current.isLoading);
expect(result.current.isSuccess).toEqual(true);
expect(result.current.data).toEqual(fooMockData);
});
When I debugged the code I saw that sometimes the values from the hooks useTypes and useAuth was not yet fetched. The values from the other custom hooks are used when I do cache manipulation. So this results in some wierd behavior for some machines. For example running locally this runs ok most of the times but on a heavy loaded CI server, it results in crashes quite often.
What I've tried so far is to add a timeout before the mutation function in the test case on a timeout that seems to work, not a nice solution. And by using the waitForNextUpdate the test case works fine most of the times but not when the CI is loaded.
So I need a good way to find out when then the useFooMutation has gathered all values from the hooks its dependent on before I can call mutate()?

Jest: Wait for called .then of async method in componentDidMount

I am currently stuck on writing a test of my React-App.
I have an async call in my componentDidMount method and are updating the state after returning. However, I do not get this to work.
I have found several solutions and None seems to work as expected. Below is the nearest point I have come to.
App:
class App extends Component<{}, IState> {
state: IState = {
fetchedData: false
};
async componentDidMount() {
await thing.initialize();
this.test();
}
test = () => {
this.setState({ fetchedData: true })
};
render() {
return this.state.fetchedData ? (
<div>Hello</div>
) : (
<Spinner />
);
}
}
The test
it('Base test for app', async () => {
const spy = spyOn(thing, 'initialize').and.callThrough(); // just for debugging
const wrapper = await mount(<App />);
// await wrapper.instance().componentDidMount(); // With this it works, but componentDidMount is called twice.
wrapper.update();
expect(wrapper.find('Spinner').length).toBe(0);
});
Well, so...thing.initialize is called (it is an async method that fetches some stuff).
If I do explicitly call wrapper.instance().componentDidMount() then it will work, but componentDidMount will be called twice.
Here are my ideas that I have tried but None succeeded:
Spying on thing.initialize() -> I did not find out how I proceed with the test after the method has been called and finished.
Spying on App.test -> The same here
Working with promises instead of async await
At the beginning, I had an thing.initialize().then(this.test) in my componentDidMount
It can't be much, but can someone tell me which piece I am missing?
if this is integration test you better to follow awaiting approach that say Selenium use: that is, just wait until some element appears or timeout reached. How it should be coded depends on library you use(for Puppeter it should be waitForSelector).
Once it's about unit test then I suggest you different approach:
mock every single external dependencies with Promise you control(by your code it's hard to say if automatic mock will work or you need to compose mock factory but one of them or both will help)
initialize element(I mean just run shallow() or mount())
await till your mocks are resolved(with extra await, using setTimeout(... ,0) or flush-promises will work, check how microtasks/macrotasks works)
assert against element's render and check if your mocks has been called
And finally:
setting state directly
mocking/spying on internal methods
verifying against state
are all lead to unstable test since it's implementation details you should not worry about during unit-testing. And it's hard to work with them anyway.
So your test would look like:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('loads data and renders it', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.resolve(/*data you expect to return*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(1);
})
or you may test how component behaves on rejection:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('renders error message on loading failuer', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.reject(/*some error data*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
expect(wrapper.find(SomeErrorMessage)).toHaveLength(1);
})

How to mock client.readQuery from Apollo 2 in Jest

There's a component which I am using the cache that I created in apollo, stored inside the client.
At some point, as a callback from an input, I do the following call:
const originalName = client.readQuery<NamesCached>({ query: NamesCached })!.names
this returns directly an array of objects.
However, I do not know how to test this in my component. My first assumption was to mock exclusively the client.readQuery to return me an array from my __fixtures__. That didn't work.
Here is my best shot to mock it:
it('filters the name in the search', () => {
jest.doMock('#/ApolloClient', () => ({
readQuery: jest.fn()
}))
const client = require('#/ApolloClient')
client.readQuery.mockImplementation(()=> Promise.resolve(names()))
const { getByPlaceholderText, queryByText } = renderIndexNames()
const input = getByPlaceholderText('Search…')
fireEvent.change(input, { target: { value: 'Second Name' } })
expect(queryByText('Title for a name')).not.toBeInTheDocument()
})
For this test I am using react-testing-library. This is my best shot so far...
jest.doMock('#/ApolloClient', () => ({
readQuery: jest.fn()
}))
const client = require('#/ApolloClient')
client.readQuery.mockImplementation(()=> Promise.resolve(names()))
It is important that relies on a single IT, not on the top since there are other tests that are using the client and works properly.
With That I could had mocked in the test when invokin client.readQuery() but then in the component itself goes back to its own original state
My goal
Find a way that the client.readQuery can be mocked and return the data I am looking for. I thought in mocking, but any other solution that can work for a single or group of tests( without all of them) would be more than welcome.
I tried as well mocking on the top, but then the others are failing and i couldn't reproduce to go back to the real implementation

Resources