For date validation, I used luxon for my app. For each function I have made unit test. My unit test passed locally but when I deployed my code in gitlab, Most of my test failed. The expected result is so different than received. I don't understand what's the wrong. This my CD/CI pipeline images. Image-1, image-2, Image-3 and there are more. Basically all my test failed
My all test passed in code-sandbox
These are my all functions:
import {
DateTime
} from 'luxon'
export const DEFAULT_DATE_FORMAT = 'd.M.yyyy'
export const SHORT_TIME_FORMAT = 'HH:mm'
export const ISO_DATE_FORMAT = 'yyyy-MM-dd'
export const DATE_TIME_FORMAT = 'd.M.yyyy HH:mm'
export const DATE_MONTH_FORMAT = 'd.M'
export const WEEKDAYS = [
'SUNDAY',
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
]
export const dateToStrings = (date: string | number | Date): DateTime =>
DateTime.fromISO(new Date(date).toISOString())
export const formatDateTime = (date: string | number | Date): string =>
dateToStrings(date).toFormat(DATE_TIME_FORMAT)
export const formatDateMonthYear = (date: string | number | Date): string =>
dateToStrings(date).toFormat(DEFAULT_DATE_FORMAT)
export const formatTime = (date: string | number | Date): string =>
dateToStrings(date).toFormat(SHORT_TIME_FORMAT)
export const NextDayFormatYearMonthDate = (date: string | number | Date): string =>
dateToStrings(date).plus({
days: 1
}).toFormat(ISO_DATE_FORMAT)
export const PreviousDayFormatYearMonthDate = (date: string | number | Date): string =>
dateToStrings(date).plus({
days: -1
}).toFormat(ISO_DATE_FORMAT)
export const dateDifference = (date: string | number | Date): number => {
const diff = dateToStrings(date).diff(DateTime.now(), 'days').toObject()
return Math.abs(Math.ceil(diff.days as number))
}
export const formatYearMonthDate = (date: Date | string | undefined): string => {
if (!date) {
// if date is undefined, return empty string
return ''
} else {
return dateToStrings(date).toFormat(ISO_DATE_FORMAT)
}
}
export const weekNumber = (date: string | Date): number =>
dateToStrings(date).startOf('week').weekNumber
export const nextWeek = (date: string | Date): string =>
dateToStrings(date).startOf('week').plus({
days: 7
}).toFormat(ISO_DATE_FORMAT)
export const previousWeek = (date: string | Date): string =>
dateToStrings(date).startOf('week').plus({
days: -7
}).toFormat(ISO_DATE_FORMAT)
export const firstDateOfWeek = (date: string | Date): string =>
dateToStrings(date).startOf('week').toFormat(ISO_DATE_FORMAT)
export const lastDateOfWeek = (date: string | Date): string =>
dateToStrings(date).startOf('week').plus({
days: 6
}).toFormat(ISO_DATE_FORMAT)
export const firstMondayOfTheWeekWithGMT = (date: Date): Date =>
dateToStrings(date).startOf('week').toJSDate()
export const formatDateMonth = (date: string | Date): string =>
dateToStrings(date).toFormat(DATE_MONTH_FORMAT)
export const shortDateString = (date: Date): string => {
const shortForm = dateToStrings(date).setLocale('fi').toFormat('EEE')
return shortForm.length > 1 ? `${shortForm.charAt(0).toUpperCase()}${shortForm.slice(1)}` : ''
}
export const hasSameDay = (date1: Date, date2: Date): boolean =>
dateToStrings(date1).hasSame(dateToStrings(date2), 'day')
export const isToday = (date: string | number | Date): boolean => {
return dateToStrings(date).toISODate() === DateTime.local().toISODate()
}
export const compareDays = (
date1: Date | string | number,
date2: Date | string | number,
): number => {
const compareWithDay = dateToStrings(date1).diff(dateToStrings(date2), ['days']).toObject()
return Math.abs(Math.ceil(compareWithDay.days as number))
}
This is my all tests
import {
formatDateTime,
formatDateMonthYear,
formatTime,
NextDayFormatYearMonthDate,
PreviousDayFormatYearMonthDate,
dateDifference,
formatYearMonthDate,
weekNumber,
nextWeek,
previousWeek,
firstDateOfWeek,
lastDateOfWeek,
formatDateMonth,
shortDateString,
compareDays,
DATE_MONTH_FORMAT,
DEFAULT_DATE_FORMAT,
SHORT_TIME_FORMAT,
ISO_DATE_FORMAT,
DATE_TIME_FORMAT,
} from 'utils/date'
import { DateTime } from 'luxon'
const toDateString = 'Mon Feb 07 2022 00:00:00 GMT+0200 (Eastern European Standard Time)'
const toISOString = '2018-05-01T13:44:48.708709Z'
const today = new Date()
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000)
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
describe('formatDateTime', () => {
it('Should return format date time when date is string', () => {
expect(formatDateTime('2022-02-11T06:44:57+00:00')).toBe('7.2.2022 00:00')
})
it('Should return format date time when date is toISOString', () => {
expect(formatDateTime(toISOString)).toBe('1.5.2018 16:44')
})
it('Should return format date time when date is today', () => {
expect(formatDateTime(today)).toBe(
DateTime.fromISO(new Date().toISOString()).toFormat(DATE_TIME_FORMAT),
)
})
})
describe('formatDateMonthYear', () => {
it('Should return format date when date is string', () => {
expect(formatDateMonthYear(toDateString)).toBe('7.2.2022')
})
it('Should return format date when date is ISO String', () => {
expect(formatDateMonthYear(toISOString)).toBe('1.5.2018')
})
it('Should return format date when date is today', () => {
expect(formatDateMonthYear(today)).toBe(
DateTime.fromISO(new Date().toISOString()).toFormat(DEFAULT_DATE_FORMAT),
)
})
})
describe('formatTime', () => {
it('Should return 00:00 when there is no time', () => {
expect(formatTime(toDateString)).toBe('00:00')
})
it('Should return format time', () => {
expect(formatTime(toISOString)).toBe('16:44')
})
it('Should return format time when date is today', () => {
expect(formatTime(today)).toBe(
DateTime.fromISO(new Date().toISOString()).toFormat(SHORT_TIME_FORMAT),
)
})
})
describe('NextDayFormatYearMonthDate ', () => {
it('Should return next day format year when date is string', () => {
expect(NextDayFormatYearMonthDate(toDateString)).toBe('2022-02-08')
})
it('Should return next day format year when date is ISOString', () => {
expect(NextDayFormatYearMonthDate(toISOString)).toBe('2018-05-02')
})
it('Should return next day format year when date is today', () => {
expect(NextDayFormatYearMonthDate(today)).toBe(
DateTime.fromISO(new Date().toISOString()).plus({ days: 1 }).toFormat(ISO_DATE_FORMAT),
)
})
})
describe('PreviousDayFormatYearMonthDate', () => {
it('Should return next day format year when date is string', () => {
expect(PreviousDayFormatYearMonthDate(toDateString)).toBe('2022-02-06')
})
it('Should return next day format year when date is ISOString', () => {
expect(PreviousDayFormatYearMonthDate(toISOString)).toBe('2018-04-30')
})
it('Should return next day format year when date is today', () => {
expect(PreviousDayFormatYearMonthDate(today)).toBe(
DateTime.fromISO(new Date().toISOString()).plus({ days: -1 }).toFormat(ISO_DATE_FORMAT),
)
})
})
describe('dateDifference', () => {
it('Should return 0 when date is today', () => {
expect(dateDifference(today)).toBe(0)
})
it('Should return 1 when date is not today', () => {
expect(dateDifference(tomorrow)).toBe(1)
})
it('Should return 1 when date is not today', () => {
expect(dateDifference(yesterday)).toBe(1)
})
})
describe('formatYearMonthDate', () => {
it('Should return format date when date is string', () => {
expect(formatYearMonthDate(toDateString)).toBe('2022-02-07')
})
it('Should return format date when date is ISO String', () => {
expect(formatYearMonthDate(toISOString)).toBe('2018-05-01')
})
it('Should return format date when date is today', () => {
expect(formatYearMonthDate(today)).toBe(
DateTime.fromISO(new Date().toISOString()).toFormat(ISO_DATE_FORMAT),
)
})
it('Should return empty string when date is undefined', () => {
expect(formatYearMonthDate(undefined)).toBe('')
})
})
describe('weekNumber', () => {
it('Should return week number when date is string', () => {
expect(weekNumber(toDateString)).toBe(6)
})
it('Should return week number when date is ISO String', () => {
expect(weekNumber(toISOString)).toBe(18)
})
it('Should return week number when date is today', () => {
expect(weekNumber(today)).toBe(DateTime.fromISO(new Date().toISOString()).weekNumber)
})
})
describe('nextWeek', () => {
it('Should return next week date when date is string', () => {
expect(nextWeek(toDateString)).toBe('2022-02-14')
})
it('Should return next week date when date is ISO String', () => {
expect(nextWeek(toISOString)).toBe('2018-05-07')
})
})
describe('previousWeek', () => {
it('Should return previous week date when date is string', () => {
expect(previousWeek(toDateString)).toBe('2022-01-31')
})
it('Should return previous week date when date is ISO String', () => {
expect(previousWeek(toISOString)).toBe('2018-04-23')
})
})
describe('firstDateOfWeek', () => {
it('Should return first date of the week when date is string', () => {
expect(firstDateOfWeek(toDateString)).toBe('2022-02-07')
})
it('Should return first date of the week when date is ISO String', () => {
expect(firstDateOfWeek(toISOString)).toBe('2018-04-30')
})
})
describe('lastDateOfWeek', () => {
it('Should return first date of the week when date is string', () => {
expect(lastDateOfWeek(toDateString)).toBe('2022-02-13')
})
it('Should return first date of the week when date is ISO String', () => {
expect(lastDateOfWeek(toISOString)).toBe('2018-05-06')
})
})
describe('formatDateMonth', () => {
it('Should return format date month when date is string', () => {
expect(formatDateMonth(toDateString)).toBe('7.2')
})
it('Should return format date month when date is ISO String', () => {
expect(formatDateMonth(toISOString)).toBe('1.5')
})
it('Should return format date month when date is today', () => {
expect(formatDateMonth(today)).toBe(
DateTime.fromISO(new Date().toISOString()).toFormat(DATE_MONTH_FORMAT),
)
})
})
describe('shortDateString', () => {
it('Should return first two letters Finnish weekdays when date is string', () => {
expect(shortDateString(new Date(toDateString))).toBe('Ma')
})
it('Should return first two letters Finnish weekdays when date is ISO String', () => {
expect(shortDateString(new Date(toISOString))).toBe('Ti')
})
})
describe('compareDays', () => {
it('Should return 0 if the dates are same', () => {
expect(compareDays(new Date(toDateString), new Date(toDateString))).toBe(0)
})
it('Should return 0 if the dates are same', () => {
expect(compareDays(new Date(toDateString), new Date(toISOString))).toBe(1378)
})
it('Should return 0 if the dates are string', () => {
expect(compareDays(toDateString, toISOString)).toBe(1378)
})
})
The difference has to do with the timezone of your local computer and the timezone of the GitLab runner. The GitLab runners use UTC timezone.
Take this case for example:
const toDateString = 'Mon Feb 07 2022 00:00:00 GMT+0200 (Eastern European Standard Time)'
// ...
describe('formatTime', () => {
it('Should return 00:00 when there is no time', () => {
expect(formatTime(toDateString)).toBe('00:00')
})
In your unit test results:
Expected: "00:00"
Received: "22:00"
This would pass if your local timezone is UTC+2, but would fail if your timezone is UTC. Mon Feb 07 2022 00:00:00 GMT+0200 is Sun Feb 06 2022 22:00:00 in UTC time.
From the luxon docs: (emphasis added)
A DateTime comprises of:
[...]
A time zone. Each instance is considered in the context of a specific zone (by default the local system's zone).
[...]
You'll probably want to normalize your tests to use a particular timezone. See this question for hints on how to change the timezone of the datetime objects.
Related
I am new to passport.js and trying to cover the unit test case for my JWT strategy. Can anyone suggest how to do that?
// Setup JWT strategy for all requests
passport.use(
new JWTStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: JWT_PRIVATE_KEY,
},
async (jwtPayload: any, done: any) => {
const isUser = jwtPayload.type === EntityType.User;
const model = isUser ? userModel : vendorModel;
try {
const document = await model.findOne({ _id: jwtPayload.id });
if (document) {
return done(null, jwtPayload);
} else {
return done(null, false);
}
} catch (err) {
return done(err, false);
}
},
),
);
Unit test solution:
index.ts:
import passport from 'passport';
import { Strategy as JWTStrategy, ExtractJwt } from 'passport-jwt';
import { userModel, vendorModel, EntityType } from './models';
const JWT_PRIVATE_KEY = 'secret 123';
passport.use(
new JWTStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: JWT_PRIVATE_KEY,
},
async (jwtPayload: any, done: any) => {
console.log('123123');
const isUser = jwtPayload.type === EntityType.User;
const model = isUser ? userModel : vendorModel;
try {
const document = await model.findOne({ _id: jwtPayload.id });
if (document) {
return done(null, jwtPayload);
} else {
return done(null, false);
}
} catch (err) {
return done(err, false);
}
},
),
);
models.ts:
export enum EntityType {
User = 'User',
}
export const userModel = {
async findOne(opts) {
return 'real user document';
},
};
export const vendorModel = {
async findOne(opts) {
return 'real vendor document';
},
};
index.test.ts:
import { Strategy as JWTStrategy, ExtractJwt, VerifyCallback, StrategyOptions } from 'passport-jwt';
import passport from 'passport';
import { userModel, vendorModel } from './models';
jest.mock('passport-jwt', () => {
const mJWTStrategy = jest.fn();
const mExtractJwt = {
fromAuthHeaderAsBearerToken: jest.fn(),
};
return { Strategy: mJWTStrategy, ExtractJwt: mExtractJwt };
});
jest.mock('passport', () => {
return { use: jest.fn() };
});
describe('62125872', () => {
let verifyRef;
beforeEach(() => {
const mJwtFromRequestFunction = jest.fn();
(ExtractJwt.fromAuthHeaderAsBearerToken as jest.MockedFunction<
typeof ExtractJwt.fromAuthHeaderAsBearerToken
>).mockReturnValueOnce(mJwtFromRequestFunction);
(JWTStrategy as jest.MockedClass<any>).mockImplementation((opt: StrategyOptions, verify: VerifyCallback) => {
verifyRef = verify;
});
});
it('should verify using user model and call done with jwtpayload if user document exists', async () => {
const payload = { type: 'User', id: 1 };
const mDone = jest.fn();
jest.spyOn(userModel, 'findOne').mockResolvedValueOnce('mocked user document');
await import('./');
await verifyRef(payload, mDone);
expect(passport.use).toBeCalledWith(expect.any(Object));
expect(JWTStrategy).toBeCalledWith(
{ jwtFromRequest: expect.any(Function), secretOrKey: 'secret 123' },
expect.any(Function),
);
expect(ExtractJwt.fromAuthHeaderAsBearerToken).toBeCalledTimes(1);
expect(userModel.findOne).toBeCalledWith({ _id: 1 });
expect(mDone).toBeCalledWith(null, { type: 'User', id: 1 });
});
it("should verify using user model and call done with false if user document doesn't exist", async () => {
const payload = { type: 'User', id: 1 };
const mDone = jest.fn();
jest.spyOn(userModel, 'findOne').mockResolvedValueOnce('');
await import('./');
await verifyRef(payload, mDone);
expect(passport.use).toBeCalledWith(expect.any(Object));
expect(JWTStrategy).toBeCalledWith(
{ jwtFromRequest: expect.any(Function), secretOrKey: 'secret 123' },
expect.any(Function),
);
expect(ExtractJwt.fromAuthHeaderAsBearerToken).toBeCalledTimes(1);
expect(userModel.findOne).toBeCalledWith({ _id: 1 });
expect(mDone).toBeCalledWith(null, false);
});
// you can do the rest parts
});
Unit test results:
PASS stackoverflow/62125872/index.test.ts
62125872
✓ should verify using user model and call done with jwtpayload if user document exists (11ms)
✓ should verify using user model and call done with false if user document doesn't exist (2ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 85 | 83.33 | 60 | 84.21 |
index.ts | 92.86 | 75 | 100 | 92.31 | 24
models.ts | 66.67 | 100 | 33.33 | 66.67 | 6,11
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.716s, estimated 10s
Using supertest to verify full cycle
import request from 'supertest';
import express from 'express';
import jwt from 'jsonwebtoken'
export const createAuthToken = (userId) => {
const body = {
type: EntityType.User,
id: userId,
};
return jwt.sign(body, JWT_PRIVATE_KEY);
};
// this function should configure express app
const appLoader = async app => {
(await import('../app/loaders/express')).expressLoader({ app }); // express bindings and routes
await import('./'); // passport config
}
describe('passport-jwt auth', () => {
const app = express();
const token = createAuthToken('user1')
beforeAll(async () => {
await appLoader({ app });
});
it('should verify auth', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValueOnce('mocked user document');
await request(app)
.get('/protected-endpoint')
.set('Authorization', `Bearer ${token}`)
.expect(200);
});
it('should verify auth - failure', async () => {
await request(app)
.get('/protected-endpoint')
.set('Authorization', `Bearer wrong-token`)
.expect(401);
});
});
Update January 22nd 2020
The solution from #slideshowp2 is correct, but I could not get it to work at all, due to this TypeError:
TypeError: Cannot read property 'query' of undefined
Well it turned out to be my jest configuration that had resetMocks: true set. After I removed it, the test did pass. (I don't know why though)
Original question:
I need to execute a graphql query in a helper function outside of a React component using Apollo Client and after a bit of trial and error I went for this approach which is working as it is supposed to:
setup.ts
export const setupApi = (): ApolloClient<any> => {
setupServiceApi(API_CONFIG)
return createServiceApolloClient({ uri: `${API_HOST}${API_PATH}` })
}
getAssetIdFromService.ts
import { setupApi } from '../api/setup'
const client = setupApi()
export const GET_ASSET_ID = gql`
query getAssetByExternalId($externalId: String!) {
assetId: getAssetId(externalId: $externalId) {
id
}
}
`
export const getAssetIdFromService = async (externalId: string) => {
return await client.query({
query: GET_ASSET_ID,
variables: { externalId },
})
return { data, errors, loading }
}
Now I am trying to write test tests for the getAssetIdFromService function, but I have trouble figuring out how to get the client.query method to work in tests.
I have tried the approach below including many others that did not work.
For this particular setup, jest throws
TypeError: client.query is not a function
import { setupApi } from '../../api/setup'
import { getAssetIdFromService } from '../getAssetIdFromService'
jest.mock('../../api/setup', () => ({
setupApi: () => jest.fn(),
}))
describe('getAssetIdFromService', () => {
it('returns an assetId when passed an externalId and the asset exists in the service', async () => {
const { data, errors, loading } = await getAssetIdFromService('e1')
// Do assertions
})
}
I assume I am missing something in relation to this part:
jest.mock('../../api/setup', () => ({
setupApi: () => jest.fn(),
}))
...but I cannot see it.
You didn't mock correctly. Here is the correct way:
getAssetIdFromService.ts:
import { setupApi } from './setup';
import { gql } from 'apollo-server';
const client = setupApi();
export const GET_ASSET_ID = gql`
query getAssetByExternalId($externalId: String!) {
assetId: getAssetId(externalId: $externalId) {
id
}
}
`;
export const getAssetIdFromService = async (externalId: string) => {
return await client.query({
query: GET_ASSET_ID,
variables: { externalId },
});
};
setup.ts:
export const setupApi = (): any => {};
getAssetIdFromService.test.ts:
import { getAssetIdFromService, GET_ASSET_ID } from './getAssetIdFromService';
import { setupApi } from './setup';
jest.mock('./setup.ts', () => {
const mApolloClient = { query: jest.fn() };
return { setupApi: jest.fn(() => mApolloClient) };
});
describe('59829676', () => {
it('should query and return data', async () => {
const client = setupApi();
const mGraphQLResponse = { data: {}, loading: false, errors: [] };
client.query.mockResolvedValueOnce(mGraphQLResponse);
const { data, loading, errors } = await getAssetIdFromService('e1');
expect(client.query).toBeCalledWith({ query: GET_ASSET_ID, variables: { externalId: 'e1' } });
expect(data).toEqual({});
expect(loading).toBeFalsy();
expect(errors).toEqual([]);
});
});
Unit test results with 100% coverage:
PASS apollo-graphql-tutorial src/stackoverflow/59829676/getAssetIdFromService.test.ts (8.161s)
59829676
✓ should query and return data (7ms)
--------------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
--------------------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
getAssetIdFromService.ts | 100 | 100 | 100 | 100 | |
--------------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.479s
Source code: https://github.com/mrdulin/apollo-graphql-tutorial/tree/master/src/stackoverflow/59829676
Use blow mock class:
class ApolloClient {
constructor(uri: string, fetch: any, request: any) {}
setupApi() {
return {
query: jest.fn(),
};
}
query() {
return jest.fn();
}
}
module.exports = ApolloClient;
and add below line to jest.cofig.ts
moduleNameMapper: {
'apollo-boost': '<rootDir>/.jest/appolo-client.ts',
},
I'm using a switch-statement to conditionally render components and I've been spending quite some time attempting to test but I just have no idea how to go about it and havent found any helpful resources online for this. Thanks for taking a look!
componentToRender = (currentPath) => {
const { years, tours, songs, shows, venues } = this.props;
switch (currentPath) {
case "/Years":
return years.map(year => <Years key={year.date} year={year} />);
case "/Tours":
return tours.map(tour => <Tours key={tour.id} tour={tour} />);
case "/Songs":
return songs.map(song => <Songs key={song.id} song={song} />);
case "/Venues":
return venues.map(venue => <Venues key={venue.id} venue={venue} />);
case "/Shows":
return shows.map(show => <Shows key={show.id} show={show} />);
case "/SetList":
return <SetLists />;
case "/UserStats":
return <UserStats />;
default:
return <HomePage />;
}
};
Here is a solution:
index.tsx:
import React, { Component } from 'react';
const Years = ({ key, year }) => (
<div>
{key}, {year}
</div>
);
const Tours = ({ key, tour }) => (
<div>
{key}, {tour}
</div>
);
const Songs = ({ key, song }) => (
<div>
{key}, {song}
</div>
);
const Venues = ({ key, venue }) => (
<div>
{key}, {venue}
</div>
);
const Shows = ({ key, show }) => (
<div>
{key}, {show}
</div>
);
const SetLists = () => <div>SetLists</div>;
const UserStats = () => <div>UserStats</div>;
const HomePage = () => <div>HomePage</div>;
export interface IXComponentProps {
years: any[];
tours: any[];
songs: any[];
shows: any[];
venues: any[];
currentPath: string;
}
export class XComponent extends Component<IXComponentProps> {
constructor(props) {
super(props);
}
public componentToRender = currentPath => {
const { years, tours, songs, shows, venues } = this.props;
switch (currentPath) {
case '/Years':
return years.map(year => <Years key={year.date} year={year} />);
case '/Tours':
return tours.map(tour => <Tours key={tour.id} tour={tour} />);
case '/Songs':
return songs.map(song => <Songs key={song.id} song={song} />);
case '/Venues':
return venues.map(venue => <Venues key={venue.id} venue={venue} />);
case '/Shows':
return shows.map(show => <Shows key={show.id} show={show} />);
case '/SetList':
return <SetLists />;
case '/UserStats':
return <UserStats />;
default:
return <HomePage />;
}
}
public render() {
const { currentPath } = this.props;
return this.componentToRender(currentPath);
}
}
index.spec.tsx:
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { XComponent, IXComponentProps } from './';
describe('XComponent', () => {
let wrapper: ShallowWrapper;
const mockedProps: IXComponentProps = {
years: [{ date: '2019-01-01' }],
tours: [{ id: '1' }],
songs: [{ id: '2' }],
shows: [{ id: '3' }],
venues: [{ id: '4' }],
currentPath: ''
};
beforeEach(() => {
wrapper = shallow(<XComponent {...mockedProps}></XComponent>);
});
it.each`
currentPath | componentToRender
${'/'} | ${'HomePage'}
${'/UserStats'} | ${'UserStats'}
${'/SetList'} | ${'SetLists'}
${'/Shows'} | ${'Shows'}
${'/Venues'} | ${'Venues'}
${'/Songs'} | ${'Songs'}
${'/Tours'} | ${'Tours'}
${'/Years'} | ${'Years'}
`(
'should render $componentToRender component by current path $currentPath correctly',
({ currentPath, componentToRender }) => {
wrapper.setProps({ currentPath });
expect(wrapper.find(componentToRender)).toHaveLength(1);
}
);
});
Unit test result with coverage report:
PASS src/stackoverflow/56453372/index.spec.tsx (7.661s)
XComponent
✓ should render HomePage component by current path / correctly (19ms)
✓ should render UserStats component by current path /UserStats correctly (1ms)
✓ should render SetLists component by current path /SetList correctly (2ms)
✓ should render Shows component by current path /Shows correctly (1ms)
✓ should render Venues component by current path /Venues correctly (1ms)
✓ should render Songs component by current path /Songs correctly (2ms)
✓ should render Tours component by current path /Tours correctly (1ms)
✓ should render Years component by current path /Years correctly (1ms)
-----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files | 67.86 | 100 | 52.94 | 100 | |
index.tsx | 67.86 | 100 | 52.94 | 100 | |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 8 passed, 8 total
Snapshots: 0 total
Time: 9.53s
HTML coverage report:
Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/56453372
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 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