Description: My action has multiple dispatches. While testing that action in 'Jest', it's pointing to every dispatch.
Action Code Eg.:
const export myAction = () => (dispatch) => {
dispatch({payload: {data1: 'Data - 1'}, type: 'UPDATE_DATA_1'});
dispatch({payload: {data2: 'Data - 2'}, type: 'UPDATE_DATA_2'});
dispatch({payload: {data3: 'Data - 3'}, type: 'UPDATE_DATA_3'});
}
Test Code Eg.:
it('myAction()', () => {
const mockDispatch = jest.fn();
const expected = {payload: {data3: 'Data - 3'}, type: 'UPDATE_DATA_3'};
myAction()(mockDispatch);
expect(mockDispatch).toHaveBeenCalledWith(expected);
});
Issue: As dispatch gets called 3 times, it's failing if I try to test particular one.
Query: How to test individual dispatch or how to test three of them at a time?
A slicker way to assert the last dispatch would be to use .toHaveBeenLastCalledWith()
it('last dispatch is data3 payload, when myAction() is invoked', () => {
const mockDispatch = jest.fn();
myAction()(mockDispatch);
expect(mockDispatch).toHaveBeenLastCalledWith({
payload: {data3: 'Data - 3'},
type: 'UPDATE_DATA_3'
});
});
Or if you wanted to check all 3 dispatches use .toHaveBeenNthCalledWith():
it('dispatch all 3 payloads, when myAction() is invoked', () => {
const mockDispatch = jest.fn();
myAction()(mockDispatch);
expect(mockDispatch).toHaveBeenNthCalledWith(1, {
payload: {data1: 'Data - 1'},
type: 'UPDATE_DATA_1'
});
expect(mockDispatch).toHaveBeenNthCalledWith(2, {
payload: {data2: 'Data - 2'},
type: 'UPDATE_DATA_2'
});
expect(mockDispatch).toHaveBeenNthCalledWith(3, {
payload: {data3: 'Data - 3'},
type: 'UPDATE_DATA_3'
});
});
I found what I wanted
it('myAction()', () => {
const mockDispatch = jest.fn();
const expected = {payload: {data3: 'Data - 3'}, type: 'UPDATE_DATA_3'};
myAction()(mockDispatch);
expect(mockDispatch.mock.calls[2][0]).toEqual(expected);
});
Related
I struggle with this problem and I don't know why the cmd say that replace is undefined.
I researched a bit but couldn't find any reason why the script should fail. I console.log the whole flow of the code but everything gets passed.
I am currently really speechless and trying to avoid but I just can't continue.
plopfile.js
const componentPlop = require('./controllers/component/component.plopfile');
module.exports = (plop) => {
plop.setWelcomeMessage('Generators');
componentPlop(plop);
};
component.plopfile.js
const componentPlop = (plop) => {
plop.setGenerator('📦 Components', {
description:
'This area is responsible for creating, editing and managing 📦 Components',
prompts: [
{
name: 'componentName',
type: 'input',
message: 'Component name: ',
},
{
name: 'componentType',
type: 'list',
message: 'Component type: ',
choices: [
'animations',
'atoms',
'molecules',
'organismns',
'templates',
'layouts',
],
},
{
type: 'list',
name: 'componentTemplate',
message: 'Component Template',
default: 'none',
choices: [
{ name: 'Default', value: 'props' },
{ name: 'With Variants', value: 'variants' },
],
},
],
actions: (data) => {
let actions = [
{
type: 'add',
path: '../src/components/{{pascalCase name}}/index.ts',
templateFile: 'templates/ComponentIndex.ts.hbs',
},
{
type: 'add',
path: '../src/components/{{pascalCase name}}/{{pascalCase name}}.test.tsx',
templateFile: 'templates/test.ts.hbs',
},
];
if (data.componentTemplate === 'props') {
actions = actions.concat([
{
type: 'add',
path: '../src/components/{{pascalCase name}}/{{pascalCase name}}.tsx',
templateFile: 'templates/Component.ts.hbs',
},
{
type: 'add',
path: '../src/components/{{pascalCase name}}/{{pascalCase name}}.stories.tsx',
templateFile: 'templates/storiesWithProps.ts.hbs',
},
]);
}
if (data.componentTemplate === 'variants') {
actions = actions.concat([
{
type: 'add',
path: '../src/components/{{pascalCase name}}/{{pascalCase name}}.tsx',
templateFile: 'templates/ComponentWithVariants.ts.hbs',
},
{
type: 'add',
path: '../src/components/{{pascalCase name}}/{{pascalCase name}}.stories.tsx',
templateFile: 'templates/storiesWithVariants.ts.hbs',
},
]);
}
return actions;
},
});
};
module.exports = componentPlop;
Even in the first action does fail I showcase the first action template
Component.ts.hbs
import { FC } from 'react';
import tw, { styled, css, theme } from 'twin.macro';
export interface {{pascalCase name}}Props {
children: React.ReactNode;
}
const Styled{{pascalCase name}} = styled.div`
${tw` `}
`;
export const {{pascalCase name}}: FC<{{pascalCase name}}Props> = ({ children }) => {
return <Styled{{pascalCase name}}>{children}</Styled>;
};
The Error
✖ ++ Cannot read property 'replace' of undefined
✖ ++ ../src/components/{{pascalCase name}}/{{pascalCase name}}.test.tsx Aborted due to previous action failure
✖ ++ ../src/components/{{pascalCase name}}/{{pascalCase name}}.tsx Aborted due to previous action failure
✖ ++ ../src/components/{{pascalCase name}}/{{pascalCase name}}.stories.tsx Aborted due to previous action failure
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Best Regards
Knome
To achieve the processing you expect, use {{pascalCase componentName}} instead of {{camelCase name}}
write the value of the name option directly.
I'm sorry for my poor English.
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'm recording this to document the answer to a problem that took me several hours to solve. Scenario:
I'm using two mutation queries on a single component in React Apollo-Client. This is a component wrapped into a larger component to form a page. Something like this (this is not the actual code, but it should give the idea):
import { compose } from 'react-apollo';
// submitNewUser contains
// postedBy
// createdAt
// content
// submitRepository contains
// otherProp
const thisProps1 = {
name: 'mutation1',
props: ({ ownProps, mutation1 }) => ({
submit: ({ repoFullName, commentContent }) => mutation1({
variables: { repoFullName, commentContent },
optimisticResponse: {
__typename: 'Mutation',
submitNewUser: {
__typename: 'Comment',
postedBy: ownProps.currentUser,
content: commentContent,
},
},
}),
}),
};
const thisProps2 = {
name: 'mutation2',
props: ({ ownProps, mutation2 }) => ({
submit: ({ repoFullName, commentContent }) => mutation2({
variables: { repoFullName, commentContent },
optimisticResponse: {
__typename: 'Mutation',
submitRepository: {
__typename: 'Comment',
otherProp: 'foobar',
},
},
}),
}),
};
const ComponentWithMutations = compose(
graphql(submitNewUser, thisProps1),
graphql(submitRepository, thisProps2)
)(Component);
Whenever the optimistic response fires, only the second result is fed back to into the query-response in the outer component. In other words, the first query gives an 'undefined' response (but no error), while the second returns an object as expect.
Why??
The property "createdAt" is not included in the optimistic reply.
__typename: 'Comment',
postedBy: ownProps.currentUser,
content: commentContent,
Should be:
__typename: 'Comment',
postedBy: ownProps.currentUser,
createdAt: Date(),
content: commentContent,
A missing field in an optimistic reply will silently fail to return anything to any queries that call that data.
I'm new to unit testing Redux-Thunk async actions using Jest.
Here is my code:
export const functionA = (a, b) => (dispatch) => {
dispatch({ type: CONSTANT_A, payload: a });
dispatch({ type: CONSTANT_B, payload: b });
}
How can I test this function using Jest?
You have an example in the Redux docs: http://redux.js.org/docs/recipes/WritingTests.html#async-action-creators
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
describe('async actions', () => {
it('should dispatch actions of ConstantA and ConstantB', () => {
const expectedActions = [
{type: 'CONSTANT_A', payload: 'a'},
{type: 'CONSTANT_B', payload: 'b'}
]
const store = mockStore({ yourInitialState })
store.dispatch(functionA('a', 'b'))
expect(store.getActions()).toEqual(expectedActions)
})
})
Also, for more more convenience, you can use this module: https://www.npmjs.com/package/redux-thunk-tester
Example:
import React from 'react';
import {createStore, applyMiddleware, combineReducers} from 'redux';
import {reducer} from './example';
import ReduxThunkTester from 'redux-thunk-tester';
import thunk from 'redux-thunk';
const request = (ms) => new Promise((resolve) => {
setTimeout(() => resolve('success response'), ms);
});
const resultRequestAction = (value) => ({ type: SOME_BACKEND_REQUEST, payload: value });
const toggleLoadingAction = (value) => ({ type: TOGGLE_LOADING, payload: value });
const asyncThunkWithRequest = () => async (dispatch) => {
try {
dispatch(toggleLoadingAction(true));
const result = await request(200);
dispatch(resultRequestAction(result));
} finally {
dispatch(toggleLoadingAction(false));
}
};
const createMockStore = () => {
const reduxThunkTester = new ReduxThunkTester();
const store = createStore(
combineReducers({exampleSimple: reducer}),
applyMiddleware(
reduxThunkTester.createReduxThunkHistoryMiddleware(),
thunk
),
);
return {reduxThunkTester, store};
};
describe('Simple example.', () => {
test('Success request.', async () => {
const {store, reduxThunkTester: {getActionHistoryAsync, getActionHistoryStringifyAsync}} = createMockStore();
store.dispatch(asyncThunkWithRequest());
const actionHistory = await getActionHistoryAsync(); // need to wait async thunk (all inner dispatch)
expect(actionHistory).toEqual([
{type: 'TOGGLE_LOADING', payload: true},
{type: 'SOME_BACKEND_REQUEST', payload: 'success response'},
{type: 'TOGGLE_LOADING', payload: false},
]);
expect(store.getState().exampleSimple).toEqual({
loading: false,
result: 'success response'
});
console.log(await getActionHistoryStringifyAsync({withColor: true}));
});
});
I recommend you to write something like this to avoid async problems:
return store
.dispatch(actionCreators.login({}))
.then(() => expect(store.getActions()).toEqual(expectedActions));
Trying to understand how do jasmine tests work.
I've got a module and a controller:
var app = angular.module('planApp', []);
app.controller('PlanCtrl', function($scope, plansStorage){
var plans = $scope.plans = plansStorage.get();
$scope.formHidden = true;
$scope.togglePlanForm = function() {
this.formHidden = !this.formHidden;
};
$scope.newPlan = {title: '', description: ''} ;
$scope.$watch('plans', function() {
plansStorage.put(plans);
}, true);
$scope.addPlan = function() {
var newPlan = {
title: $scope.newPlan.title.trim(),
description: $scope.newPlan.description
};
if (!newPlan.title.length || !newPlan.description.length) {
return;
}
plans.push({
title: newPlan.title,
description: newPlan.description
});
$scope.newPlan = {title: '', description: ''};
$scope.formHidden = true;
};
});
plansStorage.get() is a method of a service that gets a json string from localstorage and returns an object.
When I run this test:
var storedPlans = [
{
title: 'Good plan',
description: 'Do something right'
},
{
title: 'Bad plan',
description: 'Do something wrong'
}
];
describe('plan controller', function () {
var ctrl,
scope,
service;
beforeEach(angular.mock.module('planApp'));
beforeEach(angular.mock.inject(function($rootScope, $controller, plansStorage) {
scope = $rootScope.$new();
service = plansStorage;
spyOn(plansStorage, 'get').andReturn(storedPlans);
ctrl = $controller('PlanCtrl', {
$scope: scope,
plansStorage: service
});
spyOn(scope, 'addPlan')
}));
it('should get 2 stored plans', function(){
expect(scope.plans).toBeUndefined;
expect(service.get).toHaveBeenCalled();
expect(scope.plans).toEqual([
{
title: 'Good plan',
description: 'Do something right'
},
{
title: 'Bad plan',
description: 'Do something wrong'
}
]);
});
it('should add a plan', function() {
scope.newPlan = {title: 'new', description: 'plan'};
expect(scope.newPlan).toEqual({title: 'new', description: 'plan'});
scope.addPlan();
expect(scope.addPlan).toHaveBeenCalled();
expect(scope.plans.length).toEqual(3);
});
});
first test passes ok, but second one fails. The length of the scope.plans expected to be 3, but it is 2. scope.plans didn't change after scope.addPlan() call.
If I understand that right, the $scope inside addPlan method is not the same as scope that I trying to test in second test.
The question is why? And how do I test the addPlan method?
the solution is just to add andCallThrough() method after spy:
spyOn(scope, 'addPlan').andCallThrough()