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();
});
Related
How to change the implementation of a mock depending on the test.
this is the code I am trying to unit test.
open(url: string, target: string = '_self'): void {
const win = this.document.defaultView?.open(url, target);
win?.focus();
}
And below my unit test
const MockDocument = {
location: { replace: jest.fn() },
defaultView: undefined
};
const createService = createServiceFactory({
service: RedirectService,
providers: [
{ provide: DOCUMENT, useValue: MockDocument }
]
});
My strategy is to set defaultView to undefined for the first test. And inside the next test, I would change the implementation to contain the open function, like this
const defaultView = {
open: jest
.fn()
.mockImplementation((url: string, target: string = '_self') => {
url;
target;
return { focus: jest.fn().mockImplementation(() => {}) };
})
};
const MockDocument = {
location: { replace: jest.fn() },
defaultView: defaultView
};
How do I change the implementation depending on the test?
Thanks for helping
EDIT
it('should not call "open" if defaultView is not truthy', () => {
spectator.service.openFocus('foo');
expect(mockDocument.defaultView).not.toBeTruthy();
});
it('should call "open" if defaultView is truthy', () => {
//CHANGE THE IMPLEMENTATION HERE...
const spy = jest.spyOn(mockDocument.defaultView, 'open')
spectator.service.openFocus('foo');
expect(spy).toHaveBeenCalled();
});
You can set defaultView up as a variable and change the variable speed test.
let defaultView: any;
const MockDocument = {
location: { replace: jest.fn() },
defaultView};
beforeEach(() => {
...
});
it('should not call "open" if defaultView is not truthy', () => {
defaultView = undefined;
expect(mockDocument.defaultView).not.toBeTruthy();
});
it('should call "open" if defaultView is truthy', () => {
defaultView = {
open: jest
.fn()
.mockImplementation((url: string, target: string = '_self') => {
url;
target;
return { focus: jest.fn().mockImplementation(() => {}) };
})
};
const spy = jest.spyOn(mockDocument.defaultView, 'open')
expect(spy).toHaveBeenCalled();
});
I'm trying to write some unit tests of code that uses typeorm without hitting the DB.
And I'm using sinon for spy/stub/mock.
This is my function.
async updateDoingToFailedWithLock(queryRunner: QueryRunner): Promise<void> {
await queryRunner.manager
.getRepository(Report)
.createQueryBuilder("report")
.useTransaction(true)
.setLock("pessimistic_write")
.update(Report)
.set({ status: ReportStatus.FAILED })
.where(`(status = "doing")`)
.execute();
}
I already wrote a fake test to make sure execute() is called by using spy function.
But I want to test the params of these functions createQueryBuilder..., the sure the params are correct.
I took a look at sinon document and it seems like sinon support test params by this API: spy().withArgs(arg1, arg2...).
But I'm not sure how to spy my function correctly.
describe("updateDoingToFailedWithLock()", (): void => {
let sandbox: Sinon.SinonSandbox;
beforeEach(() => (sandbox = Sinon.createSandbox()));
afterEach(() => sandbox.restore);
it("should be success", async (): Promise<void> => {
const fakeManager = {
getRepository: () => {
return fakeManager;
},
createQueryBuilder: () => {
return fakeManager;
},
useTransaction: () => {
return fakeManager;
},
setLock: () => {
return fakeManager;
},
update: () => {
return fakeManager;
},
set: () => {
return fakeManager;
},
where: () => {
return fakeManager;
},
execute: () => {},
};
const fakeQueryRunner = {
manager: fakeManager,
};
const connection = new typeorm.Connection({ type: "mysql" });
const reportService = new ReportService();
sandbox.stub(connection, "createQueryRunner").callsFake((): any => {
return fakeQueryRunner;
});
const queryRunner = connection.createQueryRunner();
const spy = sandbox.spy(fakeManager, "execute");
reportService.updateDoingToFailedWithLock(queryRunner);
expect(spy.calledOnce).be.true;
});
});
Any help is welcome. Thanks in advance!
I saw your code and there's something can be improved:
Use returnsThis() to replace return fakeManager
Don't forget await when calling updateDoingToFailedWithLock
describe("updateDoingToFailedWithLock()", (): void => {
let sandbox: sinon.SinonSandbox;
beforeEach(() => (sandbox = sinon.createSandbox()));
afterEach(() => sandbox.restore);
it("should be success", async (): Promise<void> => {
// using returnsThis()
const fakeManager = {
getRepository: sandbox.stub().returnsThis(),
createQueryBuilder: sandbox.stub().returnsThis(),
useTransaction: sandbox.stub().returnsThis(),
setLock: sandbox.stub().returnsThis(),
update: sandbox.stub().returnsThis(),
set: sandbox.stub().returnsThis(),
where: sandbox.stub().returnsThis(),
execute: sandbox.stub().returnsThis(),
}
const fakeQueryRunner = {
manager: fakeManager,
};
const reportService = new ReportService();
// having await here is important
await reportService.updateDoingToFailedWithLock(fakeQueryRunner);
expect(fakeManager.execute.calledOnce).to.be.true;
expect(fakeManager.createQueryBuilder.calledWith('report')).to.be.true;
});
});
Hope it helps
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);
});
});
I am testing the following service:
#Injectable()
export class TripService {
private readonly logger = new Logger('TripService');
constructor(
#InjectRepository(TripEntity)
private tripRepository: Repository<TripEntity>
) {}
public async showTrip(clientId: string, tripId: string): Promise<Partial<TripEntity>> {
const trip = await this.tripRepository
.createQueryBuilder('trips')
.innerJoinAndSelect('trips.driver', 'driver', 'driver.clientId = :clientId', { clientId })
.where({ id: tripId })
.select([
'trips.id',
'trips.distance',
'trips.sourceAddress',
'trips.destinationAddress',
'trips.startTime',
'trips.endTime',
'trips.createdAt'
])
.getOne();
if (!trip) {
throw new HttpException('Trip not found', HttpStatus.NOT_FOUND);
}
return trip;
}
}
My repository mock:
export const repositoryMockFactory: () => MockType<Repository<any>> = jest.fn(() => ({
findOne: jest.fn(entity => entity),
findAndCount: jest.fn(entity => entity),
create: jest.fn(entity => entity),
save: jest.fn(entity => entity),
update: jest.fn(entity => entity),
delete: jest.fn(entity => entity),
createQueryBuilder: jest.fn(() => ({
delete: jest.fn().mockReturnThis(),
innerJoinAndSelect: jest.fn().mockReturnThis(),
innerJoin: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
execute: jest.fn().mockReturnThis(),
getOne: jest.fn().mockReturnThis(),
})),
}));
My tripService.spec.ts:
import { Test, TestingModule } from '#nestjs/testing';
import { TripService } from './trip.service';
import { MockType } from '../mock/mock.type';
import { Repository } from 'typeorm';
import { TripEntity } from './trip.entity';
import { getRepositoryToken } from '#nestjs/typeorm';
import { repositoryMockFactory } from '../mock/repositoryMock.factory';
import { DriverEntity } from '../driver/driver.entity';
import { plainToClass } from 'class-transformer';
describe('TripService', () => {
let service: TripService;
let tripRepositoryMock: MockType<Repository<TripEntity>>;
let driverRepositoryMock: MockType<Repository<DriverEntity>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
TripService,
{ provide: getRepositoryToken(DriverEntity), useFactory: repositoryMockFactory },
{ provide: getRepositoryToken(TripEntity), useFactory: repositoryMockFactory },
],
}).compile();
service = module.get<TripService>(TripService);
driverRepositoryMock = module.get(getRepositoryToken(DriverEntity));
tripRepositoryMock = module.get(getRepositoryToken(TripEntity));
});
it('should be defined', () => {
expect(service).toBeDefined();
expect(driverRepositoryMock).toBeDefined();
expect(tripRepositoryMock).toBeDefined();
});
describe('TripService.showTrip()', () => {
const trip: TripEntity = plainToClass(TripEntity, {
id: 'one',
distance: 123,
sourceAddress: 'one',
destinationAddress: 'one',
startTime: 'one',
endTime: 'one',
createdAt: 'one',
});
it('should show the trip is it exists', async () => {
tripRepositoryMock.createQueryBuilder.mockReturnValue(trip);
await expect(service.showTrip('one', 'one')).resolves.toEqual(trip);
});
});
});
I want to mock the call to the tripRepository.createQueryBuilder().innerJoinAndSelect().where().select().getOne();
First question, should I mock the chained calls here because I assume that it should already be tested in Typeorm.
Second, if I want to mock the parameters passed to each chained call and finally also mock the return value, how can I go about it?
I had a similar need and solved using the following approach.
This is the code I was trying to test. Pay attention to the createQueryBuilder and all the nested methods I called.
const reactions = await this.reactionEntity
.createQueryBuilder(TABLE_REACTIONS)
.select('reaction')
.addSelect('COUNT(1) as count')
.groupBy('content_id, source, reaction')
.where(`content_id = :contentId AND source = :source`, {
contentId,
source,
})
.getRawMany<GetContentReactionsResult>();
return reactions;
Now, take a look at the test I wrote that simulates the chained calls of the above methods.
it('should return the reactions that match the supplied parameters', async () => {
const PARAMS = { contentId: '1', source: 'anything' };
const FILTERED_REACTIONS = REACTIONS.filter(
r => r.contentId === PARAMS.contentId && r.source === PARAMS.source,
);
// Pay attention to this part. Here I created a createQueryBuilder
// const with all methods I call in the code above. Notice that I return
// the same `createQueryBuilder` in all the properties/methods it has
// except in the last one that is the one that return the data
// I want to check.
const createQueryBuilder: any = {
select: () => createQueryBuilder,
addSelect: () => createQueryBuilder,
groupBy: () => createQueryBuilder,
where: () => createQueryBuilder,
getRawMany: () => FILTERED_REACTIONS,
};
jest
.spyOn(reactionEntity, 'createQueryBuilder')
.mockImplementation(() => createQueryBuilder);
await expect(query.getContentReactions(PARAMS)).resolves.toEqual(
FILTERED_REACTIONS,
);
});
Guilherme's answer is totally right. I just wanted to offer a modified approach that might apply to more test cases, and in TypeScript. Instead of defining your chained calls as (), you can use a jest.fn, allowing you to make more assertions. e.g.,
/* eslint-disable #typescript-eslint/no-explicit-any */
const createQueryBuilder: any = {
select: jest.fn().mockImplementation(() => {
return createQueryBuilder
}),
addSelect: jest.fn().mockImplementation(() => {
return createQueryBuilder
}),
groupBy: jest.fn().mockImplementation(() => {
return createQueryBuilder
}),
where: jest.fn().mockImplementation(() => {
return createQueryBuilder
}),
getRawMany: jest
.fn()
.mockImplementationOnce(() => {
return FILTERED_REACTIONS
})
.mockImplementationOnce(() => {
return SOMETHING_ELSE
}),
}
/* run your code */
// then you can include an assertion like this:
expect(createQueryBuilder.groupBy).toHaveBeenCalledWith(`some group`)
The solution I found to work in my case was to
create a repository class, add your custom query to the class
#EntityRepository(User)
export class UserRepository extends Repository<User> {
async getStatus(id: string) {
const status = await this.createQueryBuilder()
.select('User.id')
.where('User.id = :id', { id })
.getRawOne();
return {status};
}
}
mock the new repository class using 'jest-mock-extended' and 'jest-when' dependencies. This way you only need to mock the UserRepository and not all it's nested queries.
Now you can define the behaviour of the repository to resolve a predefined object (in my case a Partial object).
// some file where I need to call getStatus() in a test
const userRepoMock = mock<UserRepository>()
// lines omitted
const user = {
status: open,
};
when(userRepoMock.getStatus).mockResolvedValue(user as User);
// assert status
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);
});