How to hook with useEffect/setState - react-hooks-testing-library

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({});
});
});

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());

Setting up jest mocks - one way works the other doesn't

When setting up jest mocks for a class what does not work for me with an error of "_TextObj.TextObj is not a constructor" is
import { TextObj, } from "#entities/TextObj";
jest.mock('#entities/TextObj', () => {
return jest.fn().mockImplementation((config: TextObjConfig) => {
return { ...
}
});
});
According to https://jestjs.io/docs/es6-class-mocks#calling-jestmock-with-the-module-factory-parameter I had expected the first version to work too - or not?
however what works is
import { TextObj, } from "#entities/TextObj";
jest.mock('#entities/TextObj');
...
beforeAll(() => {
TextObj.mockImplementation((config: TextObjConfig) => {
return {
..
}
});
});
TextObj is a named export and you're trying to mock default export which is why it is throwing the error _TextObj.TextObj is not a constructor.
For mocking named export, you need to do following the changes i.e return an object that contains TestObj property:
import { TextObj, } from "#entities/TextObj";
jest.mock('#entities/TextObj', () => {
TestObj: jest.fn().mockImplementation((config: TextObjConfig) => {
return { ...
}
});
});

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.

When run test, TypeError: Cannot destructure property 'travelDatas' of '(0 , _GetTravelDatas.getTravelDatas)(...)' as it is undefined

When I run test, it show TypeError: Cannot destructure property 'travelDatas' of '(0 , _GetTravelDatas.getTravelDatas)(...)' as it is undefined.
As you see the screenshot: unit test
There isn't any console error or warning.
Could anyone help please
travelListTest.spec.js
import { mount, shallowMount } from '#vue/test-utils'
import TravelList from '../../src/components/TravelList.vue'
import { getTravelDatas } from '../../src/composables/GetTravelDatas'
import ElementPlus from 'element-plus'
const wrapper = shallowMount(TravelList, {
global: {
plugins: [ElementPlus]
}
})
jest.mock('../../src/composables/GetTravelDatas')
describe('TravelList Test', () => {
test('click more will call GoToTravelDetailPage', () => {
wrapper.vm.GoToTravelDetailPage = jest.fn()
console.log(wrapper.html())
wrapper.find('.el-button').trigger('click')
expect(wrapper.vm.GoToTravelDetailPage).toHaveBeenCalled()
})
})
TravelList.vue
.....
<script>
import { ref } from '#vue/reactivity';
import { useRouter } from "vue-router";
import { getTravelDatas } from '../composables/GetTravelDatas'
export default {
name: 'TravelList',
setup() {
const { travelDatas } = getTravelDatas();
const router = useRouter();
function GoToTravelDetailPage(acctractionId) {
router.push({ path: `/travelDetail/${acctractionId}` })
}
return { travelDatas, GoToTravelDetailPage };
},
};
</script>
GetTravelDatas.js
import axios from "axios";
import { ref } from '#vue/runtime-core';
export function getTravelDatas() {
const travelDatas = ref([])
axios.get('https://localhost:5001/MyTravel/GetTravelData')
.then((response) => {
if (!response.data.success) {
alert(response.data.errorMessage)
}else{
travelDatas.value = response.data.travelDetail
}
}).catch((error) => {
alert('Unexpected Error: ', error.message)
console.log(error)
});
return { travelDatas }
}
You are mocking the GetTravelDatas module with an auto-mock version by calling:
jest.mock('../../src/composables/GetTravelDatas')
That means that all the methods exported from that module will be mocked (the real code of the method will not be called) and the mocked version will return undefined when called.
In the code you are testing you then have:
const { travelDatas } = getTravelDatas();
Reading the travelDatas property from undefined is causing the error you are seeing.
You should mock the getTravelDatas method so that it returns the appropriate data. For example, returning an empty array would look like:
getTravelDatas.mockReturnValue([]);

Resources