Related
What I am looking to do:
spy on the method calls chained onto find() used in a static Model method definition
chained methods: sort(), limit(), skip()
Sample call
goal: to spy on the arguments passed to each of the methods in a static Model method definition:
... static method def
const results = await this.find({}).sort({}).limit().skip();
... static method def
what did find() receive as args: completed with findSpy
what did sort() receive as args: incomplete
what did limit() receive as args: incomplete
what did skip() receive as args: incomplete
What I have tried:
the mockingoose library but it is limited to just find()
I have been able to successfully mock the find() method itself but not the chained calls that come after it
const findSpy = jest.spyOn(models.ModelName, 'find');
researching for mocking chained method calls without success
I was not able to find a solution anywhere. Here is how I ended up solving this. YMMV and if you know of a better way please let me know!
To give some context this is part of a REST implementation of the Medium.com API I am working on as a side project.
How I mocked them
I had each chained method mocked and designed to return the Model mock object itself so that it could access the next method in the chain.
The last method in the chain (skip) was designed to return the result.
In the tests themselves I used the Jest mockImplementation() method to design its behavior for each test
All of these could then be spied on using expect(StoryMock.chainedMethod).toBeCalled[With]()
const StoryMock = {
getLatestStories, // to be tested
addPagination: jest.fn(), // already tested, can mock
find: jest.fn(() => StoryMock),
sort: jest.fn(() => StoryMock),
limit: jest.fn(() => StoryMock),
skip: jest.fn(() => []),
};
Static method definition to be tested
/**
* Gets the latest published stories
* - uses limit, currentPage pagination
* - sorted by descending order of publish date
* #param {object} paginationQuery pagination query string params
* #param {number} paginationQuery.limit [10] pagination limit
* #param {number} paginationQuery.currentPage [0] pagination current page
* #returns {object} { stories, pagination } paginated output using Story.addPagination
*/
async function getLatestStories(paginationQuery) {
const { limit = 10, currentPage = 0 } = paginationQuery;
// limit to max of 20 results per page
const limitBy = Math.min(limit, 20);
const skipBy = limitBy * currentPage;
const latestStories = await this
.find({ published: true, parent: null }) // only published stories
.sort({ publishedAt: -1 }) // publish date descending
.limit(limitBy)
.skip(skipBy);
const stories = await Promise.all(latestStories.map(story => story.toResponseShape()));
return this.addPagination({ output: { stories }, limit: limitBy, currentPage });
}
Full Jest tests to see implementation of the mock
const { mocks } = require('../../../../test-utils');
const { getLatestStories } = require('../story-static-queries');
const StoryMock = {
getLatestStories, // to be tested
addPagination: jest.fn(), // already tested, can mock
find: jest.fn(() => StoryMock),
sort: jest.fn(() => StoryMock),
limit: jest.fn(() => StoryMock),
skip: jest.fn(() => []),
};
const storyInstanceMock = (options) => Object.assign(
mocks.storyMock({ ...options }),
{ toResponseShape() { return this; } }, // already tested, can mock
);
describe('Story static query methods', () => {
describe('getLatestStories(): gets the latest published stories', () => {
const stories = Array(20).fill().map(() => storyInstanceMock({}));
describe('no query pagination params: uses default values for limit and currentPage', () => {
const defaultLimit = 10;
const defaultCurrentPage = 0;
const expectedStories = stories.slice(0, defaultLimit);
// define the return value at end of query chain
StoryMock.skip.mockImplementation(() => expectedStories);
// spy on the Story instance toResponseShape() to ensure it is called
const storyToResponseShapeSpy = jest.spyOn(stories[0], 'toResponseShape');
beforeAll(() => StoryMock.getLatestStories({}));
afterAll(() => jest.clearAllMocks());
test('calls find() for only published stories: { published: true, parent: null }', () => {
expect(StoryMock.find).toHaveBeenCalledWith({ published: true, parent: null });
});
test('calls sort() to sort in descending publishedAt order: { publishedAt: -1 }', () => {
expect(StoryMock.sort).toHaveBeenCalledWith({ publishedAt: -1 });
});
test(`calls limit() using default limit: ${defaultLimit}`, () => {
expect(StoryMock.limit).toHaveBeenCalledWith(defaultLimit);
});
test(`calls skip() using <default limit * default currentPage>: ${defaultLimit * defaultCurrentPage}`, () => {
expect(StoryMock.skip).toHaveBeenCalledWith(defaultLimit * defaultCurrentPage);
});
test('calls toResponseShape() on each Story instance found', () => {
expect(storyToResponseShapeSpy).toHaveBeenCalled();
});
test(`calls static addPagination() method with the first ${defaultLimit} stories result: { output: { stories }, limit: ${defaultLimit}, currentPage: ${defaultCurrentPage} }`, () => {
expect(StoryMock.addPagination).toHaveBeenCalledWith({
output: { stories: expectedStories },
limit: defaultLimit,
currentPage: defaultCurrentPage,
});
});
});
describe('with query pagination params', () => {
afterEach(() => jest.clearAllMocks());
test('executes the previously tested behavior using query param values: { limit: 5, currentPage: 2 }', async () => {
const limit = 5;
const currentPage = 2;
const storyToResponseShapeSpy = jest.spyOn(stories[0], 'toResponseShape');
const expectedStories = stories.slice(0, limit);
StoryMock.skip.mockImplementation(() => expectedStories);
await StoryMock.getLatestStories({ limit, currentPage });
expect(StoryMock.find).toHaveBeenCalledWith({ published: true, parent: null });
expect(StoryMock.sort).toHaveBeenCalledWith({ publishedAt: -1 });
expect(StoryMock.limit).toHaveBeenCalledWith(limit);
expect(StoryMock.skip).toHaveBeenCalledWith(limit * currentPage);
expect(storyToResponseShapeSpy).toHaveBeenCalled();
expect(StoryMock.addPagination).toHaveBeenCalledWith({
limit,
currentPage,
output: { stories: expectedStories },
});
});
test('limit value of 500 passed: enforces maximum value of 20 instead', async () => {
const limit = 500;
const maxLimit = 20;
const currentPage = 2;
StoryMock.skip.mockImplementation(() => stories.slice(0, maxLimit));
await StoryMock.getLatestStories({ limit, currentPage });
expect(StoryMock.limit).toHaveBeenCalledWith(maxLimit);
expect(StoryMock.addPagination).toHaveBeenCalledWith({
limit: maxLimit,
currentPage,
output: { stories: stories.slice(0, maxLimit) },
});
});
});
});
});
jest.spyOn(Post, "find").mockImplementationOnce(() => ({
sort: () => ({
limit: () => [{
id: '613712f7b7025984b080cea9',
text: 'Sample text'
}],
}),
}));
Here is how I did this with sinonjs for the call:
await MyMongooseSchema.find(q).skip(n).limit(m)
It might give you clues to do this with Jest:
sinon.stub(MyMongooseSchema, 'find').returns(
{
skip: (n) => {
return {
limit: (m) => {
return new Promise((
resolve, reject) => {
resolve(searchResults);
});
}
}
}
});
sinon.stub(MyMongooseSchema, 'count').resolves(searchResults.length);
This worked for me:
jest.mock("../../models", () => ({
Action: {
find: jest.fn(),
},
}));
Action.find.mockReturnValueOnce({
readConcern: jest.fn().mockResolvedValueOnce([
{ name: "Action Name" },
]),
});
All the above didn't work in my case, after some trial and error this worked for me:
const findSpy = jest.spyOn(tdataModel.find().sort({ _id: 1 }).skip(0).populate('fields'), 'limit')
NOTE: you need to mock the query, in my case I use NestJs:
I did the following:
find: jest.fn().mockImplementation(() => ({
sort: jest.fn().mockImplementation((...args) => ({
skip: jest.fn().mockImplementation((...arg) => ({
populate: jest.fn().mockImplementation((...arg) => ({
limit: jest.fn().mockImplementation((...arg) => telemetryDataStub),
})),
})),
})),
})),
findOne: jest.fn(),
updateOne: jest.fn(),
deleteOne: jest.fn(),
create: jest.fn(),
count: jest.fn().mockImplementation(() => AllTelemetryDataStub.length),
for me it worked like this:
AnyModel.find = jest.fn().mockImplementationOnce(() => ({
limit: jest.fn().mockImplementationOnce(() => ({
sort: jest.fn().mockResolvedValue(mock)
}))
}))
I have following test cases, both are passing when I expected the test to be marked as failing:
// testing.test.js
describe('test sandbox', () => {
it('asynchronous test failure', done => {
Promise.resolve()
.then(_ => {
// failure
expect(false).toBeTruthy();// test failed
done();// never get called
})
.catch(err => {// this catches the expectation failure and finishes the test
done(err);
});
});
it('asynchronous test success', done => {
Promise.resolve()
.then(_ => {
// failure
expect(true).toBeTruthy();
done();
})
.catch(err => {
console.log(err);
done(err);
});
});
});
Now the output looks as follows:
PASS src\tests\testing.test.js
test sandbox
√ asynchronous test failure (3ms)
√ asynchronous test success (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.884s
Ran all test suites matching /testing.test/.
console.log src\tests\testing.test.js:10
{ Error: expect(received).toBeTruthy()
Expected value to be truthy, instead received
false
at Promise.resolve.then._ (C:\dev\reactjs\OmniUI\src\tests\testing.test.js:6:21)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7) matcherResult: { message: [Function], pass: false } }
This is how it started, when I've encountered this for the first time, and it stopped testing any other tests with error,
// sandbox test
describe('test sandbox', () => {
it('asynchronous test failure', done => {
Promise.resolve()
.then(_ => {
// failure
expect(false).toBeTruthy();
done();// expected this to be called, but no
})
});
it('asynchronous test success', done => {
Promise.resolve()
.then(_ => {
// failure
expect(true).toBeTruthy();
done();
})
});
});
will stop the test suite which is not good as well as I want a full test suite done with a results of x failed:
RUNS src/tests/testing.test.js
C:\dev\reactjs\OmniUI\node_modules\react-scripts\scripts\test.js:20
throw err;
^
Error: expect(received).toBeTruthy()
Expected value to be truthy, instead received
false
at Promise.resolve.then._ (C:\dev\reactjs\OmniUI\src\tests\testing.test.js:6:21)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
and in Jest console log:
{
"resource": "/C:/dev/.../src/tests/testing.test.js",
"owner": "Jest",
"code": "undefined",
"severity": 8,
"message": "Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.",
"source": "Jest",
"startLineNumber": 9,
"startColumn": 1,
"endLineNumber": 9,
"endColumn": 6
}
and here is
// real life example
// actions.js with axios call
export const startTransactionStarted = () => ({
type: 'START_CART_TRANSACTION_STARTED',
});
export const startTransactionFinished = (payload, success) => ({
type: 'START_CART_TRANSACTION_FINISHED',
payload,
success,
});
export const startTransaction = () => (dispatch, getState) => {
const state = getState();
const {
transaction: { BasketId = undefined },
} = state;
if (BasketId !== undefined && BasketId !== null) {
// we already have BasketId carry on without dispatching transaction actions
return Promise.resolve();
}
dispatch(startTransactionStarted());
return Axios.post("https://test:181/api/Basket/StartTransaction", {})
.then(response => {
const {
data: { BusinessErrors, MessageHeader },
} = response;
if (BusinessErrors !== null) {
dispatch(startTransactionFinished(BusinessErrors, false));
return Promise.reject(BusinessErrors);
}
dispatch(startTransactionFinished(MessageHeader, true));
return Promise.resolve(response.data);
})
.catch(error => {
// console.log("Axios.post().catch()", error);
dispatch(startTransactionFinished([error], false));
/* eslint-disable prefer-promise-reject-errors */
return Promise.reject([error]);
});
};
// actions.test.js
import Axios from 'axios';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import moxios from 'moxios';
import {
startTransactionStarted,
startTransaction,
startTransactionFinished
} from 'actions';
const createMockStore = configureMockStore([thunk]);
describe('actions.js', () => {
it('test 404 response', done => {
// create mocked store with least amount of data (only those that are needed by tested code)
const store = createMockStore({
transaction: {
BasketId: null,
},
});
// jest mocked function - we are using it to test that dispatch action worked
const onFulfilled = jest.fn();
const onErrorCalled = jest.fn();
const error = { message: '404 - Error' };
const expectedResponse = [error];
// single test with mock adapter (for axios)
moxios.withMock(() => {
// dispatch action with axios call, pass mocked response handler
store
.dispatch(startTransaction())
.then(onFulfilled)
.catch(onErrorCalled);
// wait for request
moxios.wait(() => {
// pick most recent request which then we will "mock"
const request = moxios.requests.mostRecent();
console.log('moxios.wait()');
request
.respondWith({
status: 404,
error,
})
.then(() => {
console.log('- then()');
expect(onFulfilled).not.toHaveBeenCalled();
expect(onErrorCalled).toHaveBeenCalled();
const actions = store.getActions();
console.log(actions);
expect(actions).toEqual([
startTransactionStarted(),
startTransactionFinished(expectedResponse, false),
]);// this fails
done();// this is never called
})
});
});
});
});
test stops with this error
RUNS src/tests/testing.test.js
C:\dev\reactjs\OmniUI\node_modules\react-scripts\scripts\test.js:20
throw err;
^
Error: expect(received).toEqual(expected)
Expected value to equal:
[{"type": "START_CART_TRANSACTION_STARTED"}, {"payload": [{"message": "404 - Error"}], "success": false, "type": "START_CART_TRANSACTION_FINISHED"}]
Received:
[{"type": "START_CART_TRANSACTION_STARTED"}, {"payload": [[Error: Request failed with status code 404]], "success": false, "type": "START_CART_TRANSACTION_FINISHED"}]
Difference:
- Expected
+ Received
## -2,13 +2,11 ##
Object {
"type": "START_CART_TRANSACTION_STARTED",
},
Object {
"payload": Array [
- Object {
- "message": "404 - Error",
- },
+ [Error: Request failed with status code 404],
],
"success": false,
"type": "START_CART_TRANSACTION_FINISHED",
},
]
at request.respondWith.then (C:\dev\reactjs\OmniUI\src\tests\testing.test.js:101:27)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Is there a way for the asynchronous tests either (first example) to pass done but notify Jest that it failed? or setup jest to dont stop further tests on timeout?
You can call done with a parameter telling Jest the test has failed:
done(error);
I am trying to perform unit test on vuex actions, using Mocha and Sinon
here is my action.spec.js
import actions from '#/vuex/actions'
import * as types from '#/vuex/mutation_types'
describe('actions.js', () => {
var server, store, lists, successPut, successPost, successDelete
successDelete = {'delete': true}
successPost = {'post': true}
successPut = {'put': true}
beforeEach(() => {
// mock shopping lists
lists = [{
id: '1',
title: 'Groceries'
}, {
id: '2',
title: 'Clothes'
}]
// mock store commit and dispatch methods
store = {
commit: (method, data) => {},
dispatch: () => {
return Promise.resolve()
},
state: {
shoppinglists: lists
}
}
sinon.stub(store, 'commit')
// mock server
server = sinon.fakeServer.create()
server.respondWith('GET', /shoppinglists/, xhr => {
xhr.respond(200, {'Content-Type': 'application/json'}, JSON.stringify(lists))
})
server.respondWith('POST', /shoppinglists/, xhr => {
xhr.respond(200, {'Content-Type': 'application/json'}, JSON.stringify(successPost))
})
server.respondWith('PUT', /shoppinglists/, xhr => {
xhr.respond(200, {'Content-Type': 'application/json'}, JSON.stringify(successPut))
})
server.respondWith('DELETE', /shoppinglists/, xhr => {
xhr.respond(200, {'Content-Type': 'application/json'}, JSON.stringify(successDelete))
})
server.autoRespond = true
})
afterEach(() => {
// restore stubs and server mock
store.commit.restore()
server.restore()
})
describe('populateShoppingLists', () => {
it('should call commit method with POPULATE_SHOPPING_LIST string parameter', done => {
actions.populateShoppingLists(store).then(() => {
expect(store.commit).to.have.been.calledWith(types.POPULATE_SHOPPING_LISTS, lists)
done()
}).catch(done)
})
})
describe('changeTitle', () => {
it('should call commit method with CHANGE_TITLE string', (done) => {
let title = 'new title'
actions.changeTitle(store, {title: title, id: '1'}).then(() => {
expect(store.commit).to.have.been.calledWith(types.CHANGE_TITLE, {title: title, id: '1'})
done()
}).catch(done)
})
})
describe('updateList', () => {
it('should return successful PUT response', (done) => {
actions.updateList(store, '1').then((data) => {
expect(data.data).to.eql(successPut)
done()
}).catch(done)
})
})
describe('createShoppingList', () => {
it('should return successful POST response', (done) => {
let newList = { title: 'new list', id: '3' }
actions.createShoppingList(store, newList).then((testResponse) => {
console.log('testResponse: ', testResponse)
expect(testResponse.body).to.eql(successPost)
done()
}).catch(done)
})
})
})
here is my action.js
import { CHANGE_TITLE, POPULATE_SHOPPING_LISTS } from './mutation_types'
import api from '../api'
import getters from './getters'
export default {
populateShoppingLists: ({ commit }) => {
return api.fetchShoppingLists().then(response => {
commit(POPULATE_SHOPPING_LISTS, response.data)
})
},
changeTitle: (store, data) => {
store.commit(CHANGE_TITLE, data)
return store.dispatch('updateList', data.id)
},
updateList: (store, id) => {
let shoppingList = getters.getListById(store.state, id)
return api.updateShoppingList(shoppingList)
},
createShoppingList: (store, shoppinglist) => {
return api.addNewShoppingList(shoppinglist).then((actionResponse) => {
console.log('actionResponse: ', actionResponse)
store.dispatch('populateShoppingLists')
})
},
}
running my unit tests , I have an issue with the createShoppingList test
console.log
actions.js
populateShoppingLists
✓ should call commit method with POPULATE_SHOPPING_LIST string parameter
changeTitle
✓ should call commit method with CHANGE_TITLE string
updateList
✓ should return successful PUT response
LOG LOG: 'actionResponse: ', Response{url: 'http://localhost:3000/shoppinglists', ok: true, status: 200, statusText: 'OK', headers: Headers{map: Object{Content-Type: ...}}, body: Object{post: true}, bodyText: '{"post":true}'}
LOG LOG: 'testResponse: ', undefined
createShoppingList
✗ should return successful POST response
undefined is not an object (evaluating 'testResponse.body')
webpack:///test/unit/specs/vuex/actions.spec.js:90:28 <- index.js:15508:28
webpack:///~/vue-resource/dist/vue-resource.es2015.js:151:0 <- index.js:17984:52
webpack:///~/vue/dist/vue.esm.js:701:0 <- index.js:3198:18
nextTickHandler#webpack:///~/vue/dist/vue.esm.js:648:0 <- index.js:3145:16
whicj indicates that in the createShoppingList action, the reponse is not sent back on the return, so expect(testResponse.body).to.eql(successPost) is not true...
what's wrong with my Promise handling in this case ?
thanks for feedback
You're on the right track - testResponse is undefined, because createShoppingList resolves with the return value of addNewShoppingList.then, which is unspecified, and defaults to undefined.
Should createShoppingList resolve with addNewShoppingList's response or the response from populateShoppingLists? If the former, return actionResponse from the handler:
return api.addNewShoppingList(shoppinglist).then((actionResponse) => {
store.dispatch('populateShoppingLists')
return actionResponse
});
As a side-note, because the actions you're testing are promises, you can get rid of done in your tests by returning the actions directly:
it('should call commit method with POPULATE_SHOPPING_LIST string parameter', () => {
// mocha will fail the test if the promise chain rejects or the expectation is not met
return actions.populateShoppingLists(store).then(() => {
expect(store.commit).to.have.been.calledWith(types.POPULATE_SHOPPING_LISTS, lists)
})
})
I want to write a test for Axios use Jest Framework. I'm using Redux.
Here is my function get-request of Axios
export const getRequest = a => dispatch => {
return axios
.get(a)
.then(function(response) {
dispatch({
type: FETCH_DATA,
payload: response.data
});
})
.catch(function(error) {
dispatch({ type: ERROR_DATA, payload: { status: error.response.status, statusText: error.response.statusText } });
});
};
thanks in advance :)
Here is the solution:
index.ts:
import axios from 'axios';
export const FETCH_DATA = 'FETCH_DATA';
export const ERROR_DATA = 'ERROR_DATA';
export const getRequest = a => dispatch => {
return axios
.get(a)
.then(response => {
dispatch({
type: FETCH_DATA,
payload: response.data
});
})
.catch(error => {
dispatch({ type: ERROR_DATA, payload: { status: error.response.status, statusText: error.response.statusText } });
});
};
index.spec.ts:
import axios from 'axios';
import { getRequest, FETCH_DATA, ERROR_DATA } from './';
describe('getRequest', () => {
const dispatch = jest.fn();
it('should get data and dispatch action correctly', async () => {
const axiosGetSpyOn = jest.spyOn(axios, 'get').mockResolvedValueOnce({ data: 'mocked data' });
await getRequest('jest')(dispatch);
expect(axiosGetSpyOn).toBeCalledWith('jest');
expect(dispatch).toBeCalledWith({ type: FETCH_DATA, payload: 'mocked data' });
axiosGetSpyOn.mockRestore();
});
it('should dispatch error', async () => {
const error = {
response: {
status: 400,
statusText: 'client error'
}
};
const axiosGetSpyOn = jest.spyOn(axios, 'get').mockRejectedValueOnce(error);
await getRequest('ts')(dispatch);
expect(axiosGetSpyOn).toBeCalledWith('ts');
expect(dispatch).toBeCalledWith({ type: ERROR_DATA, payload: error.response });
axiosGetSpyOn.mockRestore();
});
});
Unit test result and coverage:
PASS 45062447/index.spec.ts
getRequest
✓ should get data and dispatch action correctly (9ms)
✓ should dispatch error (2ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.551s, estimated 3s
Here is the code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/45062447
I'm trying to test the GET HTTP method from a requests module:
const get = (host, resource, options) => {
...
return new Promise((resolve, reject) => fetch(url, opts)
.then(response => {
if (response.status >= 400) {
reject({
message: `[API request error] response status: ${response.status}`,
status: response.status });
}
resolve(response.json());
})
.catch(error => reject(error)));
};
And here is how I tested the .then part:
it('Wrong request should return a 400 error ', (done) => {
let options = { <parameter>: <wrong value> };
let errorJsonResponse = {
message: '[API request error] response status: 400',
status: 400,
};
let result = {};
result = get(params.hosts.api, endPoints.PRODUCTS, options);
result
.then(function (data) {
should.fail();
done();
},
function (error) {
expect(error).to.not.be.null;
expect(error).to.not.be.undefined;
expect(error).to.be.json;
expect(error).to.be.jsonSchema(errorJsonResponse);
done();
}
);
});
However I didn't find a way to test the catch part (when it gives an error and the response status is not >= 400).
Any suggestions?
It would also help me solve the problem a simple example with another code that tests the catch part of a Promise.
I've ended up writing the following code in order to test the catch:
it('Should return an error with invalid protocol', (done) => {
const host = 'foo://<host>';
const errorMessage = 'only http(s) protocols are supported';
let result = {};
result = get(host, endPoints.PRODUCTS);
result
.then(
() => {
should.fail();
done();
},
(error) => {
expect(error).to.not.be.null;
expect(error).to.not.be.undefined;
expect(error.message).to.equal(errorMessage);
done();
}
);
});