I am trying to mock Stripe's class constructor, but it does not seem to be working as it should according to Jest's docs. I followed these instructions in order to mock to the constructor.
Here is the controller code that uses Stripe (/src/controllers/stripe.ts):
import Stripe from 'stripe';
/**
* Creates a Stripe Customer
*
* #param {object} req The express request object.
* #param {object} res The express response object.
* #param {function} next The express next function.
*/
export const createStripeCustomer = async (
req: Request,
res: Response,
next: NextFunction
): Promise<Response | void> => {
const stripeApiKey: string = process.env.STRIPE_API_KEY ? process.env.STRIPE_API_KEY : '';
const user: IUser = req.user as IUser;
/** Send error if no Stripe API key or no user */
if (!stripeApiKey) {
return next(
'Something went wrong. If this problem persists, please contact technical support.'
);
} else if (!user) {
res.status(401);
return next('Unauthroized Access');
}
const stripe = new Stripe(stripeApiKey, {
apiVersion: '2020-03-02',
});
};
Here is the test code (/src/tests/controllers/stripe.ts):
import Stripe from 'stripe';
/**
* Return error if stripe.customer.create fails
*/
test('Should error if stripe.customer.create fails', async (done) => {
process.env.STRIPE_API_KEY = 'StripeAPIKey';
jest.mock('stripe', () => {
return jest.fn().mockImplementation(function () {
return {
customers: {
create: jest.fn().mockRejectedValue('Stripe error'),
},
};
});
});
mockRequest = ({
body: {
payment_method: mockPaymentMethodObject,
},
user: {
email: 'jdoe#google.com',
first_name: 'John',
last_name: 'Doe',
},
} as unknown) as Request;
await createStripeCustomer(mockRequest, mockResponse, mockNextFunction);
expect(mockNextFunction).toHaveBeenCalledTimes(1);
expect(mockNextFunction).toHaveBeenCalledWith('Stripe error');
done();
});
My expectation is that the test should pass, but I am getting the following error:
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: "Stripe error"
Received: [Error: Invalid API Key provided: StripeAPIKey]
Number of calls: 1
137 |
138 | expect(mockNextFunction).toHaveBeenCalledTimes(1);
> 139 | expect(mockNextFunction).toHaveBeenCalledWith('Stripe error');
| ^
140 | done();
141 | });
142 | test('Should error if user update fails', () => {});
at _callee3$ (src/tests/controllers/stripe.test.ts:139:30)
at tryCatch (node_modules/regenerator-runtime/runtime.js:45:40)
at Generator.invoke [as _invoke] (node_modules/regenerator-runtime/runtime.js:274:22)
at Generator.prototype.<computed> [as next] (node_modules/regenerator-runtime/runtime.js:97:21)
at asyncGeneratorStep (node_modules/#babel/runtime/helpers/asyncToGenerator.js:3:24)
at _next (node_modules/#babel/runtime/helpers/asyncToGenerator.js:25:9)
This seems to be throwing because Stripe is not being mocked and it is trying to authenticate with the API key of StripeAPIKey.
Update 1:
I also tried the following, which led to the same result:
import * as stripe from 'stripe';
jest.mock('stripe', () => {
return {
Stripe: jest.fn().mockImplementation(() => ({
customers: {
create: jest.fn().mockRejectedValueOnce('Stripe error'),
},
})),
};
});
I found this post and I'm not exactly sure why this worked instead of using jest.mock(), but here is how I got the tests to start passing. I created a separate file in /tests/__mocks__/.
/tests/mocks/stripe.js (Note: this is not a TS file)
export default class Stripe {}
and here is my new test in /tests/src/controllers/stripe.ts
/**
* Return error if stripe.customer.create fails
*/
test('Should error if stripe.customer.create fails', async (done) => {
Stripe.prototype.customers = ({
create: jest.fn().mockRejectedValueOnce('Stripe error'),
} as unknown) as Stripe.CustomersResource;
await createStripeCustomer(mockRequest, mockResponse, mockNextFunction);
expect(Stripe.prototype.customers.create).toHaveBeenCalledTimes(1);
expect(mockNextFunction).toHaveBeenCalledTimes(1);
expect(mockNextFunction).toHaveBeenCalledWith('Stripe error');
done();
});
If someone with more Jest/Typescript knowledge wants to shed some light on this. That would be great!
Related
I have a post request with express that upload a file and some data to the mongodb:
// Routes
Router.post('/api/training', validator(createVideoSchema, 'body'), uploadVideo, createVideoHandler);
// Route Handlers
async function createVideoHandler (req: Request, res: Response, next: NextFunction) {
try {
const dataToCreate = {
...req.body,
url: req.file?.path,
mimetype: req.file?.mimetype
};
const data = await service.create(dataToCreate);
response(req, res, data, 201);
} catch (error) {
next(error);
}
}
the body must be validate by joi using the following schema:
import Joi from 'joi';
const title = Joi.string().email().min(5).max(255);
const description = Joi.string().min(5).max(255);
const thumbnail = Joi.string().min(5).max(255);
const tags = Joi.array().items(Joi.string().min(5).max(100));
const createVideoSchema = Joi.object({
title: title.required(),
description: description.required(),
thumbnail: thumbnail.required(),
tags: tags.required(),
});
export { createVideoSchema };
Then I am creating a test to verify I am receiving a 201 status code:
it('should have a 201 status code', async () => {
const response = await request(app).post(route)
.set('Accept', 'application/json')
.field('title', data.title)
.field('description', data.description)
.field('thumbnail', data.thumbnail)
.field('tags', data.tags)
.attach('video', Buffer.from('video'), { filename: 'video.mp4' });
expect(response.status).toBe(201);
});
For some reason the validation middleware throws me a 400 error saying that the data is missing:
Error: "title" is required. "description" is required. "thumbnail" is required. "tags" is required
I tried to send the data using .set('Accept', 'multipart/form-data') but it throws me the same error.
I guess this error has to do with the way I send the data, but I don't fully understand.
You typically should not call a live API from a test. Instead you should mock the different possibly API response scenarios and be sure your code handles the different possibilities correctly. Ideally you'll also have a client class of some kind to place direct calls to your API inside a class that can easily be mocked.
For example, you could mock the endpoint response for valid data with something like:
export class VideoClient {
async createVideo(data) {
const response = await request(app).post(route) // Whatever url points to your API endpoint
.set('Accept', 'application/json')
.field('title', data.title)
.field('description', data.description)
.field('thumbnail', data.thumbnail)
.field('tags', data.tags)
.attach('video', Buffer.from('video'), { filename: 'video.mp4' });
if (response.status.ok) {
return { response, message: 'someGoodResponseMessage'};
}
return { response, message: 'someErrorOccurred' };
}
}
Then in your test you can mock your client call:
import { VideoClient } from './clients/VideoClient.js'; // or whatever path you saved your client to
const goodData = { someValidData: 'test' };
const badData = {someBadData: 'test' };
const goodResponse = {
response: { status: 201 },
message: 'someGoodResponseMessage'
}
const badResponse = {
response: { status: 400 },
message: 'someErrorOccurred'
}
it('should have a 201 status code', async () => {
VideoClient.createVideo = jest.fn().mockReturnValue(goodResponse);
const results = await VideoClient.createVideo(goodData);
expect(results.response.status).toBe(201);
expect(results.message).toEqual('someGoodResponseMessage');
});
it('should have a 400 status code', async () => {
VideoClient.createVideo = jest.fn().mockReturnValue(badResponse);
const results = await VideoClient.createVideo(badData);
expect(results.response.status).toBe(400);
expect(results.message).toEqual('someErrorOccurred');
});
This is by no means a working test or exhaustive example, but demonstrating the idea that you really should not call your API in your tests, but instead call mock implementations of your API to handle how your client code responds in different situations.
I am unit testing the individual components that lead up to an API response, in other words, I'm testing it independently of a route since every route runs through this component
I need to test that the function responsible for sending my express response is the correct shape, but without sending an actual HTTP request I can't figure out how to test it.
Here is my component
'use strict'
const moment = require('moment')
module.exports = (req, res, payload) => {
try {
let data = []
if (payload.token) data.push({ token: payload.token })
data.push({ [payload.resource]: payload.data })
res.send({
status: 'OK',
recordCount: payload.data.length,
startTimestamp: req.start.toDate(),
endTimestamp: moment().toDate(),
timeTaken: moment().toDate().getTime() - req.start.toDate().getTime(),
data: data
})
} catch (error) {
return res.status(500).json({
errors: [{
location: 'n/a',
param: 'n/a',
msg: 'something happened when generating the response'
}]
})
}
}
here is my current test ...
const chai = require('chai')
const sinonChai = require('sinon-chai')
const { mockReq, mockRes } = require('sinon-express-mock')
const moment = require('moment')
const present = require('../../src/lib/present')
chai.use(sinonChai)
describe('unit test the present lib method', () => {
it('should return the expected shape', (done) => {
const req = mockReq({
start: moment().toDate(),
body: {}
})
const res = mockRes()
const shape = present(req, res, {
resource: 'empty_array',
data: []
})
shape.should.have.own.property('data') // doesnt work
// AssertionError: expected { Object (append, attachement, ...) } to have own property 'data'
done()
})
})
To properly test schema of response you need to do E2E test, which requires you, to send an API call.
If you want to test just logic, inside of the route, you can always extract it to some service, and just test this service.
You can read the following article: https://www.freecodecamp.org/news/how-to-mock-requests-for-unit-testing-in-node-bb5d7865814a/
I am setting up a graphql server with graphql-yoga and `prisma using Typescript. When a user signs up, an email with a link for validation will be sent to the given email address.
Everything is working fine, but i want to write a test for the mutation before refactoring the functionality, which checks if the 'send' function of SendGrid hast been called.
I tried spying on the function with jest.spyOn, but all I get is an error, that comes from not providing an API key for SendGrid in the tesing environment.
I have used spyOn before, and it worked, though this is the first time I am using jest with Typescript.
SignUp Mutation
import * as sgMail from '#sendgrid/mail';
sgMail.setApiKey(process.env.MAIL_API_KEY);
export const Mutation = {
async signUpUser(parent, { data }, { prisma }, info) {
[...]
const emailData = {
from: 'test#test.de',
to: `${user.email}`,
subject: 'Account validation',
text: `validation Id: ${registration.id}`
};
await sgMail.send(emailData);
return user;
}
}
Trying spyOn
import * as sgMail from '#sendgrid/mail';
const signUpUserMutation = gql`
mutation($data: ValidationInput) {
signUpUser (data: $data) {
id
email
}
}
`;
it('should send a registration email, with a link, containing the id of the registration', async () => {
spyOn(sgMail, "send").and.returnValue(Promise.resolve('Success'));
const variables = {
data: {
email: "test#test.de",
password: "anyPassword"
}
};
await client.mutate({ mutation: signUpUserMutation, variables});
expect(sgMail.send).toHaveBeenCalled();
});
Running the test gives me:
Error: GraphQL error: Unauthorized
Commenting out the function call of send in the mutation and running the test gives me:
Error: expect(spy).toHaveBeenCalled()
Expected spy to have been called, but it was not called.
You don't mock #sendgrid/mail module in a correct way. That's why the error happened. Here is the solution without using GraphQL test client. But you can use GraphQL test client to test your GraphQL resolver and GraphQL Schema after you mock #sendgrid/mail module correctly.
mutations.ts:
import * as sgMail from '#sendgrid/mail';
sgMail.setApiKey(process.env.MAIL_API_KEY || '');
export const Mutation = {
async signUpUser(parent, { data }, { prisma }, info) {
const user = { email: 'example#gmail.com' };
const registration = { id: '1' };
const emailData = {
from: 'test#test.de',
to: `${user.email}`,
subject: 'Account validation',
text: `validation Id: ${registration.id}`
};
await sgMail.send(emailData);
return user;
}
};
mutations.spec.ts:
import { Mutation } from './mutations';
import * as sgMail from '#sendgrid/mail';
import { RequestResponse } from 'request';
jest.mock('#sendgrid/mail', () => {
return {
setApiKey: jest.fn(),
send: jest.fn()
};
});
describe('Mutation', () => {
describe('#signUpUser', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should send a registration email, with a link, containing the id of the registration', async () => {
(sgMail.send as jest.MockedFunction<typeof sgMail.send>).mockResolvedValueOnce([{} as RequestResponse, {}]);
const actualValue = await Mutation.signUpUser({}, { data: {} }, { prisma: {} }, {});
expect(actualValue).toEqual({ email: 'example#gmail.com' });
expect(sgMail.send).toBeCalledWith({
from: 'test#test.de',
to: 'example#gmail.com',
subject: 'Account validation',
text: `validation Id: 1`
});
});
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/56379585/mutations.spec.ts (12.419s)
Mutation
#signUpUser
✓ should send a registration email, with a link, containing the id of the registration (23ms)
--------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
--------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
mutations.ts | 100 | 100 | 100 | 100 | |
--------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 14.315s
Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/56379585
I am writing tests to test my saga. Can anyone guide me how I can change the code below so that I can mock the api call? I don`t want to test real data.
import { call, put } from 'redux-saga/effects';
import { API_BUTTON_CLICK_SUCCESS, } from './actions/consts';
import { getDataFromAPI } from './api';
it('apiSideEffect - fetches data from API and dispatches a success action', () => {
const generator = apiSideEffect();
expect(generator.next().value)
.toEqual(call(getDataFromAPI));
expect(generator.next().value)
.toEqual(put({ type: API_BUTTON_CLICK_SUCCESS }));
expect(generator.next())
.toEqual({ done: true, value: undefined });
});
The getDataFromAPI()
import axios from "axios";
export const getDataFromAPI =(
method: string,
url: string,
path: string,
data?: any
) =>{
switch (method) {
case "create": {
return axios
.post(url + path, data, {
headers: {
Accept: "application/json",
"content-type": "application/json"
}
})
.catch(error => {
throw error.response;
});
}
I have tried to use
jest.mock('../../src/Utilities/api');
const { callApi } = require('../../src/Utilities/api');
callApi.mockImplementation( () => console.log("some api call"));
I am having the error
TypeError: Cannot read property 'mockImplementation' of undefined
at Object.<anonymous> (src/Payments/PaymentSagas.spec.ts:10:17)
at new Promise (<anonymous>)
at Promise.resolve.then.el (node_modules/p-map/index.js:46:16)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
I usually do
import * as apis from '../../src/Utilities/api';
jest.spyOn(api, "callApi");
api.callApi.mockImplementation(/* your mock */);
easily exportable as a per-se function
export function spyUtil(obj, name, mockFunction = undefined) {
const spy = jest.spyOn(obj, name);
let mock;
if (mockFunction) {
mock = jest.fn(mockFunction);
obj[name].mockImplementation(mock);
}
return { spy, mock };
}
and consumable, in your test
spyUtil(apis, "callApi", jest.fn())
Scenario: The data from mutiple rest calls must be aggregated into one single object and returned as the rest response of an initial request which is served via Node.js.
Issue: The rest response is not waiting until the observable is finished thus the mutations (aggregations) are realized after the rest response has been dispatched.
//teamsController class invoked via GET /teams
import * as Rx from 'rxjs/Rx'
import http from 'axios'
import Teams from '../models/teams'
const teamsAPI = "http://localhost:8081/api/v1/teams/players/";
const usersAPI = "http://localhost:8082/api/v1/users/";
exports.getTeamByPlayer = function (req, res) {
let username= req.get("username");
Rx.Observable.fromPromise(fetchTeam(username))
.map(team => {
Rx.Observable.from(team.players).subscribe(player => {
console.log(`Player name is ${player.username}`);
Rx.Observable.fromPromise(fetchUser(player.username))
.map(avatar => avatar.avatar)
.subscribe(avatar => {
player.avatar = avatar;
console.log(player)
})
});
return team;
})
.subscribe(result => {
console.log(`result is ${JSON.stingify(result)}`);
res.json(result);
})
}
/**
* Fetch a team by a player
*
* #param name The name of the team
* #returns {Promise.<Teams>}
*/
function fetchTeam(name) {
return http.get(teamsAPI + name)
.then(response => new Teams(response.data.data))
.catch(error => {
throw new Error("todo: fill error message");
})
}
/**
* Fetch a user given its username
*
* #param username The username of the player
* #returns {Promise.<TResult>}
*/
function fetchUser(username) {
return new Promise(function (resolve, reject) {
console.log(`fetching user: ${username}`);
resolve();
}).then(() => {
return {
"avatar": {
"flagColor": "dummyValue",
"flagCrest": "dummyValue"
}
}
});
Log results:
Player name is username1
fetching user: username1
Player name is username2
fetching user: username2
result is {"id":"5a1c2a4030c39e5d88aed087","name":null,"avatar":{"flagColor":"abcdefg","flagCrest":"hijklmn"},"description":"string","motto":"string","players":[{"userId":"59b94a7b8b68ef0a048e85c1","username":"username1","status":"ACTIVE","role":"ADMIN","dateJoined":1511795264314,"dateInvited":null,"score":0},{"userId":"59b94a7b8b68ef0a048e85c1","username":"username2","status":"ACTIVE","role":"MEMBER","dateJoined":1511795264314,"dateInvited":null,"score":0}],"score":0,"type":"TEAM","open":true,"location":{"longitude":0,"latitude":0,"country":"string"},"owner":"username1","dateCreated":1511795264314}
{ userId: '59b94a7b8b68ef0a048e85c1',
username: 'username1',
status: 'ACTIVE',
role: 'ADMIN',
dateJoined: 1511795264314,
dateInvited: null,
score: 0,
avatar: { flagColor: 'dummyValue', flagCrest: 'dummyValue' } }
{ userId: '59b94a7b8b68ef0a048e85c1',
username: 'username2',
status: 'ACTIVE',
role: 'MEMBER',
dateJoined: 1511795264314,
dateInvited: null,
score: 0,
avatar: { flagColor: 'dummyValue', flagCrest: 'dummyValue' } }
AppServer: Node.JS v8.7.0, Middleware: expess 4.16.2 , Libs: RxJs 5.0.0-beta.12, axios 0.17.1
first, stop converting your http requests to promises. It makes things way more complicated than they need to be.
function fetchTeam(name) {
return http.get(teamsAPI + name).map(res => new Teams(res.json().data));
}
function fetchUser(username) {
return Rx.Observable.of({
"avatar": {
"flagColor": "dummyValue",
"flagCrest": "dummyValue"
}
});
}
If some outside consumer NEEDS a promise based api, then make public functions that wrap these private functions in promises.
second, map is a synchronous operator, you can't execute an asynchronous operation inside of it, you need to use an asynchronous operator that does the subscription work for you. If you ever find yourself subscribing within an observable stream, you're doing something wrong.
let username= req.get("username");
fetchTeam(username)
.switchMap(team => { // switchMap will subscribe to inner observables
//build observables for fetching each avatar, pay close attention, I'm mixing the array map operator and the rx map operator in here
let players$ = team.players.map(player => fetchUser(player.username).map(avatar => avatar.avatar));
return Rx.Observable.forkJoin(players$); // use forkjoin to executre requests
}, (team, playerAvatars) => { // use second switchMap argument to combine results
team.players.forEach((player, idx) => player.avatar = playerAvatars[idx]);
return team;
})
.subscribe(result => {
console.log(`result is ${JSON.stingify(result)}`);
res.json(result);
});