how to test reducers with jest - jestjs

I have this reducer:
import { fromJS } from 'immutable';
import { DEFAULT_ACTION, SOURCES_LOADED, SOURCES_REQUEST } from './constants';
export const initialState = fromJS({
sources: null,
loading: false
});
function appReducer(state = initialState, action) {
switch (action.type) {
case SOURCES_REQUEST:
return state
.set('loading', true);
case SOURCES_LOADED:
return state
.set('sources', action.payload.sources)
.set('loading', false);
case DEFAULT_ACTION:
return state;
default:
return state;
}
}
export default appReducer;
and this tests:
import { fromJS } from 'immutable';
import reducer from '../reducer';
import * as types from '../constants';
describe('application reducers', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(fromJS(
{
sources: null,
loading: false
}
));
});
it('should handle the sources request', () => {
expect(reducer({ loading: true }, {
type: types.SOURCES_REQUEST
})).toEqual(fromJS({ loading: true }));
});
});
the second test is failing:
TypeError: state.set is not a function
11 | case SOURCES_REQUEST:
12 | return state
> 13 | .set('loading', true);
| ^
14 | case SOURCES_LOADED:
15 | return state
16 | .set('sources', action.payload.sources)
How can I add the test to these reducers, since this is redux sagas and I am following this https://redux.js.org/recipes/writing-tests due to it is the documentation I found which is closer to my needs.

Your reducer expects the state it receives to be an immutable object.
But in your second test you pass it a plain javascript object, which does not have the .set method you try to call.
it('should handle the sources request', () => {
expect(reducer(fromJS({
loading: true
}), {
type: types.SOURCES_REQUEST
})).toEqual(fromJS({
loading: true
}));
});
or in this particular case you could pass it undefined and the reducer would use the initialState

Related

how to mock react-query useQuery in jest

I'm trying to mock out axios that is inside an async function that is being wrapped in useQuery:
import { useQuery, QueryKey } from 'react-query'
export const fetchWithAxios = async () => {
...
...
...
const response = await someAxiosCall()
...
return data
}
export const useFetchWithQuery = () => useQuery(key, fetchWithAxios, {
refetchInterval: false,
refetchOnReconnect: true,
refetchOnWindowFocus: true,
retry: 1,
})
and I want to use moxios
moxios.stubRequest('/some-url', {
status: 200,
response: fakeInputData,
})
useFetchWithQuery()
moxios.wait(function () {
done()
})
but I'm getting all sorts of issues with missing context, store, etc which I'm iterested in mocking out completely.
Don't mock useQuery, mock Axios!
The pattern you should follow in order to test your usages of useQuery should look something like this:
const fetchWithAxios = (axios, ...parameters) => {
const data = axios.someAxiosCall(parameters);
return data;
}
export const useFetchWithQuery = (...parameters) => {
const axios = useAxios();
return useQuery(key, fetchWithAxios(axios, ...parameters), {
// options
})
}
Where does useAxios come from? You need to write a context to pass an axios instance through the application.
This will allow your tests to look something like this in the end:
const { result, waitFor, waitForNextUpdate } = renderHook(() => useFetchWithQuery(..., {
wrapper: makeWrapper(withQueryClient, withAxios(mockedAxios)),
});
await waitFor(() => expect(result.current.isFetching).toBeFalsy());

Why does jest mockResolvedValueOnce called multiple times returns the same value?

I have a class method where I trigger a #google-cloud/firestore multiple times. I would like to mock the call over the same .get() method multiple times.
Using a mockResolvedValueOnce multiple times with different values to return, the 2nd value is ignored.
jest.doMock('#google-cloud/firestore', () => class {
collection () {
return {
get: jest.fn().mockResolvedValue({
docs: []
}),
doc: () => {
return {
set: jest.fn(),
get: jest.fn().mockResolvedValueOnce({})
}
},
limit: () => {
return {
get: jest.fn().mockResolvedValue({ empty: true })
}
},
onSnapshot: jest.fn(),
select: () => {
return {
get: jest.fn() // <------------ MULTIPLE CALLS CHAINED BELOW
.mockResolvedValueOnce({
size: 1
}).mockResolvedValueOnce({
size: 2
})
}
}
}
}
})
When I console.log(snapshot.size) it returns me the same value "1" twice for both calls.
if (isEmptyModels || isStatsEmptyModels) {
// ...
console.log('📊 [STATS][MODELS] - Fulfilling the counters')
await Database.collection('models').select('id').get().then(snapshot => {
console.log(snapshot.size) // <--------- 1st call
this.fields.models.count = snapshot.size
this.fields.models.linked = snapshot.size
})
// ...
}
if (isEmptyProducts1P || isStatsEmptyProducts1P) {
// ...
console.log('📊 [STATS][PRODUCTS1P] - Fulfilling the counters')
await Database.collection('products1P').select('isMaintained').get().then(snapshot => {
console.log(snapshot.size) // <--------- 2nd call
snapshot.forEach(doc => {
if (doc.data().isMaintained) {
// ...
}
})
// ...
})
// ...
}
Why is that, and what is done wrong here ?
Error message is:
console.log
📊 [STATS][MODELS] - Fulfilling the counters
at Statistics.fulfillProductsCount (app/services/statistics/index.js:95:15)
console.log
1
at app/services/statistics/index.js:97:17
console.log
📊 [STATS][PRODUCTS1P] - Fulfilling the counters
at Statistics.fulfillProductsCount (app/services/statistics/index.js:106:15)
console.log
1
at app/services/statistics/index.js:108:17
TypeError: snapshot.forEach is not a function
117 | await Database.collection('products1P').select('isMaintained').get().then(snapshot => {
118 | console.log(snapshot.size)
> 119 | snapshot.forEach(doc => {
| ^
120 | if (doc.data().isMaintained) {
121 | this.fields.products1P.maintained += 1
122 | } else {
at app/services/statistics/index.js:119:18
This happens because each time you call Database.collection(), it creates a new object, and as a new object, this is the first time its properties are called. It is also valid for the others functions inside collection.
What I mean is that Database.collection is a function that returns an object that contains other functions that return object that contains properties mocked. By mocking this way, you will never be able to use mock...ValueOnce. But, I see two ways to "bypass" this problem :
1 - The short but conflicting way
You can use .mockReturnThis() to avoid entering into deep mock objects/functions, but in may be quickly conflicting when dealing with "fat" classes that have multiple times the same method names. In can also be helpful when mocking chainable methods (example: ORM queries with .find().filter().sort()...).
jest.doMock('#google-cloud/firestore', () => class {
collection = jest.fn().mockReturnThis();
select = jest.fn().mockReturnThis();
get = jest.fn().mockResolvedValueOnce({ size: 1 }).mockResolvedValueOnce({ size: 2 });
})
2 - The Long but working way
Mock the whole collection method Once instead of mocking only collection().select().get().
Database.collection.prototype.mockReturnValueOnce({
select: () => {
get: () => ({ size: 1 })
}
}).mockReturnValueOnce({
select: () => {
get: () => ({ size: 2 })
}
})
--> You will need access to the mocked Class and mock the method "collection" of the prototype (collection = jest.fn()).

Why can't I mock `crypto.createHash`?

I'm trying to test a function which makes use of Node's crypto.createHash function. However, when I try to mock it using jest.mock('crypto', () => ({ createHash: … })), the mock doesn't seem to work.
The unit under test uses both Hash#update and Hash#digest, so I'm trying to add those to the createHash mock, but when the unit runs, createHash returns undefined.
src/hash.ts
import { createHash } from 'crypto';
export function hash(input: string | Uint8Array): Uint8Array {
return new Uint8Array(createHash('sha512').update(input).digest().buffer);
}
src/__test__/hash.test.ts
import { createHash } from 'crypto';
import { hash } from '../hash-node.js';
const mockDigest = jest.fn();
jest.mock('crypto', () => ({
createHash: jest.fn().mockReturnValue({
update: jest.fn().mockReturnThis(),
digest: mockDigest,
}),
}));
describe('hash', () => {
it('uses SHA512', () => {
hash('foo');
expect(createHash).toHaveBeenCalledWith(
expect.stringMatching(/sha512/i),
);
});
});
Output
TypeError: Cannot read properties of undefined (reading 'update')
3 |
4 | export function hash(input: string | Uint8Array): Uint8Array {
> 5 | return new Uint8Array(createHash('sha512').update(input).digest().buffer);
| ^
6 | }
7 |
at hash (src/hash.ts:4:25)
at Object.<anonymous> (src/__test__/hash.test.ts:16:5)
As you can see, createHash is clearly returning undefined, as though .mockReturnValue hadn't been called.
What am I doing wrong? How can I get this module mock to work?
I came back to this and was able to figure it out; it's a result of configuring Jest to reset mocks between tests. Once I moved the mockReturnValue call on createHash to inside a beforeEach block, it all started working as expected.
import { type Hash, createHash } from 'crypto';
import { hash } from '../hash.js';
const mockDigest = jest.fn();
jest.mock('crypto', () => ({
createHash: jest.fn(),
}));
describe('hash', () => {
beforeEach(() => {
(createHash as jest.MockedFunction<typeof createHash>).mockReturnValue({
update: jest.fn().mockReturnThis(),
digest: mockDigest,
} as unknown as Hash);
});
it('uses SHA512', () => {
hash('foo');
expect(createHash).toHaveBeenCalledWith(
expect.stringMatching(/sha512/i),
);
});
});

Jest doMock the same method multiple times

I would like to test following part of the code:
// ... code above
const created = async payload => {
const model = await db.collection('models').doc(payload.model)
.get() // <--- 1st .get() occurence
if (!model.exists) {
// Add product to the orphans collection
await db.collection('orphans').doc(payload.sku).set(payload)
} else {
// Grab the categories field
const categories = model.get('categories') // <--- 2nd .get() occurence
// Product is either empty or does not exists at all
if (!categories || categories.length < 1) {
// Add product to the orphans collection
await db.collection('orphans').doc(payload.sku).set(payload)
} else {
// Otherwise remove from the orphans collection
await deleted(payload.sku)
}
}
}
I do not know how to properly mock the file twice in the same callback. Here is what I get:
test.only('it should react when an event "created" has been fired', async () => {
const spy = jest.fn()
jest.doMock('#google-cloud/firestore', () => class {
collection () {
return {
doc: () => {
return {
get: () => {
return {
exists: () => {
spy()
}
}
},
set: () => {
spy()
}
}
}
}
}
})
const observer = require('./product')
await observer('created', {})
await expect(spy.mock.calls.length).toBe(1)
})
I get this error:
● it should react when an event "created" has been fired
TypeError: model.get is not a function
25 | } else {
26 | // Grab the categories field
> 27 | const categories = model.get('categories')
| ^
28 |
29 | // Product is either empty or does not exists at all
30 | if (!categories || categories.length < 1) {
at created (app/observers/product.js:27:30)
at Object.<anonymous>.module.exports (app/observers/product.js:6:28)
at Object.<anonymous> (app/observers/product.spec.js:34:3)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 skipped, 2 total
Snapshots: 0 total
Time: 0.147 s, estimated 1 s
Ran all test suites matching /app\/observers\/product.spec.js/i.
What is the working solution to test two scenarios of the same mocked get() method ?
In your code :
const model = await db.collection('models').doc(payload.model)
.get() // <--- 1st .get() occurence
If we look at your mock, the get method of doc returns :
{
exists: () => {
spy()
}
}
There are no property named get, so it is undefined (and not a function).
I guess you just have to change this part to :
{
exists: true, // can be false
get: spy,
}
And your problem should be solved.
Btw, you can also change the mock of set method to set: spy. Or you can keep it to set: () => { spy() }, but you should at least return the value if you want to mock it : set: () => { spy() }.
Now, about how to properly mock multiple times, here's what you can do :
const observer = require('./product')
const spyGet = jest.fn()
const spySet = jest.fn() // I like having different mocks, if one function use get & set, tests will be clever & more readable if you use different spies
describe('on event "created" fired', () => {
const categories = []
beforeEach(() => {
// I put mocks here to make test more readable
jest.doMock('#google-cloud/firestore', () => class {
collection () {
return {
doc: () => {
return {
get: () => {
return {
exists: true,
get: spyGet,
}
},
set: spySet
}
}
}
}
})
spyGet.mockResolvedValueOnce(categories) // you can also use mockResolvedValue, but mockResolvedValueOnce allow you to mock with different values on the same test & same mock
})
it.only('should get categories', async () => {
await observer('created', {})
// here's all the ways you can test it
expect(spyGet).toBeCalledTimes(1)
expect(spyGet.mock.calls.length).toBe(1)
expect(spyGet).toBeCalledWith('categories')
expect(spyGet).toHaveBeenNthCalledWith(1, 'categories')
})
})
Note : You should reset & clear your mocks between tests manually (in a afterEach or beforeEach) if you don't set it into jest config.

How to hook with useEffect/setState

I'm having trouble making the following test pass:
import { useEffect, useState } from "react";
export function useComponentResources(required) {
const [componentResources, setComponentResources] = useState(null);
useEffect(() => {
if (required) {
// api call
setTimeout(() => setComponentResources({}), 100);
}
}, [required]);
return componentResources;
}
import { renderHook } from "#testing-library/react-hooks";
import { useComponentResources } from "./component-resources.hook";
describe("component-resources.hook", () => {
it("fetches resources when required", () => {
//act
const { result } = renderHook(() => useComponentResources(true));
//assert
expect(result.current).toEqual({});
});
});
It keeps failing:
expect(received).toEqual(expected)
Expected value to equal:
{}
Received:
null
Difference:
Comparing two different types of values. Expected object but received null.
7 | const { result } = renderHook(() => useComponentResources(true));
9 | //assert
> 10 | expect(result.current).toEqual({});
11 | });
12 | });
I have created a repro case in codesandbox:
https://codesandbox.io/embed/priceless-browser-94ec2
renderHook doesn't wait for your setTimeout to fire; it can't know what 'side effects' your component has. So when your expect() runs, the current value is still its default - null.
We can force the test to wait until the hook updates again by using waitForNextUpdate, which is on the object renderHook returns. waitForNextUpdate is a function that returns a promise that resolves once the hook is updated again (e.g. when your setTimeout fires).
import { renderHook } from "#testing-library/react-hooks";
import { useComponentResources } from "./component-resources.hook";
describe("component-resources.hook", () => {
it("fetches resources when required", async () => {
const { result, waitForNextUpdate } = renderHook(() => useComponentResources(true));
await waitForNextUpdate();
expect(result.current).toEqual({});
});
});

Resources