I'm trying to test async service that is returning response object after few intertwined API calls (axios with interceptors). Right now I'm using jest-mock-axios lib but I'm open to any alternatives or pure Jest.
(I removed irrelevant parts of code, originally written in TS)
// services/persons.js
import personsAgent from '../agents/persons';
import places from './places';
[...]
const get = async ({ search = '', limit, offset }) => {
const places = await places.get({ search: '', limit: 1000, offset: 0 }); // api call to endpoint url '/places/'
const params = {
search: !!search.length ? search : null,
limit,
offset,
};
[...]
return personsAgent.getAll({ ...params }).then(resp => {
const results = sort(resp.data.results, .....).map((person, i) => {
const place = places?.data?.results.filter(.....);
return {
id: person.id,
name: person.first_name,
surname: person.last_name,
place,
};
});
return {
data: { ...resp.data, results },
status: resp.status,
};
});
};
[....]
export default {
get,
};
// agents/persons.js
import requests from '../utils/axios';
export default {
getAll: (params: object) => requests.get('/persons/', { params }),
}
// services/persons.test.js
import mockAxios from 'jest-mock-axios';
import persons from './persons';
afterEach(() => {
mockAxios.reset();
});
it('returns Persons data from API', async () => {
let catchFn = jest.fn(),
thenFn = jest.fn();
persons
.get({ search: '', limit: 10, offset: 0 })
.then(thenFn)
.catch(catchFn);
expect(mockAxios.get).toHaveBeenCalledWith('/persons/', {
params: { search: null, limit: 10, offset: 0 },
}); // FAIL - received: '/places/', { search: '', limit: 1000, offset: 0 }
let responseObj = {
data: {
results: ['test'],
},
};
mockAxios.mockResponse(responseObj);
expect(thenFn).toHaveBeenCalledWith({
data: {
results: ['test'],
},
status: 200,
});
expect(catchFn).not.toHaveBeenCalled();
});
I'm using jest-mock-axios and for my others, simpler services without additional, internal call everything is working fine, but this is problematic.
How to ignore or mock const places = await places.get() to focus on personsAgent.getAll()?
Issue right now is that I'm testing request for const places = await places.get() and there is no secondary request for personsAgent.getAll().
axios.getReqByUrl('/persons/') // null
Any ideas, examples or alternatives? Thx in advance!
Related
I use the Firestore Rest API in nextJs getServersideProps to fetch a firestore doc. It works as expected, but every 5:30min the function getServersideProps gets retriggered without reloading or navigating (is this only on dev environment?) and then the result of the rest api is simply
[ { readTime: '2022-10-28T14:24:01.348248Z' } ]
The document key is missing and no data is present, which breaks the server function (App behaves without showing error).
The fetching function looks like this:
const { GoogleToken } = require('gtoken');
const { documentToJson } = require('./helpers');
const getConfig = require('next/config').default;
const FIRESTORE = getConfig().serverRuntimeConfig.firestore;
export async function fetchWebsitePropsByPath(path: string) {
const body = JSON.stringify({
structuredQuery: {
from: [{ collectionId: 'websites' }],
where: {
compositeFilter: {
op: 'AND',
filters: [
{
fieldFilter: {
field: {
fieldPath: 'path',
},
op: 'ARRAY_CONTAINS',
value: {
stringValue: path,
},
},
},
],
},
},
limit: 1,
},
});
// Authenticate with Google
const gtoken = new GoogleToken({
key: FIRESTORE.key,
email: FIRESTORE.email,
scope: ['https://www.googleapis.com/auth/datastore'], // or space-delimited string of scopes
eagerRefreshThresholdMillis: 5 * 60 * 1000,
});
const getToken = () =>
new Promise((resolve, reject) => {
gtoken.getToken((err, token) => {
if (err) {
reject(err);
}
resolve(token);
});
});
const token: any = await getToken();
let headers = new Headers();
headers.append('Authorization', 'Bearer ' + token.access_token);
const res = await fetch(`${FIRESTORE.api}:runQuery`, {
method: 'POST',
headers,
body: body,
});
const rawData = await res.json();
const id = rawData[0].document.name.split('/').pop();
const docData = documentToJson(rawData[0].document.fields);
docData.id = id;
return docData;
}
I would like to know if I can prevent the refetching every 5:30 min if it is not dev env specific and why the rest api returns nothing here.
I mocked the "createTask" function in the "api":
import configureMockStore from "redux-mock-store";
import { createTask } from "../../actions/tasks";
import * as api from "../../api";
import thunk from "redux-thunk";
jest.mock('../../api')
// mock api module
api.createTask = jest.fn(() => {
return Promise.resolve({data : 'foo'})
})
// configuration mock store
const middleware = [thunk];
const mockStore = configureMockStore(middleware);
// suite tests
describe("create task action create store<async>", () => {
test("works", () => {
const expectedActions = [
{ type: "REQUEST_STARTED" },
{
type: "CREATE_TASK_SUCCEED",
payLaod: { task: "foo" },
meta: { analytics: { event: "create_task", data: { id: undefined } } },
},
];
const store = mockStore({
tasks: {
tasks: [],
},
});
return store.dispatch(createTask({})).then(() => {
expect(store.getActions()).isEqual(expectedActions);
expect(api.createTask).toHaveBeenCalled();
});
});
});
But after running the test, I get this error. seem my mock function does not return a promise.
● create task action create store<async> › works
TypeError: Cannot read properties of undefined (reading 'then')
23 | return (dispatch) => {
24 | dispatch(requestStarted());
> 25 | api
| ^
26 | .createTask({ title, description, status, timer, projectId })
27 | .then((resp) => {
28 | dispatch(createTaskSucceed(resp.data));
I do not know the reason for this. Can anyone help me?
createTask action creator code
function createTask({
title,
description,
status = "Unstarted",
timer = 0,
projectId,
}) {
return (dispatch) => {
dispatch(requestStarted());
api
.createTask({ title, description, status, timer, projectId })
.then((resp) => {
dispatch(createTaskSucceed(resp.data));
})
.catch((error) => {
dispatch(requestFailed(error));
});
};
}
I also added action creator code to show what is happening in this function
Intended result
I have two routes: /test/ and /test/:id.
On /test/ I have a list of events and it's only made of events that haven't been resolved
On /test/:id I have a mutation to mark an event as resolved, and, on success, I'm redirecting the user back to /test/.
This success means that the event should no longer appear on /test/ and I'm expecting a new request to get the list of events.
// my file with the mutation
const [eventResolveMutation] = useEventResolveMutation({
onCompleted: () => {
showSuccessToast(
`${t('form:threat-resolved')}! ${t('general:threat')} ${t(
'form:has-been-resolved'
)}.`
)
setTimeout(() => {
navigate('/threats/live')
}, 2000)
},
onError: (error: ApolloError) => {
showErrorToast(error.message)
},
})
const handleEventResolveClick = (id: string) => {
eventResolveMutation({ variables: { id: id, isResolved: true } })
}
return (
<button onClick={() => handleEventResolveClick(id)}>Press</button>
)
// my file with the `events` query
// the results are displayed in a table, which is way I have `currentPage` and `pageSize` in them
const [getEvents, { loading, data }] = useEventsLazyQuery()
useEffect(() => {
getEvents({
variables: {
page: currentPage,
pageSize: paginationSizeOptions[chosenDropdownIndex],
isThreat: true,
isResolved: false,
},
})
}, [chosenDropdownIndex, currentPage, getEvents])
Actual outcome:
Once I press the button that triggers the mutation and I'm redirected to the /tests, I can see that I'm landing inside the useEffect by logging something. What I don't see is a request made via getEvents, which is expected to happen since all the functionalities with the page work
Extra info:
// my graphqlclient.ts
import {
ApolloClient,
ApolloLink,
createHttpLink,
InMemoryCache,
} from '#apollo/client'
const serverUrl = () => {
switch (process.env.REACT_APP_ENVIRONMENT) {
case 'staging':
return 'env'
case 'production':
return 'env'
default:
return 'env'
}
}
const cleanTypeName = new ApolloLink((operation, forward) => {
if (operation.variables) {
const omitTypename = (key: string, value: any) =>
key === '__typename' ? undefined : value
operation.variables = JSON.parse(
JSON.stringify(operation.variables),
omitTypename
)
}
return forward(operation).map((data) => data)
})
const httpLink = createHttpLink({
uri: serverUrl(),
credentials: 'include',
})
const httpLinkWithTypenameHandling = ApolloLink.from([cleanTypeName, httpLink])
const client = new ApolloClient({
link: httpLinkWithTypenameHandling,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
})
export default client
// my mutation
// this mutation will mark an `id` as `resolved` and that means that it should disappear from the list above
mutation EventResolve($id: ID!, $isResolved: Boolean!) {
eventResolve(id: $id, isResolved: $isResolved) {
id
sequence
}
}
I'm trying to test a service in Nestjs which is responsible for getting one record out of a mongo database, using Jest.
As per common convention, when writing unit tests that test services we can mock a record that would sit in a database.
I'm trying the following implementation:
import { Test } from '#nestjs/testing';
import { QuestionsService } from './questions.service';
import { CreateQuestionRequestDto } from './dto/create-question-request.dto';
import { getModelToken } from '#nestjs/mongoose';
import { UpdateQuestionRequestDto } from './dto/update-question-request.dto';
import { NotFoundException } from '#nestjs/common';
import { DuplicateQuestionRequestDto } from './dto/duplicate-question-request.dto';
const testQuestion: CreateQuestionRequestDto = {
data: {
createdBy: { id: 0, name: '' },
lanugageTexts: undefined,
options: undefined,
status: undefined,
type: undefined,
entityId: 1,
propertyId: 'propHash1',
companyId: 1,
entityType: 'announcement',
},
};
describe('QuestionsService', () => {
let questionService: QuestionsService;
let findOne: jest.Mock;
let findOneAndUpdate: jest.Mock;
let find: jest.Mock;
beforeEach(async () => {
// save = jest.fn();
findOne = jest.fn();
findOneAndUpdate = jest.fn();
find = jest.fn();
const module = await Test.createTestingModule({
providers: [
QuestionsService,
{
provide: getModelToken('Question'),
useValue: {}
}
]
})
.compile();
questionService = await module.get<QuestionsService>(QuestionsService);
});
it('should be defined', () => {
expect(questionService).toBeDefined();
});
/**
* Question Get
*/
describe('when getting a question', () => {
describe('and the questionId does not exist', () => {
beforeEach(() => {
findOne.mockReturnValue(undefined);
})
it('should throw a NotFound exception', async () => {
const response = await questionService.get('announcement', 9136500000);
expect(response).toThrow(NotFoundException);
});
});
describe('and the questionId exists', () => {
beforeEach(() => {
findOne.mockResolvedValue(Promise.resolve(testQuestion));
});
it('should update the correct question', async() => {
const response = await questionService.get('announcement', 1);
expect(response).toMatchObject(updatedTestQuestion);
});
});
});
});
When I run this test I get the following error message.
● QuestionsService › when getting a question › and the questionId does not exist › should throw a NotFound exception
TypeError: this.questionModel.find is not a function
52 | const data: Question[] = [];
53 | const questions = await this.questionModel
> 54 | .find(
| ^
55 | { entityType: entityType, entityId: entityId, status: QuestionStatus.ACTIVE },
56 | { answers: 0 },
57 | {
at QuestionsService.get (questions/questions.service.ts:54:14)
at Object.<anonymous> (questions/questions.spec.ts:128:56)
The service method I'm testing is.
async get(entityType: string, entityId: number): Promise<any> {
const data: Question[] = [];
const questions = await this.questionModel
.find(
{ entityType: entityType, entityId: entityId, status: QuestionStatus.ACTIVE },
{ answers: 0 },
{
sort: { _id: -1 },
limit: 1,
}
)
.exec();
if (!questions.length) {
throw new NotFoundException();
}
questions.forEach((question) => {
data.push(question);
});
return { data };
}
find() is the mongoose method that fetches the record from the database. I believe for the test I need to somehow include these methods I'm using in the service and mock them but I cannot find one clear answer.
I have a componentDidMount that executes the fetchUser(). I am trying to test that componentDidMount.
The Component Code:
static propTypes = {
match: PropTypes.shape({
isExact: PropTypes.bool,
params: PropTypes.object,
path: PropTypes.string,
url: PropTypes.string
}),
label: PropTypes.string,
actualValue: PropTypes.string,
callBack: PropTypes.func
};
state = {
user: {}
};
componentDidMount() {
this.fetchUser();
}
getUserUsername = () => {
const { match } = this.props;
const { params } = match;
return params.username;
};
fetchUser = () => {
getUser(this.getUserUsername()).then(username => {
this.setState({
user: username.data
});
});
};
The Test:
it('should call fetchUsers function only once', () => {
const match = { params: { username: 'testUser' }, isExact: true, path: '', url: '' };
const fetchUserFn = jest.fn(match);
const wrapper = shallow(<UserDetailsScreen match={match} fetchUsers={fetchUserFn} />);
wrapper.instance().componentDidMount(match);
expect(fetchUserFn).toHaveBeenCalledTimes(1); // I get expected 1 and got 0
});
I mean why is this componentDidMount(), testing different than my other ones? I have tested quite a few of them over the past few weeks, never this issue. Maybe because the getUser() is a promise and I need to mock it. Has anyone stumpled on something like this before?
The code for the getUser()
export const getUser = username => {
const options = {
method: httpMethod.GET,
url: endpoint.GET_USER(username)
};
return instance(options);
};
I found the solution, by using spyOn(), by jest. Not sure why, I needed to spy for this particular use-case but please explain if you can. The solution below:
it('should call fetchUsers function only once', () => {
const match = { params: { username: 'testUser' }, isExact: true, path: '', url: '' };
const fetchUserFn = jest.fn(match);
const spy = jest.spyOn(UserDetailsScreen.prototype, 'componentDidMount');
const wrapper = shallow(<UserDetailsScreen match={match} fetchUsers={fetchUserFn} />, {
disableLifecycleMethods: true
});
wrapper.instance().componentDidMount(match);
expect(spy).toHaveBeenCalledTimes(1);
});
One caveat here. If you do not user disableLifecycleMethods, the function will be called twice. Once for every render if I am not mistaken.