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

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

Related

Typescript: cannot load json file contents into array

I need load my apikeys from json file (api.json) with file structure
api.json:
{
"service1": ["apikey1", "apikey2"],
"service2": ["apikey1", "apikey2"],
}
I have a class that describes a key & value:
class ApiKey {
constructor(apiName: String, apiKeys: Array<String>)
}
And I have a class, that loads all keys from file to Array:
//read file async: https://stackoverflow.com/questions/46867517/how-to-read-file-with-async-await-properly
import { readFile } from 'fs'
import { promisify } from 'util'
import { join } from 'path'
export class ApikeysService {
constructor(private apiKeys: Array<ApiKey> = new Array<ApiKey>()) {
this.loadKeys()
}
public loadKeys () {
const filePath = join(__dirname, "../../../", "api.json");
const readfile = promisify(readFile);
const result = readfile(filePath, "utf8")
.then(content => JSON.parse(content))
.then(result => {
Object.keys(result).forEach(key => {
this.apikeys.push(new ApiKey(key, result[key]))
})
})
}
public getKeyFor(name: String) {
return this.keyCounters.find(x => x.keyname == name).apiKeys
}
}
And my tests:
describe('should load api keys', () => {
it("should load apikeys from file", async () => {
const service = new ApikeysService ()
it("should load api keys for service1", () => {
expect(service.getKeyFor("service1")[0]).toEqual("apikey1")
expect(service.getKeyFor("service1")[1]).toEqual("apikey2")
})
})
Test Result:
expect(received).toEqual(expected) // deep equality
> Expected: "apikey1"
> Received: undefined
I tried i lot of different ways to load contents from file to array in class (async also) but it wont work
You are firing an async function in your constructor, and checking the result directly afterwards - you give no chance for the promise to resolve. It is not synchronous.
Either return result from loadKeys and make sure to await it, or change it to become synchronous, using readFileSync.
Example 1
export class ApikeysService {
constructor(private apiKeys: Array<ApiKey> = new Array<ApiKey>()) {
this.loadKeys()
}
public loadKeys () {
const filePath = join(__dirname, "../../../", "api.json");
const readfile = promisify(readFile);
const content = JSON.parse(readFileSync(filePath, "utf8"));
Object.keys(result).forEach(key => {
this.apikeys.push(new ApiKey(key, result[key]))
})
}
public getKeyFor(name: String) {
return this.keyCounters.find(x => x.keyname == name).apiKeys
}
}
Example 2
export class ApikeysService {
constructor(private apiKeys: Array<ApiKey> = new Array<ApiKey>()) {
// this.loadKeys()
}
public loadKeys () {
const filePath = join(__dirname, "../../../", "api.json");
const readfile = promisify(readFile);
const result = readfile(filePath, "utf8")
.then(content => JSON.parse(content))
.then(result => {
Object.keys(result).forEach(key => {
this.apikeys.push(new ApiKey(key, result[key]))
})
})
return result;
}
public getKeyFor(name: String) {
return this.keyCounters.find(x => x.keyname == name).apiKeys
}
}
describe('should load api keys', () => {
it("should load apikeys from file", async () => {
const service = new ApikeysService ()
await service.loadKeys();
it("should load api keys for service1", () => {
expect(service.getKeyFor("service1")[0]).toEqual("apikey1")
expect(service.getKeyFor("service1")[1]).toEqual("apikey2")
})
})
})

Node/Apollo/GraphQL - advice on using async/await in an Apollo Server plugin

Any advice on using async/await in an apollo plugin? I'm attempting to await a twilio service promise and running into the Can not use keyword 'await' outside an async function babel parser error and unsure how to convert the parent function to be async. Here's the basic layout:
export const twilioVerification = async () => {
return {
requestDidStart () {
return {
willSendResponse ({ operationName, response, context }) {
if (['UpdateUser', 'CreateUser', 'SignInByPhone'].includes(operationName)) {
const user = response.data[operationName];
if (user != null) {
await sendVerificationText(user.phoneNumber);
}
}
}
}
},
}
};
The above code throws the BABEL_PARSE_ERROR. I've attempted a variety of ways to add async to willSendResponse and/or requestDidStart with mixed unsuccessful results. For reference, here's how I am instantiating ApolloServer as well:
const server = new ApolloServer({
context: { driver, neo4jDatabase: process.env.NEO4J_DATABASE },
schema: schema,
introspection: process.env.APOLLO_SERVER_INTROSPECTION,
playground: process.env.APOLLO_SERVER_PLAYGROUND,
plugins: [
pushNotifications(firebase),
twilioVerification(),
]
})
The function that isn't async is your function. Just add async on your willSendResponse. Here is one way to do that:
export const twilioVerification = async () => {
return {
requestDidStart () {
return {
willSendResponse: async ({ operationName, response, context }) => {
if (['UpdateUser', 'CreateUser', 'SignInByPhone'].includes(operationName)) {
const user = response.data[operationName];
if (user != null) {
await sendVerificationText(user.phoneNumber);
}
}
}
}
},
}
};

Nodejs Typescript Jest Unit Test Coverage shows some code to covered

This is my nodejs typescript class and written jest unit test for isHealthy() public method.
Test coverage shows that this.pingCheck() then block, catch and last return statement are not covered.
Please advise.
Can we do unit test for pingCheck private method ?
This my class
import { HttpService, Injectable } from '#nestjs/common';
import { DependencyUtlilizationService } from '../dependency-utlilization/dependency-utlilization.service';
import { ComponentType } from '../enums/component-type.enum';
import { HealthStatus } from '../enums/health-status.enum';
import { ComponentHealthCheckResult } from '../interfaces/component-health-check-result.interface';
import { ApiHealthCheckOptions } from './interfaces/api-health-check-options.interface';
#Injectable()
export class ApiHealthIndicator {
private healthIndicatorResponse: {
[key: string]: ComponentHealthCheckResult;
};
constructor(
private readonly httpService: HttpService,
private readonly dependencyUtilizationService: DependencyUtlilizationService,
) {
this.healthIndicatorResponse = {};
}
private async pingCheck(api: ApiHealthCheckOptions): Promise<boolean> {
let result = this.dependencyUtilizationService.isRecentlyUsed(api.key);
if (result) {
await this.httpService.request({ url: api.url }).subscribe(() => {
return true;
});
}
return false;
}
async isHealthy(
listOfAPIs: ApiHealthCheckOptions[],
): Promise<{ [key: string]: ComponentHealthCheckResult }> {
for (const api of listOfAPIs) {
const apiHealthStatus = {
status: HealthStatus.fail,
type: ComponentType.url,
componentId: api.key,
description: `Health Status of ${api.url} is: fail`,
time: Date.now(),
output: '',
links: {},
};
await this.pingCheck(api)
.then(response => {
apiHealthStatus.status = HealthStatus.pass;
apiHealthStatus.description = `Health Status of ${api.url} is: pass`;
this.healthIndicatorResponse[api.key] = apiHealthStatus;
})
.catch(rejected => {
this.healthIndicatorResponse[api.key] = apiHealthStatus;
});
}
return this.healthIndicatorResponse;
}
}
This is my unit test code.
I get the following error when I run npm run test
(node:7876) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'status' of undefined
(node:7876) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 6)
import { HttpService } from '#nestjs/common';
import { Test, TestingModule } from '#nestjs/testing';
import { DependencyUtlilizationService } from '../dependency-utlilization/dependency-utlilization.service';
import { ApiHealthIndicator } from './api-health-indicator';
import { ApiHealthCheckOptions } from './interfaces/api-health-check-options.interface';
import { HealthStatus } from '../enums/health-status.enum';
describe('ApiHealthIndicator', () => {
let apiHealthIndicator: ApiHealthIndicator;
let httpService: HttpService;
let dependencyUtlilizationService: DependencyUtlilizationService;
let dnsList: [{ key: 'domain_api'; url: 'http://localhost:3001' }];
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ApiHealthIndicator,
{
provide: HttpService,
useValue: new HttpService(),
},
{
provide: DependencyUtlilizationService,
useValue: new DependencyUtlilizationService(),
},
],
}).compile();
apiHealthIndicator = module.get<ApiHealthIndicator>(ApiHealthIndicator);
httpService = module.get<HttpService>(HttpService);
dependencyUtlilizationService = module.get<DependencyUtlilizationService>(
DependencyUtlilizationService,
);
});
it('should be defined', () => {
expect(apiHealthIndicator).toBeDefined();
});
it('isHealthy should return status as true when pingCheck return true', () => {
jest
.spyOn(dependencyUtlilizationService, 'isRecentlyUsed')
.mockReturnValue(true);
const result = apiHealthIndicator.isHealthy(dnsList);
result.then(response =>
expect(response['domain_api'].status).toBe(HealthStatus.pass),
);
});
it('isHealthy should return status as false when pingCheck return false', () => {
jest
.spyOn(dependencyUtlilizationService, 'isRecentlyUsed')
.mockReturnValue(false);
jest.spyOn(httpService, 'request').mockImplementation(config => {
throw new Error('could not call api');
});
const result = apiHealthIndicator.isHealthy(dnsList);
result
.then(response => {
expect(response['domain_api'].status).toBe(HealthStatus.fail);
})
.catch(reject => {
expect(reject['domain_api'].status).toBe(HealthStatus.fail);
});
});
});
Looks like you should define the status before initialize the unit test, try to grab some more logs using console.log and for the second test, added catch block to make sure you're grabing the failures

Unit test for customPollingHook which uses apollo useLazyQuery

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

Jest reset mock of a node module

I'm working on the Google Cloud Functions tests.
The files are these:
index.ts which only exports the functions which are also imported there.
if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'contactSupportByMail') {
exports.contactSupportByMail = require('./contactSupportByMail');
}
contactSupportByMail.ts the function to test.
And the test:
describe('Cloud Functions', (): void => {
let myFunctions;
let adminInitStub;
beforeAll((): void => {
// [START stubAdminInit]
// If index.js calls admin.initializeApp at the top of the file,
// we need to stub it out before requiring index.js. This is because the
// functions will be executed as a part of the require process.
// Here we stub admin.initializeApp to be a dummy function that doesn't do anything.
adminInitStub = sinon.stub(admin, 'initializeApp');
testEnv.mockConfig({
sendgrid: {
key: 'apiKey',
},
brand: {
support_email: 'supportEmail',
},
});
// [END stubAdminInit]
});
afterAll((): void => {
// Restore admin.initializeApp() to its original method.
adminInitStub.restore();
// Do other cleanup tasks.
process.env.FUNCTION_NAME = '';
myFunctions = undefined;
testEnv.cleanup();
});
describe('contactSupportByMail', (): void => {
// Mocking node_modules library before the require
jest.mock('#sendgrid/mail', (): { [key: string]: any } => ({
setApiKey: (): void => { },
send: (): Promise<any> => Promise.resolve('ok'),
}));
// Setting up cloud function name
process.env.FUNCTION_NAME = 'contactSupportByMail';
// Importing the index file
myFunctions = require('../src/index');
const wrapped = testEnv.wrap(myFunctions.contactSupportByMail);
it('it should export contactSupportByMail', (): void => {
const cFunction = require('../src/contactSupportByMail');
assert.isObject(myFunctions);
assert.include(myFunctions, { contactSupportByMail: cFunction });
});
it('should fully work', async (): Promise<void> => {
const onCallObjects: [any, ContextOptions] = [
{ mailBody: 'mailBody', to: 'toEmail' },
{ auth: { token: { email: 'userEmail' } } },
];
return assert.deepEqual(await wrapped(...onCallObjects), { ok: true });
});
it('not auth', async (): Promise<void> => {
await expect(wrapped(undefined)).rejects.toThrow('The function must be called while authenticated.');
});
it('sendgrid error', async (): Promise<void> => {
// Mocking node_modules library before the require
jest.mock('#sendgrid/mail', (): { [key: string]: any } => ({
setApiKey: (): void => { },
send: (): Promise<any> => Promise.reject('errorsengrid'),
}));
// Importing the index file
const a = require('../src/index');
const wrapped_2 = testEnv.wrap(a.contactSupportByMail);
const onCallObjects: [any, ContextOptions] = [
{ mailBody: 'mailBody', to: 'toEmail' },
{ auth: { token: { email: 'userEmail' } } },
];
await expect(wrapped_2(...onCallObjects)).rejects.toThrow('errorsengrid');
});
});
});
The problem is provoking the sendgrid error. I don't know how to reset the mock of sendgrid's library which is required inside contactSupportByMail. After mocking it for the first time, it always returns the send function as resolved.
Just a note - if using babel-jest, mock calls are hoisted to the top of the transpiled js... doMock allow you to mock in the before functions of a test.
This is one way to mock a module for some tests within a file - and restore it for the others:
describe("some tests", () => {
let subject;
describe("with mocks", () => {
beforeAll(() => {
jest.isolateModules(() => {
jest.doMock("some-lib", () => ({ someFn: jest.fn() }));
subject = require('./module-that-imports-some-lib');
});
});
// ... tests when some-lib is mocked
});
describe("without mocks - restoring mocked modules", () => {
beforeAll(() => {
jest.isolateModules(() => {
jest.unmock("some-lib");
subject = require('./module-that-imports-some-lib');
});
});
// ... tests when some-lib is NOT mocked
});
});
I finally got the solution:
afterEach((): void => {
jest.resetModules();
});

Resources