Unit test for customPollingHook which uses apollo useLazyQuery - node.js

So I have written a custom polling hook which uses useContext and useLazyQuery hooks. I want to write a unit test for this, which should cover its returned values state and side effect.
So far I have managed to do this much but I'm not so sure how to proceed ahead. Any tips?
export const useUploadActivityPolling = (
teId: TeIdType
): UploadActivityPollingResult => {
const { dispatch, uploadActivityId }: StoreContextType = useAppContext();
const [fetchActivityStatus, { error: UploadActivityError, data: UploadActivityData, stopPolling }] = useLazyQuery(
GET_UPLOAD_ACTIVITY,
{
pollInterval: 3000,
fetchPolicy: 'network-only',
variables: { teId, activityId: uploadActivityId },
}
);
useEffect(() => {
if (UploadActivityData) {
setUploadActivityId(
UploadActivityData.getUploadActivityStatus.activity_id,
dispatch
);
updateActivityStateAction(UploadActivityData.getExcelUploadActivityStatus.status, dispatch);
}
}, [UploadActivityData]);
return { fetchActivityStatus, stopPolling, UploadActivityError };
};
import React from 'react';
import { mount } from 'enzyme';
const TestCustomHook = ({ callback }) => {
callback();
return null;
};
export const testCustomHook = callback => {
mount(<TestCustomHook callback={callback} />);
};
describe('useUploadActivityPolling', () => {
let pollingResult;
const teId = 'some id';
beforeEach(() => {
testCustomHook(() => {
pollingResult = useUploadActivityPolling(teId);
});
});
test('should have an fetchActivityStatus function', () => {
expect(pollingResult.fetchActivityStatus).toBeInstanceOf(Function);
});
});

Related

Nuxt Vitest Mock useRoute in another module

I am trying to write a test for the following Nuxt 3 composable (useLinkToSlug).
import { computed } from 'vue';
export default function () {
const route = useRoute();
return computed(() => route?.params?.slug ? `/${route.params.slug}` : undefined);
}
To keep the code as lean as possible, I tried to mock the vue-router module and set the return of useRoute() manually.
My test looks like this:
import { vi, it, expect, describe } from 'vitest';
import useLinkToSlug from '~~/composables/useLinkToSlug';
describe('useLinkToSlug', () => {
it('should return link to slug', () => {
vi.mock('vue-router', () => ({
useRoute: () => ({ params: { slug: 'abc' } })
}));
const link = useLinkToSlug();
expect(link.value).toEqual('/abc');
});
it('should return null', () => {
vi.mock('vue-router', () => ({
useRoute: () => ({ params: { slug: undefined } })
}));
const link = useLinkToSlug();
expect(link.value).toBeNull();
});
});
The first one succeeds, but the later one fails, with:
AssertionError: expected '/abc' to be null
I don't get why and what to do, to make this work.
Using: Nuxt3 with Vitest
Got it now. Was so close. Solution:
Add import statement for useRoute to composable.
import { computed } from 'vue';
import { useRoute } from 'vue-router'; // added this import statement
export default function () {
const route = useRoute();
return computed(() => route?.params?.slug ? `/${route.params.slug}` : undefined);
}
Mock vue-router:
import { vi, it, expect, describe } from 'vitest';
import useLinkToSlug from '~~/composables/useLinkToSlug';
vi.mock('vue-router'); // mock the import
describe('useLinkToSlug', () => {
it('should return link to slug', () => {
const VueRouter = await import('vue-router');
VueRouter.useRoute.mockReturnValueOnce({
params: { slug: 'abc' }
});
const link = useLinkToSlug();
expect(link.value).toEqual('/abc');
});
it('should return null', () => {
const VueRouter = await import('vue-router');
VueRouter.useRoute.mockReturnValueOnce({
params: { slug: undefined }
});
const link = useLinkToSlug();
expect(link.value).toBeNull();
});
});

Tests haphazardly fail when using graphql-request and MSW

I am trying to set up tests to test a Remix loader and noticed that the request function from graphql-request haphazardly fails when using MSW. If I replace this and use a simple fetch for the requests the tests pass.
Is there any configuration I need to change? I have created a sample repo that demonstrates the bug. The bug gets worse the more tests you have that are using the same mocked request.
Sample repo: https://github.com/charklewis/sb9or3
Here is a summary of the code I am using:
//modules/database.server
import { GraphQLClient } from "graphql-request";
const client = new GraphQLClient("http://some-graphql-api.com/api/graphql", {});
export const fetchQuery = async (query: any, variables: any) => {
try {
const response = await client.request(query, variables || {});
return response;
} catch (error) {
return {};
}
};
//routes/index
import type { LoaderFunction } from "remix";
import { json } from "remix";
import { gql } from "graphql-request";
import { fetchQuery } from "~/modules/database.server";
export const loader: LoaderFunction = async () => {
const query = gql`
query MyQuery {
demoQuery {
value
}
}
`;
const response = await fetchQuery(query, {});
return json({ value: response.demoQuery.value });
};
//routes/__tests__/index
import { graphql } from "msw";
import { setupServer } from "msw/node";
import { loader } from "~/routes/index";
const createServer = (handlers: any[] = []) => {
const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: "bypass" }));
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
return server;
};
const createDemoQueryHandler = ({ value = true } = {}) => {
return graphql.query("MyQuery", (req, res, ctx) => {
return res(ctx.data({ demoQuery: { value } }));
});
};
createServer([createDemoQueryHandler()]);
test("the loader returns data (round 1)", async () => {
const response = await loader({
request: new Request("/", { method: "GET" }),
params: {},
context: {},
});
const data = await response.json();
expect(data.value).toBe(true);
});
My vitest configuration is:
/// <reference types="vitest" />
/// <reference types="vite/client" />
import { defineConfig } from "vite";
import react from "#vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
globals: true,
environment: "jsdom",
setupFiles: "./app/test-utils.ts",
testTimeout: 20000
}
});

How to reset global variable on custom hook

I need to reset global variable on custom hook when unit testing React component. I have read few tutorials and StackOverflow answers to this simple task, but without luck to implement it correctly.
The problem
userInfo is undefined in the first and second test but when runs the third test userInfo is defined then on useEffect doesn't change the value... So my question is how to reset userInfo for each test.
jest.resetModules // doesn't work
jest.isolateModules // doesn't work
My simplest possible setup for single test is as following:
My Environment
"jest": "^24.9.0",
My Hook
import {useState, useEffect} from "react";
// This variable is an object save user info
let userInfo = null;
export default (authService) => {
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (userInfo !== null || authService === null) {
return;
}
setLoading(true);
authService
?.getUser()
.then((response) => {
userInfo = {owners: [{...response, cost_center: response.costCenter || "N/A"}]};
})
.catch(() => {
setError({
title: "authService Error",
message: "Error getting user",
status: 500
});
})
.finally(() => setLoading(false));
}, [authService]);
return [userInfo, error, loading];
};
My Test
import {renderHook} from "#testing-library/react-hooks";
import * as sinon from "sinon";
import {getSpyOfUseOktaAuth} from "../../../__tests__/";
import {Info, InfoFromRequest, InfoWithNoCostCenter} from "../../../__tests__/";
describe("useGetUserInfo", () => {
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers();
jest.useFakeTimers();
});
afterAll(() => {
clock.restore();
});
it("should set the error value after the getUserInfo function throws an error", async () => {
const useGetUserInfo = require("../index").default;
const errorMessage = {
title: "authService Error",
message: "Error getting user from",
status: 500
};
const getAuthMock = getSpyOfUseAuth({
Auth: {
signOut: jest.fn(),
getUser: jest.fn(async () => {
throw new Error("Auth Error");
})
},
authState: {}
});
const {result, rerender, waitForNextUpdate} = renderHook(() =>
useGetUserInfo(getAuthMock.results.Auth)
);
rerender();
await waitForNextUpdate();
expect(result.current[1]).toEqual(errorMessage);
getAuthMock.instance.mockRestore();
});
it("should return the user info from after run the getUserInfo function", async () => {
const useGetUserInfo = require("../index").default;
let authService = null;
const {result, rerender, waitForNextUpdate} = renderHook(() => useGetOktaUserInfo(authService));
const getAuthMock = getSpyOfUseAuth({
Auth: {
signOut: jest.fn(),
getUser: jest.fn(async () => Info)
},
authState: {}
});
authService = getAuthMock.results.Auth;
rerender();
await waitForNextUpdate();
expect(result.current[0]).toEqual(InfoFromRequest);
getAuthMock.instance.mockRestore();
});
it("should set cost_center as in data as N/A if costCenter is not defined in user info ", async () => {
const useGetUserInfo = require("../index").default;
const getAuthMock = getSpyOfUseAuth({
Auth: {
signOut: jest.fn(),
getUser: jest.fn(async () => InfoWithNoCostCenter)
},
authState: {}
});
const {result, rerender} = renderHook(() => useGetUserInfo(getAuthMock.results.Auth));
rerender();
expect(result.current[0].owners[0].cost_center).toEqual("N/A");
getAuthMock.instance.mockRestore();
});
});
I would say that either you export the 'userInfo' variable from the hook and you set it to null manually before each test, or you treat 'userInfo' as a state variable just like 'error' and 'loading'
If you go for the first option, you will need to export by reference Node Modules - exporting a variable versus exporting functions that reference it?
For the second option, it would be something like this
import {useState, useEffect} from "react";
export default (authService) => {
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [userInfo, setUserInfo] = useState(null);
useEffect(() => {
if (userInfo !== null || authService === null) {
return;
}
setLoading(true);
authService
?.getUser()
.then((response) => {
setUserInfo({owners: [{...response, cost_center: response.costCenter || "N/A"}]});
})
.catch(() => {
setError({
title: "authService Error",
message: "Error getting user",
status: 500
});
})
.finally(() => setLoading(false));
}, [authService]);
return [userInfo, error, loading];
};
The solution I got is to separate each case into different files. As jest load each file in a different process, this will be enough

Jest - resetting value in module and reloading import

I'm writing a test in jest for a module which uses a constant from a different module.
I want to set a different value for it for every test case, but I don't seem to be able to do so.
The test file:
import { Request, Response } from 'express';
const activityConsumer = require('../../src/utils/activity.consumer');
const mockRequest = {
params: {
activityArn: 'activityArn'
}
} as Request;
const mockedJsonFunction = jest.fn();
const mockResponse: any = {
json: jest.fn(),
status: jest.fn().mockReturnValue({ json: mockedJsonFunction }),
} as Response;
let stopConsumerMock;
describe('consumer handler', () => {
beforeAll(() => {
stopConsumerMock = activityConsumer.stopConsumer = jest.fn().mockReturnValue(1);
});
beforeEach(() => {
jest.resetModules();
});
afterEach(() => {
stopConsumerMock.mockClear();
mockResponse.json.mockClear();
});
describe('stopConsumingHandler', () => {
it('Should return success true and not call stopConsumer when no consumer exists', () => {
activityConsumer.consumer = undefined;
const { stopConsumingHandler } = require ('../../src/handlers/consumer.handlers');
stopConsumingHandler(mockRequest, mockResponse);
expect(stopConsumerMock.mock.calls.length).toEqual(0);
expect(mockResponse.json.mock.calls.length).toEqual(1);
expect(mockResponse.json).toHaveBeenCalledWith({ success: true });
});
it('Should return success true and call stopConsumer when consumer exists', () => {
activityConsumer.consumer = true;
const { stopConsumingHandler } = require ('../../src/handlers/consumer.handlers');
stopConsumingHandler(mockRequest, mockResponse);
expect(stopConsumerMock.mock.calls.length).toEqual(1);
expect(mockResponse.json.mock.calls.length).toEqual(1);
expect(mockResponse.json).toHaveBeenCalledWith({ success: true });
});
});
});
I want to replace the value of activityConsumer.consumer and then reload the consumer.handlers module but the re-assignment and reload does not seem to have any effect.
Please advise on how can I write this test properly.
Try this way, using jest.mock to modify import value of activityConsumer
import { Request, Response } from 'express';
// const activityConsumer = require('../../src/utils/activity.consumer');
const mockRequest = {
params: {
activityArn: 'activityArn'
}
} as Request;
const mockedJsonFunction = jest.fn();
const mockResponse: any = {
json: jest.fn(),
status: jest.fn().mockReturnValue({ json: mockedJsonFunction }),
} as Response;
let stopConsumerMock;
describe('consumer handler', () => {
beforeAll(() => {
// stopConsumerMock = activityConsumer.stopConsumer = jest.fn().mockReturnValue(1);
stopConsumerMock = jest.fn().mockReturnValue(1);
});
beforeEach(() => {
jest.resetModules(); // important line
});
afterEach(() => {
stopConsumerMock.mockClear();
mockResponse.json.mockClear();
});
describe('stopConsumingHandler', () => {
it('Should return success true and not call stopConsumer when no consumer exists', () => {
// activityConsumer.consumer = undefined;
// mock by this way
jest.mock('../../src/utils/activity.consumer', () => ({
consumer: undefined,
stopConsumer: stopConsumerMock,
}));
const { stopConsumingHandler } = require('../../src/handlers/consumer.handlers');
stopConsumingHandler(mockRequest, mockResponse);
expect(stopConsumerMock.mock.calls.length).toEqual(0);
expect(mockResponse.json.mock.calls.length).toEqual(1);
expect(mockResponse.json).toHaveBeenCalledWith({ success: true });
});
it('Should return success true and call stopConsumer when consumer exists', () => {
// activityConsumer.consumer = true;
// mock by this way
jest.mock('../../src/utils/activity.consumer', () => ({
consumer: true, // mock value for consumer
stopConsumer: stopConsumerMock,
}));
const { stopConsumingHandler } = require('../../src/handlers/consumer.handlers');
stopConsumingHandler(mockRequest, mockResponse);
expect(stopConsumerMock.mock.calls.length).toEqual(1);
expect(mockResponse.json.mock.calls.length).toEqual(1);
expect(mockResponse.json).toHaveBeenCalledWith({ success: true });
});
});
});

Testing custom hook with react-hooks-testing-library throws an error

I am trying to test a simple hook that fetches some data using axios. However the test is throwing a TypeError: "Cannot read property 'fetchCompanies' of undefined". Here's my custom hook (the full repo is here):
import { useState, useEffect } from 'react';
import { Company } from '../../models';
import { CompanyService } from '../../services';
export const useCompanyList = (): {
loading: boolean;
error: any;
companies: Array<Company>;
} => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState();
const [companies, setCompanies] = useState<Array<Company>>([]);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const companies = await CompanyService.fetchCompanies();
// Sort by ticker
companies.sort((a, b) => {
if (a.ticker < b.ticker) return -1;
if (a.ticker > b.ticker) return 1;
return 0;
});
setCompanies(companies);
setLoading(false);
} catch (e) {
setError(e);
}
};
fetchData();
}, []);
return { loading, error, companies };
};
and here's my test:
import { renderHook } from 'react-hooks-testing-library';
import { useCompanyList } from './useCompanyList';
const companiesSorted = [
{
ticker: 'AAPL',
name: 'Apple Inc.'
},
...
];
jest.mock('../../services/CompanyService', () => {
const companiesUnsorted = [
{
ticker: 'MSFT',
name: 'Microsoft Corporation'
},
...
];
return {
fetchCompanies: () => companiesUnsorted
};
});
describe('useCompanyList', () => {
it('returns a sorted list of companies', () => {
const { result } = renderHook(() => useCompanyList());
expect(result.current.loading).toBe(true);
expect(result.current.error).toBeUndefined();
expect(result.current.companies).toEqual(companiesSorted);
});
});
Please help me understand how to use react-hooks-testing-library in this case.
Edit
This seems to be related to a Jest issue that was seemingly resolved. Please see https://github.com/facebook/jest/pull/3209.
The
TypeError: "Cannot read property 'fetchCompanies' of undefined"
is caused by the way you define the CompanyService service. In the code, you are exporting an object CompanyService with all the service methods. But in your test, you are mocking the CompanyService to return an object with the methods.
So, the mock should return a CompanyService object that is an object with all the methods:
jest.mock('../../services/CompanyService', () => {
const companiesUnsorted = [
{
ticker: 'MSFT',
name: 'Microsoft Corporation'
},
...
];
return {
CompanyService: {
fetchCompanies: () => companiesUnsorted
}
};
});
Now, once you solve this, you will find that you don't have the TypeError anymore but your test is not passing. That is because the code you are trying to test is asynchronous, but your test is not. So, immediately after you render your hook (through renderHook) result.current.companies will be an empty array.
You will have to wait for your promise to resolve. Fortunately, react-hooks-testing-library provides us a waitForNextUpdate function in order to wait for the next hook update. So, the final code for the test would look:
it('returns a sorted list of companies', async () => {
const { result, waitForNextUpdate } = renderHook(() => useCompanyList());
expect(result.current.loading).toBe(true);
expect(result.current.error).toBeUndefined();
expect(result.current.companies).toEqual([]);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeUndefined();
expect(result.current.companies).toEqual(companiesSorted);
});

Resources