I'm doing unit testing with jest and was able to successfully run some of it but there's certain code that I don't know how to test.
I have Create Organization method that needs to check first if the organization is already exist.
async createOrganization(opt) {
try {
const organizationExist = await this.OrganizationRepository.getOne({name: opt.name})
if (organizationExist) {
throw new Error('Organization already exist')
}
} catch (error) {
throw error
}
let organizationObject = {}
organizationObject.name = opt.name
return this.OrganizationRepository.save(organizationObject)
}
and so far this is the unit test code that I was able to cover
describe('Create Organization', () => {
it('should call getOne function', () => {
const mockGetOne = jest.spyOn(OrganizationRepository.prototype, 'getOne')
organizationService.createOrganization(expectedOrganization)
expect(mockGetOne).toHaveBeenCalledWith({name: 'sample org'})
})
it('should return created organization', async () => {
const mockSave = jest.spyOn(OrganizationRepository.prototype, 'save')
mockSave.mockReturnValue(Promise.resolve(expectedOrganization))
const result = await organizationService.createOrganization({name: 'sample org'})
expect(mockSave).toHaveBeenCalledWith({name: 'sample org'})
expect(result).toBe(expectedOrganization)
})
})
now what I want to test is this part
const organizationExist = await this.OrganizationRepository.getOne({name: opt.name})
if (organizationExist) {
throw new Error('Organization already exist')
}
I want to throw an error if the organization is already exist using the name parameter.
Hope you guys can help me. Thanks
you could use toThrowError to test this scenario.
it("Should throw error", async () => {
const mockGetOne = jest.spyOn(OrganizationRepository.prototype, 'getOne')
await organizationService.createOrganization({ name: 'sample org' }); ;
expect(mockGetOne).toHaveBeenCalledWith({ name: 'sample org' });
// Test the exact error message
expect( organizationService.createOrganization({ name: 'sample org' }))
.resolves
.toThrowError(new Error("Organization already exist"));
});
Are you looking for toThrow()?
expect(() => someFunctionCall()).toThrow();
Related
I have the following validation code at a Service Class:
order.items.map(async (item) => {
const itemResult = await this.itemRepository.getById(item.id)
if(itemResult == undefined && itemResult == null){
throw new NotFoundError('Item not found in database')
}
})
And I created the following test at Jest with an Order that doesn't have an Item on the Database:
it('Should validate a Order', async () => {
const item: Item = {
id: '2',
name: 'Item do not exist',
price: '20.0'
}
const order = {
id: uuidv4(),
amount: '100.0',
items: [item],
created_at: new Date()
} as Order
await expect(async () => {
orderService.createOrder(order)
}).toThrowError()
})
But when I run the Jest the test fails and the terminal is shown:
RUNS src/core/service/OrderService.spec.ts
/home/user/repo/projetc/src/core/service/OrderService.ts:1162
throw new NotFoundError_1.NotFoundError('Item not found in database');
^
NotFoundError: Item not found in database
at OrderService.<anonymous> (/home/user/repo/projetc/src/core/service/OrderService.ts:1162:19)
at Generator.next (<anonymous>)
at fulfilled (/home/user/repo/projetc/src/core/service/OrderService.ts:1058:24)
[Update]
After the hoangdv comment I changed the way that I'm validating for this:
const itemResult = await Promise.all(order.items.map(async (item) => {
return await this.itemRepository.getById(item.id)
}))
itemResult.forEach((item) => {
if(!item){
throw new NotFoundError('Item not found in database')
}
})
And changed the test to add rejects Jest property:
await expect(async () => {
await orderService.createOrder(order)
}).rejects.toThrow(NotFoundError)
I am facing the problem in mocking function which is being called from other function while writing the unit test cases in Node JS. I am using sinon chai and mocha to write the unit test case in my application. Below is my code
if (req.body.escalationDay === undefined) {
throw new Error('\'escalationDay\' attribute is required');
}
const escalationDay = req.body.escalationDay;
const invoiceId = req.params.id;
const lineItemNumber = req.params.lineItemNumber;
const invoice = await doGetInvoiceById(invoiceId, req);
if (!invoice) {
console.log("hi i am in")
throw new Error(`No invoice found with id ${invoiceId}`);
}
if (!invoice.InvoiceLineItems) {
throw new Error(`No line items found for invoice with id ${invoiceId}`);
}
const lineItem = invoice.InvoiceLineItems.find(li => +li.Id === +lineItemNumber);
if (!lineItem) {
throw new Error(`No line items with line item id ${lineItem.Id} found for invoice with id ${invoiceId}`);
}
}```
and below is the unit test case I am writing for this function
``` it('Should test escalation', () => {
let mockReq = {
params: {
id: '5f6bfc693b2253001a140cba',
lineItemNumber: 1
},
body: { escalationDay: 0 },
query: {
system: "test"
}
};
let sinonStub = sinon.stub(service(reqHandler, microservicesettings), 'doGetInvoiceById');
sinonStub.withArgs('5f6bfc693b2253001a140cba', mockReq).returns(
{ InvoiceLineItems : [] }
);
assert.equal(InvoiceService(reqHandler, microservicesettings).doInvoiceLineItemEscalation(mockReq)).to.be.equal( { InvoiceLineItems : [] });
})```
It will be good if I can get any idea what i am missing
I'm having trouble getting the AWS Secrets Manager module mocked for the jest unit tests... The part it errors on is the .promise(). When I remove that, the code doesn't work for the real Secrets Manager so I think it needs to stay there. How do I mock the getSecretData function so that getSecretData.promise() will work for the mock?
Here is the SecretsManager.js code:
import AWS from 'aws-sdk';
export class SecretsManager {
constructor() {
AWS.config.update({
region: 'us-east-1',
});
this.secretsManager = new AWS.SecretsManager();
}
async getSecretData(secretName) {
try {
const response = await this.secretsManager.getSecretValue({
SecretId: secretName,
}).promise();
const secretString = response.SecretString;
const parsedSecret = JSON.parse(secretString);
return parsedSecret;
} catch (e) {
console.log('Failed to get data from AWS Secrets Manager.');
console.log(e);
throw new Error('Unable to retrieve data.');
}
}
}
Here is the SecretsManager.test.js code:
import { SecretsManager } from '../utils/SecretsManager';
jest.mock('aws-sdk', () => {
return {
config: {
update(val) {
},
},
SecretsManager: function () {
return {
async getSecretValue({
SecretId: secretName
}) {
return {
promise: function () {
return {
UserName: 'test',
Password: 'password',
};
}
};
}
};
}
}
});
describe('SecretsManager.js', () => {
describe('Given I have a valid secret name', () => {
describe('When I send a request for test_creds', () => {
it('Then the correct data is returned.', async () => {
const mockReturnValue = {
UserName: 'test',
Password: 'password',
};
const logger = getLogger();
const secretManager = new SecretsManager();
const result = await secretManager.getSecretData('test_creds');
expect(result).toEqual(mockReturnValue)
});
});
describe('When I send a request without data', () => {
it('Then an error is thrown.', async () => {
const secretManager = new SecretsManager();
await expect(secretManager.getSecretData()).rejects.toThrow();
});
});
});
});
This is the error I get when running the tests:
this.secretsManager.getSecretValue(...).promise is not a function
Any suggestions or pointers are greatly appreciated!
Thank you for looking at my post.
I finally got it to work... figures it'd happen shortly after posting the question, but instead of deleting the post I'll share how I changed the mock to make it work incase it helps anyone else.
Note: This is just the updated mock, the tests are the same as in the question above.
// I added this because it's closer to how AWS returns data for real.
const mockSecretData = {
ARN: 'x',
Name: 'test_creds',
VersionId: 'x',
SecretString: '{"UserName":"test","Password":"password"}',
VersionStages: ['x'],
CreatedDate: 'x'
}
jest.mock('aws-sdk', () => {
return {
config: {
update(val) {
},
},
SecretsManager: function () {
return {
getSecretValue: function ( { SecretId } ) {
{
// Adding function above to getSecretValue: is what made the original ".promise() is not a function" error go away.
if (SecretId === 'test_creds') {
return {
promise: function () {
return mockSecretData;
}
};
} else {
throw new Error('mock error');
}
}
}
};
}
}});
I ran into this issue as well. There may be a more elegant way to handle this that also allows for greater control and assertion, but I haven't found one. Note that the in-test option may work better with newer versions of Jest.
I personally solved this issue by making use of manual mocks and a custom mock file for aws-sdk. In your case, it would look something like the following:
# app_root/__tests__/__mocks__/aws-sdk.js
const exampleResponse = {
ARN: 'x',
Name: 'test_creds',
VersionId: 'x',
SecretString: '{"UserName":"test","Password":"password"}',
VersionStages: ['x'],
CreatedDate: 'x'
};
const mockPromise = jest.fn().mockResolvedValue(exampleResponse);
const getSecretValue = jest.fn().mockReturnValue({ promise: mockPromise });
function SecretsManager() { this.getSecretValue = getSecretValue };
const AWS = { SecretsManager };
module.exports = AWS;
Then in your test file:
// ... imports
jest.mock('aws-sdk');
// ... your tests
So, in a nutshell:
Instead of mocking directly in your test file, you're handing mocking control to a mock file, which Jest knows to look for in the __mocks__ directory.
You create a mock constructor for the SecretsManager in the mock file
SecretsManager returns an instance with the mock function getSecretValue
getSecretValue returns a mock promise
the mock promise returns the exampleResponse
Bada boom, bada bing. You can read more here.
I ran into a same issue, I have tried to solve as below. It worked perfectly in my case.
Terminalsecret.ts
import AWS from 'aws-sdk';
AWS.config.update({
region: "us-east-1",
});
const client = new AWS.SecretsManager();
export class Secret {
constructor(){}
async getSecret(secretName: string) {
let secret: any;
const data = await client.getSecretValue({ SecretId: secretName).promise();
if ('SecretString' in data) {
secret = data.SecretString;
} else {
const buff = Buffer.alloc(data.SecretBinary as any, 'base64');
secret = buff.toString('ascii');
}
const secretParse = JSON.parse(secret);
return secretParse[secretName];
}
}
Terminalsecret.test.ts
import { SecretsManager as fakeSecretsManager } from 'aws-sdk';
import { Secret } from './terminalSecret';
jest.mock('aws-sdk');
const setup = () => {
const mockGetSecretValue = jest.fn();
fakeSecretsManager.prototype.getSecretValue = mockGetSecretValue;
return { mockGetSecretValue };
};
describe('success', () => {
it('should call getSecretValue with the argument', async () => {
const { mockGetSecretValue } = setup();
mockGetSecretValue.mockReturnValueOnce({
promise: async () => ({ SecretString: '{"userName": "go-me"}' })
});
const fakeName = 'userName';
const terminalSecretMock: TerminalSecret = new TerminalSecret()
terminalSecretMock.getTerminalSecret(fakeName);
expect(mockGetSecretValue).toHaveBeenCalledTimes(1);
});
});
I'm trying to write a jasmine test for an #Effect that has chained rxjs catchError operators, but am struggling to test any observables beyond the first catchError.
here's the effect:
#Effect() submitEndsheets$ = this.actions$.pipe(
ofType<SubmitEndSheets>(SpreadActionTypes.SUBMIT_ENDSHEETS),
withLatestFrom(this.store.pipe(select(fromAppStore.fromOrder.getDocumentId))),
concatMap(([action, documentId]) =>
this.spreadService.submitEndSheets(documentId).pipe(
map((response: ActionProcessorDto) => new SubmitEndSheetsSuccess(response.data)),
catchError((error) => of(undo(action))),
catchError((error) => of(new MessageModal({
message: error.message,
title: 'Submission Error!'
})
))
)
)
);
and the corresponding test:
it('handles errors by sending an undo action', () => {
const action = {
type: SpreadActionTypes.SUBMIT_ENDSHEETS,
};
const source = cold('a', { a: action });
const error = new Error('Error occurred!');
const service = createServiceStub(error);
const store = createStoreState();
const effects = new Effects(service, new Actions(source), store);
const expected = cold('ab', {
a: undo(action),
b: new MessageModal({
message: 'Sorry, something went wrong with your request. Please try again or contact support.',
title: 'Update Error!'
}),
});
expect(effects.submitEndsheets$).toBeObservable(expected);
});
for reference, here are the createServiceStub that mocks the service and createStoreState that, you guessed it, creates a mock store.
function createServiceStub(response: any) {
const service = jasmine.createSpyObj('spreadService', [
'load',
'update',
'updateSpreadPosition',
'submitEndSheets'
]);
const isError = response instanceof Error;
const serviceResponse = isError ? throwError(response) : of(response);
service.load.and.returnValue(serviceResponse);
service.update.and.returnValue(serviceResponse);
service.updateSpreadPosition.and.returnValue(serviceResponse);
service.submitEndSheets.and.returnValue(serviceResponse);
return service;
}
function createStoreState() {
const store = jasmine.createSpyObj('store', ['pipe']);
store.pipe.and.returnValue(of({ documentId: 123 }));
return store;
}
here's the test output:
FAILED TESTS:
✖ handles errors by sending an undo action
HeadlessChrome 0.0.0 (Mac OS X 10.14.2)
Expected $.length = 1 to equal 2.
Expected $[1] = undefined to equal Object({ frame: 10, notification: Notification({ kind: 'N', value: MessageModal({ payload: Object({ message: 'Sorry, something went wrong with your request. Please try again or contact support.', title: 'Update Error!' }), type: 'MESSAGE_MODAL' }), error: undefined, hasValue: true }) }).
at compare node_modules/jasmine-marbles/bundles/jasmine-marbles.umd.js:389:1)
at UserContext.<anonymous> src/app/book/store/spread/spread.effects.spec.ts:197:46)
at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
Thanks in advance for any help!
Update:
catchError can send an array of actions back out of the effect like so:
#Effect() submitEndsheets$ = this.actions$.pipe(
ofType<SubmitEndSheets>(SpreadActionTypes.SUBMIT_ENDSHEETS),
withLatestFrom(this.store.pipe(select(fromAppStore.fromOrder.getDocumentId))),
concatMap(([action, documentId]) =>
this.spreadService.submitEndSheets(documentId).pipe(
map((response: ActionProcessorDto) => new SubmitEndSheetsSuccess(response.data)),
catchError(error => [
new PopSingleToast({
severity: ToastSeverity.error,
summary: 'Failure',
detail: `Some error occurred: \n Error: ${error}`
}),
undo(action)
])
)
)
);
The corresponding test looks like so:
it('handles errors by sending an undo action', () => {
const action = {
type: SpreadActionTypes.SUBMIT_ENDSHEETS
};
const source = cold('a', { a: action });
const error = new Error('Error occurred!');
const service = createServiceStub(error);
const store = createStoreState();
const effects = new Effects(service, new Actions(source), store);
const expectedAction = new PopSingleToast({
severity: ToastSeverity.error,
summary: 'Failure',
detail: `Some error occurred: \n Error: ${error}`
});
const expected = cold('(ab)', {
a: expectedAction,
b: undo(action)
});
expect(effects.submitEndsheets$).toBeObservable(expected);
});
Thanks for to all for the help!
Having two catchErrors in a row like this means the second one will never trigger because the first one will eat the error.
You would need to rethrow the error in the first catchError to get inside the second one:
catchError(error => throw new Error()),
catchError(error => console.log('now I trigger'))
So I am afraid your questions doesn't make really sense.
After trying all manner of methods to test a route's mongoose save() throwing, I was not really sure how it should be done. I'm aiming for 100 % coverage with istanbul. Here's the core setup:
model.js
let mongoose = require('mongoose');
let Schema = mongoose.Schema;
let PasteSchema = new Schema(
{
message: { type: String, required: true },
tags: [String],
marked: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now },
updatedAt: Date
}
);
module.exports = mongoose.model('paste', PasteSchema);
controller.js
let Paste = require('./model');
// Other stuff
// I use a bit non-standard DELETE /pastes/:id for this
const markPaste = (req, res) => {
Paste.findById({ _id: req.params.id }, (err, paste) => {
if (!paste) {
res.status(404).json({ result: 'Paste not found' });
return;
}
paste.marked = true;
paste.updatedAt = new Date();
paste.save((err) => {
err
? res.status(400).json({ result: err })
: res.json({ result: 'Paste marked' });
});
});
}
module.exports = {
markPaste,
// Other stuff
}
routes.js
const express = require('express');
const app = express();
const pastes = require('./apps/pastes/controller'); // The file above
app.route('/pastes/:id')
.delete(pastes.markPaste);
module.exports = app;
In the below test, I want to simulate an error being thrown in the paste.save((err) => { above.
process.env.NODE_ENV = 'test';
let mongoose = require('mongoose');
let Paste = require('../apps/pastes/model');
let server = require('../index');
let chai = require('chai');
chai.use(require('chai-http'));
chai.use(require('chai-date-string'));
let expect = chai.expect;
let sinon = require('sinon');
let sandbox = sinon.createSandbox();
let pastes = require('../apps/pastes/controller');
let httpMocks = require('node-mocks-http');
// Other tests
Then the test I want to test save() error in the route:
it('should handle an error during the save in the endpoint', (done) => {
// Create a paste to be deleted
const pasteItem = new Paste({ message: 'Test 1', tags: ['integration', 'test'] });
pasteItem.save()
.then((paste) => {
// Attempt code from below goes here
})
.catch((err) => {
console.log('Should not go here');
});
done();
});
I didn't really find any clear reference to this in various Stack questions, or online, so here's how I did it:
The secret is in using the sinon sandbox, which applies even inside the route context during tests. Here is the working test:
it('should handle an error during the save in the endpoint', (done) => {
const pasteItem = new Paste({ message: 'Test 1', tags: ['integration', 'test'] });
pasteItem.save()
.then((paste) => {
// the sandbox is defined in the imports
// This forces calling save() to raise an error
sandbox.stub(mongoose.Model.prototype, 'save').yields({ error: 'MongoError' });
chai.request(server)
.delete('/pastes/' + paste._id)
.end((err, res) => {
// It applies within the route handling, so we get the expected 400
expect(res).to.have.status(400);
done();
});
})
.catch((err) => {
console.log('Should not go here');
});
});
If you would call it outside of the sandbox, you would break all subsequent tests that use sinon. Ie.
// This would break things unintendedly
sinon.stub(mongoose.Model.prototype, 'save').yields({ error: 'MongoError' });
// This only breaks things (on purpose) in the test we want it to break in:
sandbox.stub(mongoose.Model.prototype, 'save').yields({ error: 'MongoError' });
If you have multiple things within the particular sandbox instance, you can of course restore the "unbroken" state within the test with sandbox.restore(); after the test case.
->
=============================== Coverage summary ===============================
Statements : 100% ( 60/60 )
Branches : 100% ( 14/14 )
Functions : 100% ( 0/0 )
Lines : 100% ( 57/57 )
================================================================================
Yay!