I followed the Cloud Functions recommendation and created my unit testing in online mode, everything makes sense to me, but I am getting inconsistency when I am debugging. I just trying to reset the firestore state by resetting and seeding the database.
When I run my test suite separately, they work perfectly; if I run all the test suites together appear all the errors, I am pretty sure that this is related to the beforeEach hooks but idk how to fix them.
I have 3 test suites, I will share with you 1 of them and the function responsible of the reset of the firestore.
// eslint-disable-next-line spaced-comment
/// <reference types="jest" />
import { cleanup, getDb, initializeFirebase, makeChange, resetDb, wrap } from './util';
initializeFirebase();
import { streamerIsOffline, streamerIsOnline } from './__fixtures__/onStreamerUpdateSnaps';
import { getScheduler, SchedulerClientWrapper } from './../utils/SchedulerClientWrapper';
import onStreamerUpdate from '../onStreamerUpdate';
const db = getDb();
jest.mock('./../utils/SchedulerClientWrapper');
const mockedGetScheduler = jest.mocked(getScheduler, true);
const wrapped = wrap(onStreamerUpdate);
const schedulerClientWrapper = new SchedulerClientWrapper();
const mockedPause: jest.Mock = jest.fn();
const mockedResume: jest.Mock = jest.fn();
const mockedJobIsEnabled: jest.Mock = jest.fn();
schedulerClientWrapper.pause = mockedPause;
schedulerClientWrapper.resume = mockedResume;
schedulerClientWrapper.jobIsEnabled = mockedJobIsEnabled;
describe('onStreamerUpdate', () => {
beforeEach(
async () => {
await resetDb(); //I am resetting firestore here
mockedGetScheduler.mockClear();
jest.resetAllMocks();
});
it('should resume the scheduledJob when the first streamer becomes online',
async () => {
await updateStreamerStatus('64522496', true); //I am preparing the setup here
const beforeSnap = streamerIsOffline;
const afterSnap = streamerIsOnline;
const change = makeChange(beforeSnap, afterSnap);
mockedGetScheduler.mockReturnValue(schedulerClientWrapper);
mockedJobIsEnabled.mockResolvedValue(false);
await wrapped(change);
expect(mockedJobIsEnabled).toBeCalled();
expect(mockedResume).toBeCalled();
expect(mockedPause).not.toBeCalled();
});
afterAll(() => {
cleanup();
});
});
const updateStreamerStatus = async (streamerId: string, isOnline: boolean) => {
const stremersRef = db.collection('streamers');
const query = stremersRef.where('broadcasterId', '==', streamerId);
const querySnapshot = await query.get();
console.log('streamers', (await stremersRef.get()).docs.map((doc) => doc.data())); //I am debugging here
const streamerDocId = querySnapshot.docs[0].id;
await stremersRef.doc(streamerDocId).update({ isOnline });
};
And you can find the functions that wipe and seed firestore below:
import firebaseFunctionsTest from 'firebase-functions-test';
import { getApp, getApps, initializeApp, cert } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
const projectId = 'latamqchallengetest';
export const { wrap, cleanup, firestore: firestoreTestData, makeChange } = firebaseFunctionsTest({
databaseURL: `https://${projectId}.firebaseio.com`,
storageBucket: `${projectId}.appspot.com`,
projectId: projectId,
}, './../google-credentials/latamqchallengetest-firebase-adminsdk.json');
export const initializeFirebase = () => {
if (getApps().length <= 0) {
initializeApp({
credential: cert('./../google-credentials/latamqchallengetest-firebase-adminsdk.json'),
});
}
return getApp();
};
export const getDb = () => {
initializeFirebase();
const db = getFirestore();
return db;
};
export const wipeDb = async () => {
const db = getDb();
const collections = await db.listCollections();
const deletePromises = collections.map((collectionRef) =>
db.recursiveDelete(collectionRef));
Promise.all([...deletePromises]);
};
export const seed = async () => {
const db = getDb();
await db.collection('streamers').add({
broadcasterId: '64522496',
broadcasterName: 'ITonyl',
color: '1565C0',
description: 'Sr. Truchita',
isOnline: false,
puuid: 'kRQyIDe5TfLhWtn8jXgo_4Zjlfg4rPypXWiPCXrkTMUsiQT3TYVCkVO6Au3QTdd4x-13CbluPA53dg',
summonerId: '9bnJOmLXDjX-sgPjn4ZN1_-f6m4Ojd2OWlNBzrdH0Xk2xw',
});
await db.collection('streamers').add({
broadcasterId: '176444069',
broadcasterName: 'Onokenn',
color: '424242',
description: 'El LVP Player',
isOnline: false,
puuid: 'Gbfj8FyB6OZewfXgAwvLGpkayA6xyevFfEW7UZdrzA6saKVTyntP4HxhBQFd_EIGa_P1xC9eVdy8sQ',
summonerId: 'pPWUsvk_67FuF7ky1waWDM_gho-3NYP4enWTtte6deR3CjxOGoCfoIjjNw',
});
};
export const resetDb = async () => {
await wipeDb();
await seed();
};
Sometimes I find that firestore has 4 records, other times it has 0 records, 2 records and so on .. I don't know why I await the promises before each test.
I expect to keep the same state before each test, can you help me please?
All the problem was the parallel behavior of jest, I just needed to run the tests sequentially with the param --runInBand or maxWorkers=1
Related
I am facing difficulty writing a unit test in jest for the code snippet below:
async addCronJob(props: IAddAndUpdateCronJobDetails) {
const {name, individualSchedule} = props;
const parsedCronTime = convertDateAndTimeToCron(
individualSchedule.timeOfRun,
individualSchedule.dateOfrun
)
const {jobType, dateOfRun, id, timeOfRun} = individualSchedule;
const newJob = new CronJob(
parsedCronTime,
async () => {
return this.sqsService.getSqsApproval({
//some properties
}).then(() => {
//some logic
})
},
null,
false,
'Asia/Singapore'
)
this.schedulerRegistry.addCronJob(name, newJob)
newJob.start()
}
And here is my unit test:
//at the top
jest.mock('cron', () => {
const mScheduleJob = {start: jest.fn(), stop: jest.fn()};
const mCronJob = jest.fn(() => mScheduleJob);
return {CronJob: mCronJob}
})
***************
describe('addCronJob', () => {
it('should add a new cron job', async (done) => {
const testFn = jest.fn();
const parsedCronTime = convertDateAndTimeToCron(
mockSchedule.timeOfRun,
mockSchedule.dateOfrun
)
const testCronJob = new CronJob(
parsedCronTime,
testFn,
null,
false,
'Asia/Singapore'
);
return dynamicCronService.addCron({//properties}).then(() => {
expect(CronJob).toHaveBeenCalledWith(//properties);
expect(testCronJob.start).toBeCalledTimes(1);
done()
})
})
})
The above test passes without error. However, it is unable to test for this block of async code within the cron job itself:
async () => {
return this.sqsService.getSqsApproval({
//some properties
}).then(() => {
//some logic
})
}
Anyone have an idea how to test the above block of code?
Thanks!
Probably late to the party, but I struggled with this myself and wanted to share my solution:
Method in service
async addCronJob(taskName: string, cronEx: string, onTickCallback: () => void | Promise<void>): Promise<void> {
const newJob = new CronJob(cronEx, onTickCallback);
this.schedulerRegistry.addCronJob(taskName, newJob);
newJob.start();
}
Test
it('should create cronJob', async () => {
await service.addCronJob(jobName, testCronExpression, callbackFunction);
expect(schedulerRegistryMock.addCronJob).toHaveBeenCalledWith(jobName, expect.any(CronJob));
jest.advanceTimersByTime(60 * 60 * 1000);
expect(callbackFunction).toHaveBeenCalled();
});
Instead of creating a test cronjob with a test function, I had to mock the actual function I'm expecting the cronjob to call on tick (in your case, that should be getSqsApproval I believe). Then I expected schedulerRegistry.addCronJob to be called with any CronJob, since I can't know the specific job. Creating a new job and expecting it here won't work.
Finally, I advanced the time by 1 hour because my testCronExpression was 0 * * * *. You should advance the time depending on the cron expression you use for testing.
Expecting the callbackFunction to have been called after the time passed (virtually) worked for me.
Hope this helps!
After much trial/error, searching here on SO, & flexing my Google Fu, throwing in the towel & asking for help.
TL/DR -
Trying to correctly mock node module, change internal method return types, and spy on ctor's & method calls within the node module.
My specific scenario is to test the Microsoft Azure Storage blob SDK #azure/storage-blob, but the questions aren't specific to this package. It's just a good example as 4 LOC's capture achieve a task (upload a file to a storage container) as 2-3 of those LOC's cover 4 scenarios. Here's my code that I want to test, with comments on WHAT exactly I want to test:
export async function saveImage(account: string, container: string, imageBuffer: Buffer, imageName: string): Promise<void> {
try {
// init storage client
// (1) want to spy on args passed into ctor
const blobServiceClient: BlobServiceClient = new BlobServiceClient(`https://${account}.blob.core.windows.net`, new DefaultAzureCredential());
// init container
// (2) same as #1
const containerClient: ContainerClient = await blobServiceClient.getContainerClient(container);
// init block blob client
// (3) same as #1 & #2
const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(imageName);
// save file
// (4) same as #1,2, & 3
// (5) manipulate returned value
// (6) throw cause method to internally throw error
await blockBlobClient.upload(imageBuffer, imageBuffer.length, { blobHTTPHeaders: { blobContentType: 'image/png' } });
return Promise.resolve();
} catch (err: Error) {
return Promise.reject(err);
}
}
I've setup a manual mock for the module in the ./__mocks/#azure/storage-blob.ts as follows:
const MockStorageBlob = jest.createMockFromModule('#azure/storage-blob');
/**
* Utility method throw exception in the `BlockBlobClient.upload()` method.
*/
(MockStorageBlob as any).__setBlockBlobUpload_toFail = () => {
(MockStorageBlob as any).BlobServiceClient = jest.fn().mockImplementation(() => {
return {
getContainerClient: jest.fn().mockReturnValue({
getBlockBlobClient: jest.fn().mockReturnValue({
upload: jest.fn().mockImplementation(() => {
throw new Error('synthetic error');
})
})
})
}
});
}
module.exports = MockStorageBlob;
In my test, I can successfully test for #6 above like this:
import {
BlockBlobClient,
BlockBlobUploadResponse
} from '#azure/storage-blob';
import { saveImageToCDN as functionUnderTest } from './saveImageToCDN';
// mock Azure Storage blob NPM package
jest.mock('#azure/storage-blob');
describe('check expected with failure', () => {
beforeEach((done) => {
// reset number of times things have been called
jest.clearAllMocks();
done();
});
test(`it calls 'trackException()' when upload throws exception`, async (done) => {
expect.assertions(1);
// setup test
require('#azure/storage-blob').__setBlockBlobUpload_toFail();
// run SUT
const imageBuffer = Buffer.from('test string');
functionUnderTest(imageBuffer, 'imageName.png')
.then(() => {
expect(new Error('should not reach this')).toBeUndefined();
})
.catch((err: Error) => {
expect(err).toBeDefined();
})
.finally(() => {
done();
});
});
});
... but I can't figure out the correct syntax to spy on the upload() method (#4), or any of the other things I'm trying to test for (#1-5). If it matters, using Jest v26 on Node v14.
Could the __setBlockBlobUpload_toFail function return references to the mock functions ?
That would give something like this :
const MockStorageBlob = jest.createMockFromModule('#azure/storage-blob');
/**
* Utility method throw exception in the `BlockBlobClient.upload()` method.
*/
(MockStorageBlob as any).__setBlockBlobUpload_toFail = () => {
const upload = jest.fn().mockImplementation(() => {
throw new Error('synthetic error');
});
const getBlockBlobClient = jest.fn().mockReturnValue({ upload });
const getContainerClient = jest.fn().mockReturnValue({ getBlockBlobClient });
const BlobServiceClient = jest.fn().mockImplementation(() => {
return {
getContainerClient
}
});
(MockStorageBlob as any).BlobServiceClient = BlobServiceClient;
return {
upload,
getBlockBlobClient,
getContainerClient,
BlobServiceClient
};
}
module.exports = MockStorageBlob;
And in your test you would retrieve them like :
// setup test
const mockFns = require('#azure/storage-blob').__setBlockBlobUpload_toFail();
// run SUT
const imageBuffer = Buffer.from('test string');
functionUnderTest(imageBuffer, 'imageName.png')
.then(() => {
expect(new Error('should not reach this')).toBeUndefined();
})
.catch((err: Error) => {
expect(mockFns.getBlockBlobClient.mock.calls[0][0]).toBe('imageName.png')
expect(err).toBeDefined();
})
.finally(() => {
done();
});
i have this bootstrap vue component:
<b-form-input
v-model="currentUser.name"
placeholder="Name *"
name="name"
#input="checkSubmitStatus()"
></b-form-input>
checkSubmitStatus in the methods goes to call updateSubmitDisabled which I have in the mutations inside another file:
methods: {
...mapMutations({
updateSubmitDisabled: "updateSubmitDisabled"
}),
checkSubmitStatus() {
const isDisabled = this.currentUser.name.length === 0;
this.updateSubmitDisabled(isDisabled);
}
}
this is the .spec.js file:
import { createLocalVue, mount } from "#vue/test-utils";
import Vue from "vue";
import Vuex from 'vuex';
import UserForm from "#/components/event-created/UserForm.vue";
import { BootstrapVue, BootstrapVueIcons } from "bootstrap-vue";
const localVue = createLocalVue();
localVue.use(BootstrapVue);
localVue.use(BootstrapVueIcons);
localVue.use(Vuex);
describe("UserForm.vue", () => {
let mutations;
let store;
beforeEach(() => {
mutations = {
updateSubmitDisabled: jest.fn()
};
store = new Vuex.Store({
state: {
currentUser: {
name: 'pippo',
}
},
mutations
});
})
it("should call the updateSubmitDisabled mutation", async () => {
const wrapper = mount(UserForm, { localVue, store });
const input = wrapper.get('input[name="name"]');
await Vue.nextTick();
input.element.value = 'Test';
await input.trigger('input');
await Vue.nextTick();
expect(mutations.updateSubmitDisabled).toHaveBeenCalled();
});
});
for now I just want to test if "updateSubmitDisabled" is called on "name" but as a result the test says:
Expected number of calls:> = 1
Received number of calls: 0
I finally settled with:
it("should call the updateSubmitDisabled mutation", () => {
const wrapper = mount(UserForm, { localVue, store });
const input = wrapper.get('input[name="name"]');
input.element.dispatchEvent(new Event('input'));
expect(mutations.updateSubmitDisabled).toHaveBeenCalled();
});
I am testing a react-native component in which imports a dependency (named import and async) which performs some logic and just returns a boolean but i think jest is not waiting for it to finish. which also logs an error.
"Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with --detectOpenHandles to troubleshoot this issue."
currently this is the implementation i have tried..
this is the code of the test
// OfflineNotice.test.js
import React from 'react';
import { OfflineNotice } from './OfflineNotice';
jest.mock('../../service/user', () => ({
__esModule: true,
checkIfConnectivityIsValid: () => (
Promise.resolve(true)
),
}));
describe('<OfflineNotice/> test suite', () => {
const mockOnNetworkConnected = jest.fn();
test('it should render <OfflineNotice/> component', () => {
const wrapper = shallow(
<OfflineNotice
onNetworkConnected={mockOnNetworkConnected}
network={{
isConnected: true,
connectionType: 'value',
}}
/>,
);
expect(wrapper).toBeDefined();
});
});
the code of the component that i was testing
// the dependency i need to mock
import { checkIfConnectivityIsValid } from '../../service/user';
// the implementation is as follows
export class OfflineNotice extends PureComponent {
componentWillMount() {
const { network } = this.props;
const { isConnected, connectionType } = network;
this.handleConnectivityChange(isConnected, connectionType);
}
componentDidUpdate() {
const { network } = this.props;
const { isConnected, connectionType } = network;
this.handleConnectivityChange(isConnected, connectionType);
}
handleConnectivityChange = async (isConnected, connectionType) => {
const { onNetworkConnected } = this.props;
// how the service was used only returns boolean
const isValid = await checkIfConnectivityIsValid(connectionType);
let status = null;
let message = null;
if (isConnected && isValid) {
status = 'online';
message = string.NETWORK_MESSAGE.AVAILABLE;
this.fadeOut();
onNetworkConnected();
} else if (isConnected) {
status = 'invalid';
message = string.NETWORK_MESSAGE.INVALID;
this.containerOpacity.setValue(1);
} else {
status = 'offline';
message = string.NETWORK_MESSAGE.NO_INTERNET;
this.containerOpacity.setValue(1);
}
this.setState({ status, message });
};
then running the test is able to render the component. though on code coverage the code stops on the "const isValid = await checkIfConnectivityIsValid(connectionType);" part in which it says that the statement onward is not covered.
Im trying to mock this mail function so I dont send mails everytime I test my code. But the mocking is not working. This code gives me the error: mockImplementation is not a function.
It's the add function that calls sendUserInvitationMail(). The mailer module export looks like this:
module.exports = {
sendUserInvitationMail,
};
this is the test code:
require('dotenv').config();
const { startWithCleanDb } = require('../../../utils/test.helpers');
const { add } = require('../invitation.service');
const { ADMIN_LEVELS, TABLES } = require('../../../constants');
const { AuthorizationError } = require('../../../errors');
const knex = require('../../../../db/connection');
const mailer = require('../../../mailer/index');
jest.mock('../../../mailer/index');
beforeEach(() => startWithCleanDb());
mailer.sendUserInvitationMail.mockImplementation(() => console.log('Mocked mail function called'));
mailer.sendUserInvitationMail();
describe('invitation.service', () => {
describe('add', () => {
it('adds an invitation to the db', async () => {
expect.assertions(2);
const result = await add(
{
email: 'tester#test.be',
badgeNumber: '344d33843',
},
{ currentZoneId: 1 },
ADMIN_LEVELS.ADMINISTRATOR,
);
const invitation = (await knex.select('*').from(TABLES.INVITATIONS))[0];
expect(invitation.id).toEqual(result.id);
expect(invitation.email).toEqual(result.email);
});
});
});
In mailer, sendUserInvitationMail is undefined, so it has no property mockImplementation.
Try:
mailer.sendUserInvitationMail = jest.fn().mockImplementation(() => console.log('Mocked mail function called'));
or
mailer.sendUserInvitationMail = jest.fn(() => console.log('Mocked mail function called'));