I'm using Mocha/Chai/Sinon to develop some unit tests on a Node.js application (Express). I made a fake request/response object to pass to the function that I'm trying to test. Inside one of my stubs, the value of the property statusCode of the response object is changed to 200. However, at the end of the test, Mocha reports it as being the default value for the property (500, in this case).
Here is the test code:
it('should return status 200 if product was saved', function(){
//fake objects
const fakeReq = {
flash(){return true},
body:{
title: '',
price: 0,
description: ''
},
file: {
path: 'path'
},
session: {
user: ''
}
}
const fakeRes = {
statusCode: 500,
redirect(){
this.statusCode = 200; //this successfully changes to 200
return this;
}
}
//fake methods
sinon.stub(expressValidator, 'validationResult').callsFake(fakeValidation);
sinon.stub(Product.prototype, 'save').callsFake(sinon.fake.resolves('ok'));
//expectations
adminController.postAddProduct(fakeReq, fakeRes, ()=>{});
sinon.restore();
expect(fakeRes.statusCode).to.equal(200); //test fails returning 500
});
Here is the method that I want to test, more specifically, the product.save() call, at the end:
exports.postAddProduct = (req, res, next) => {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
req.flash('validationErrors', validationErrors.array());
return res.status(422).redirect('/admin/add-product');
}
const title = req.body.title;
const image = req.file;
const price = req.body.price;
const description = req.body.description;
if(!image){
req.flash('validationErrors', [{param:'productImage', msg: 'Insira uma imagem válida!'}]);
return res.status(422).redirect('/admin/add-product');
}
const product = new Product({
title: title,
price: price,
imageUrl: image.path,
description: description,
userId: req.session.user
});
product.save()
.then(results => {
return res.redirect('/admin/products');
})
.catch(err => {
next(new Error(err));
});
};
EDIT:
Changed the test function to use a spy in order to check if the redirect method was being called, but it's always returning false. I'm thinking that it might be because Js doesn't pass by reference, unless the contents of the object gets changed. New testing code:
const fakeRes = {
status(code){
this.statusCode = code;
return this;
},
redirect(){return true;},
statusCode: 500
}
const redirectSpy = sinon.spy(fakeRes, 'redirect');
//fake methods
sinon.stub(expressValidator, 'validationResult').callsFake(fakeValidation);
sinon.stub(Product.prototype, 'save').callsFake(sinon.fake.resolves('ok'));
//expectations
adminController.postAddProduct(fakeReq, fakeRes, ()=>{});
sinon.restore();
expect(redirectSpy.withArgs('/admin/products').calledOnce).to.be.true;
});
Related
I already tried some possible solutions and even created and wrote the code again but I am still getting errors. I have created a diminute version of my whole code which connects to the database using Mongoose but after the Schema is created and I import the model in places-controllers my data that I write in POSTMAN goes directly to:
FYI: In this case I want POST request from createPlace to properly work.
Data entry: URL: http://localhost:5000/api/places/
{
"title": "Punta Arena Stfdsfdsfsdfop",
"description": "One stop Stop. Does not have tr12affic lights.",
"busrespect": "12ysdfdsfsfes",
"address": "Avenida Solunna",
"creator": "52peru soiflsdjf36"
}
OUTPUT:
{
"status": "error caught"
}
which is what I told the program to define if the try did not work.
IN app.js I have the following code:
const express= require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const placesRoutes = require("./routes/places-routes");
const HttpError = require ("./models/http-error");
const app = express();
app.use(bodyParser.json());
app.use('/api/places', placesRoutes);
app.use((req, res, next) => {
const error= new HttpError('Route not available. Try something different?', 404);
throw error;
});
app.use((error, req, res, next) =>{
if (res.headerSent) {
return next(error);
}
res.status(error.code || 500)
res.json({message: error.message || "An unknown error occured! Sorry" });
});
url = '<mongo_url>'
mongoose.connect(url, {useNewUrlParser: true}).then(()=>{
console.log("Connected to database")
app.listen(5000);
}).catch(erro => {
console.log(erro)
});
In places-routes.js I have the following code:
const express = require('express');
const {check} = require('express-validator')
const placesControllers=require('../controllers/places-controllers');
const router = express.Router();
router.get('/:pid', placesControllers.getPlaceById );
router.get('/user/:uid',placesControllers.getPlacesByCreatorId );
router.post('/' ,[
check('title')
.not()
.isEmpty(),
check('description').isLength({ min: 5 }),
check('address')
.not()
.isEmpty()
],
placesControllers.createPlace);
router.patch('/:pid', [
check('title')
.not()
.isEmpty(),
check('description').isLength({ min: 5 })
] , placesControllers.updatePlace );
router.delete('/:pid', placesControllers.deletePlace);
module.exports=router;
In places-controllers.js I have the following code:
const HttpError = require('../models/http-error');
const { validationResult } = require('express-validator');
//const getCoordsForAddress= require('../util/location');
const BusStop = require('../models/place');
let INITIAL_DATA = [
{
id: "p1",
title: "Samoa Stop",
description: "My first bus stop in Lima",
//location: {
// lat: 40.1382,
// lng:-23.23
// },
address: "Av. La Molina interseccion con calle Samoa",
busrespect: "yes",
creator: "u1"
}
];
const getPlaceById = (req, res, next) => {
const placeId = req.params.pid // Accessing the p1 in pid URL scrapping {pid:'p1'}
const place= INITIAL_DATA.find(p => { //find method goes over each element in the array, the argument p represents the element where find loop is
return p.id ===placeId
});
if (!place) {
const error= new HttpError('No bus stop found for the provided ID.', 404);
throw error;
}
res.json({place: place});
};
const getPlacesByCreatorId = (req, res, next)=> {
const userId = req.params.uid;
const places = INITIAL_DATA.filter(p=>{ //filter to retrieve multiple places, not only the first one
return p.creator ===userId;
});
if (!places || places.length===0) {
return next(
new HttpError('Could not find bus stops for the provide user id', 404)
);
}
res.json({places});
};
const createPlace = async (req, res,next) => {
const errors = validationResult(req);
if (!errors.isEmpty()){
return next(new HttpError ('Invalid bus stop please check your data', 422));
}
//const { title, description, busrespect, address, creator } = req.body; //erased location for now.
/* let place = new BusStop({
title: req.body.title,
description: req.body.description,
busrespect: req.body.busrespect,
address : req.body.address,
creator: req.body.creator
})
awaitplace.save()
.then(response=>{
res.json({
message : "Employee added sucessfully!"
})
})
.catch(err=>{
res.json({
message : "An error has occured!"
})
})
} */
const { title, description, busrespect, address, creator } = req.body;
try {
await BusStop.create({
title:title,
description: description,
busrespect:busrespect,
address: address,
creator: creator
});
res.send({status: "ok"});
} catch(error) {
res.send({status:"error caught"});
}
};
const updatePlace = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()){
console.log(errors);
throw new HttpError ("Invalid inputs passed, please check your data ", 422);
};
const { title, description } = req.body;
const placeId = req.params.pid;
const updatedPlace = { ...INITIAL_DATA.find(p => p.id === placeId)};
const placeIndex = INITIAL_DATA.findIndex(p => p.id === placeId);
updatedPlace.title = title;
updatedPlace.description = description;
INITIAL_DATA[placeIndex] = updatedPlace;
res.status(200).json({place: updatedPlace});
};
const deletePlace = (req, res, next) => {
const placeId = req.params.pid;
if (!INITIAL_DATA.find(p=> p.id ===placesId))
throw new HttpError('Could not find a bus stop for that ID ')
INITIAL_DATA = INITIAL_DATA.filter(p=> p.id !== placeId)
res.status(200).json({message: 'Deleted Place'});
};
exports.getPlaceById= getPlaceById;
exports.getPlacesByCreatorId = getPlacesByCreatorId;
exports.createPlace = createPlace;
exports.updatePlace = updatePlace;
exports.deletePlace = deletePlace;
Inside models folder I have two files: http-error.js which has this code:
class HttpError extends Error {
constructor(message, errorCode) {
super (message);
this.code = errorCode;
}
}
module.exports = HttpError;
The other file inside is the schema which is place.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const placeSchema = new Schema({
title: {
type: String
},
description: {
type: String
},
address: {
type: String
},
busrespect: {
type: String
},
creator: {
type: String
}
},
)
const BusStop = mongoose.model('BusStop', placeSchema)
module.exports= BusStop
Summary: somewhere in the try catch part from createPlace something is going wrong since my data entry is always going to the error status I indicated in that part.
I have a Firebase function that executes on a Stripe webhooks via express. The function executes fine but the sending of the email (using Axios) keeps resulting in an error:
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
It does work fine on localhost but the error appears when pushed to Firebase staging server. Weirdly the whole function executes fully including the Axios call where I'm getting the header issue (sending the email). It does take about 2-3 minutes to fully execute due to the error.
I've tried a number of different methods using return, and then() promises but it's still flagging this error. My code is as follows:
index.js
// Controllers
const stripeWebhookSubscription = require("./src/controllers/stripe/webhooks/subscription");
// Firebase
const admin = require("firebase-admin");
const functions = require("firebase-functions");
// Express
const express = require("express");
const cors = require("cors");
// Stripe
const stripe = require("stripe")(functions.config().stripe.key_secret);
const serviceAccount = require(functions.config().project.service_account);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: functions.config().project.database_url,
storageBucket: functions.config().project.storage_bucket,
});
const database = admin.firestore();
// -------------------------
// Stripe
// -------------------------
const stripeFunction = express();
stripeFunction.use(cors({origin: true}));
stripeFunction.post("/webhooks", express.raw({type: "application/json"}), (req, res) => {
const sig = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
functions.config().stripe.webhook_secret
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
switch (event.type) {
case "customer.subscription.created":
stripeWebhookSubscription.createSubscription(database, event.data.object, res);
break;
(...)
default:
console.log(`Unhandled event type ${event.type}`);
break;
}
res.json({received: true});
});
exports.stripe = functions.https.onRequest(stripeFunction);
subscription.js
const functions = require("firebase-functions");
const stripe = require("stripe")(functions.config().stripe.key_secret);
const axios = require("axios");
class stripeWebhookSubscription {
static createSubscription(database, subscription, res) {
let barcode;
let plan;
let merchant;
let customer;
database.collection("subscriptions").add({
id: subscription.id,
customer: subscription.customer,
status: subscription.status,
price: {
amount: (subscription.items.data[0].price.unit_amount / 100).toFixed(2),
interval: subscription.items.data[0].price.recurring.interval,
interval_count: subscription.items.data[0].price.recurring.interval_count,
},
product: subscription.items.data[0].price.product,
created: subscription.created,
current_period_start: subscription.current_period_start,
current_period_end: subscription.current_period_end,
cancel_at: subscription.cancel_at,
cancel_at_period_end: subscription.cancel_at_period_end,
payment_gateway: "stripe",
current_usage: 1,
})
.then((doc) => {
barcode = doc.id;
return database.collection("plans").where("stripe.product", "==", subscription.items.data[0].price.product).limit(1).get();
})
.then((docs) => {
docs.forEach((doc) => {
return plan = doc.data();
});
})
.then(() => {
return database.collection("subscriptions").doc(barcode).set({
merchant: plan.merchant,
}, {merge: true});
})
.then(() => {
return database.collection("merchants").doc(plan.merchant).get();
})
.then((doc) => {
return merchant = doc.data();
})
.then(() => {
async function stripeCustomer() {
const stripeData = await stripe.customers.retrieve(subscription.customer);
customer = stripeData;
}
return stripeCustomer().then(() => {
return customer;
});
})
.then(() => {
return database.collection("customers").doc(subscription.customer).set({
name: customer.name,
email: customer.email,
phone: customer.phone,
delinquent: customer.delinquent,
created: customer.created,
livemode: customer.livemode,
merchant: plan.merchant,
subscriptions: [barcode],
}, {merge: true});
})
.then((doc) => {
return axios.request({
url: "https://api.sendinblue.com/v3/smtp/email",
method: "post",
headers: {
"api-key": functions.config().sendinblue.key,
"Content-Type": "application/json",
},
data: {
"to": [
{
"email": customer.email,
"name": customer.name,
},
],
"replyTo": {
"email": "support#scanable.com.au",
"name": "Scanable",
},
"templateId": 2,
"params": {
"plan_name": plan.name,
"interval_count": plan.interval_count,
"interval": plan.interval,
"subscription": barcode,
"merchant_name": merchant.name,
"merchant_email": merchant.email,
},
},
})
.then((response) => {
return console.log("Membership email sent to " + customer.email);
});
})
.then(() => {
res.status(200).send("✅ Subscription " + subscription.id + " created!");
})
.catch((err) => {
res.status(400).send("⚠️ Error creating subscription (" + subscription.id + "): " + err);
});
}
}
module.exports = stripeWebhookSubscription;
In index.js, you call this line:
stripeWebhookSubscription.createSubscription(database, event.data.object, res);
immediately followed by this line:
res.json({received: true});
By the time the createSubscription path has finished, the response has already been sent. When deployed to Cloud Functions, this will also terminate your function before it's done any of its workload (Note: this termination behaviour is not simulated by the local functions emulator).
Depending on what you are trying to achieve, you can probably just add the missing return to this line so that the res.json({received: true}) never gets called:
return stripeWebhookSubscription.createSubscription(database, event.data.object, res);
Additionally, on this line in subscription.js:
database.collection("subscriptions").add({
you need to add the missing return statement so the asynchronous tasks are properly chained:
return database.collection("subscriptions").add({
I'm New to unit test and trying to test my controller method.my project architecture design is as follow
Controller->Service->Model.
My test scenarios :
Pass correct parameters to controller method and test success response
Pass Invalid parameters to controller method and test error response
When i going to test scenario 1 ,according to my understanding i want to mock my programService and it return values.I have write test as follow and got errors.
I would really appreciate some one can fix this
ProgramsController.js
const ProgramService = require('../../services/program/programService');
class ProgramsController {
constructor() {
this.programService = new ProgramService();
}
async subscribe(req, res) {
try {
const { userId, uuid, msisdn, body: { programId } } = req;
const data = { userId, programId, msisdn, uuid }
const subscribe = await this.programService.subscribeUser(data);
res.json({
status: true,
message: 'Success',
friendly_message: constant.MSG.SUBSCRIPTION,
data: subscribe
})
} catch (error) {
res.status(500)
.json({
status: false,
message: 'Fail',
friendly_message: constant.MSG.SUBSCRIPTION_FAIL
})
}
}
}
ProgramService.js
class ProgramService {
constructor() {
this.subscriber = new Subscriber();
this.subsciberProgram = new SubsciberProgram()
}
async subscribeUser(data) {
try {
const { msisdn, userId, programId, uuid } = data;
...
return subscribedData;
} catch (error) {
throw error;
}
}
}
module.exports = ProgramService;
test.spec.js
const ProgramsService = require('../src/services/program/programService')
const ProgramsController = require('../src/controllers/programs/programsController')
const programController = new ProgramsController()
const programsService = new ProgramsService()
beforeAll(() => {
db.sequelize.sync({ force: true }).then(() => { });
});
const mockRequest = (userId, uuid, msisdn, body) => ({
userId,
uuid,
msisdn,
body,
});
const mockResponse = () => {
const res = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
return res;
};
const serviceRecord = { userId: 1, programId: 1, msisdn: '56768382967', uuid: '46651a19-3ef1-4149-818e-9bd8a5f359ef' };
const fakeServiceReturn = { program_id: 1, amount: 5, no_of_questions: 10 }
describe('Subscribe', () => {
test('should return 200', async () => {
const req = mockRequest(
1,
'56768382967',
'46651a19-3ef1-4149-818e-9bd8a5f359ef',
{ 'programId': 1 }
);
const res = mockResponse();
const spy = jest.spyOn(programsService, 'subscribeUser').mockImplementation(() => serviceRecord);
await programController.subscribe(req, res);
expect(programsService.subscribeUser()).toHaveBeenCalledWith(fakeServiceReturn);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
status: true,
message: 'Success',
friendly_message: 'successfull get data',
data : { program_id: 1, amount: 5, no_of_questions: 10 }
});
spy.mockRestore();
});
});
how can i mock programService.subscribeUser and test success response?
This mock should return a promise:
jest.spyOn(programsService, 'subscribeUser').mockImplementation(() => Promise.resolve(serviceRecord));
I use Angular on the frontend and send a post request to a backend built with Express.
This is how the Angular code looks:
addEntry() {
let type = "Random Type";
let location = "Random Location";
let date = new Date();
let results = [
{ name: "my first name", age: 23 },
{ name: "new second name", age: 35 },
];
return this.http.post<{ message: string }>(
this._url + "/myendpoint/add",
{ type, location, date, results }
);
}
And this is how the express code looks like:
router.post(
"/add",
middleware.verifyToken,
async function (req, res) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json(errors.array());
}
try {
let userID = req.tokenData.userID;
let user = await User.findOne({ _id: userID });
if (user === null || user.length <= 0) {
return res.status(422).json("Could not add entry");
} else {
let type = req.body.type;
let location = req.body.location;
let date = req.body.date;
console.log(req.body);
let results = req.body.results.map(async (el) => {
let result = await Entry.findOne({ name: el.name });
return {
name: result.name,
age: el.age,
};
});
[some more code ...]
}
} catch (err) {
console.log(err);
return res.status(422).json(err);
}
}
);
The console log outputs:
{
type: 'Random Type',
centre: 'Random Location',
date: '2020-06-19T16:50:25.357Z',
results: '[object Object]'
}
And I also receive this error which is probably generated because the results show as '[object Object]';
TypeError: req.body.results.map is not a function
How do I get the results array in Express instead of '[object Object]'? I have the following settings enabled in Express:
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
I have less reputation so i cant post a comment, have you tried results[1]['name']; ?
In Angular: results: encodeURIComponent(JSON.stringify(results));
In Express: JSON.parse(decodeURIComponent(req.body.results));
I am using mocha and chai for writing test for RESTful APIs
I have read some articles where people suggests to create stubs for queries, and you shouldn't be actually making a database query.
But How would I make sure if it works?
See below controller.
const Op = require('sequelize').Op
//Models
const {
Item,
Location,
Combo,
Service,
ComboItem,
ItemLocation
} = require('../models')
const _ = require('lodash')
//Services
const paginate = require('../services/PaginationService')
const getAllItems = async function(req, res) {
if(req.query.location_id){
let items
const item = await Location.findOne({
where: {
id: 1
},
include: {
model: Item,
through: {
model: ItemLocation,
attributes: []
},
as: 'itemsAtLocation',
include: [
{
model: Service,
as: 'service',
attributes: ["id"]
},
{
model: Combo,
as: 'combo',
attributes: ["start_date", "expiry_date"]
}
]
}
})
if(!item)
return res.status(200).send({
status: true,
message: "No item found at location!",
data: {}
})
items = item.itemsAtLocation
let data = {}
data.services = []
data.combos = []
_.forEach(items, item => {
let itemData = {
id: item.id,
name: item.name,
price: item.price,
discount_per: item.discount_per,
}
if(item.service)
data.services.push(itemData)
if(item.combo) {
itemData.start_date = item.combo.start_date
itemData.expiry_date = item.combo.expiry_date
data.combos.push(itemData)
}
})
return res.status(200).send({
status: true,
message: "Successfully fetch all items!",
data: data
})
} else {
const items = await Item.findAll({
include: [
{
model: Service,
as: 'service',
attributes: ["id"]
},
{
model: Combo,
as: 'combo',
attributes: ["start_date", "expiry_date"]
}
],
attributes: ["id", "name", "price", "discount_per", "description"],
...paginate(+req.query.page, +req.query.per_page)
})
let data = {}
data.services = []
data.combos = []
_.forEach(items, item => {
let itemData = {
id: item.id,
name: item.name,
price: item.price,
discount_per: item.discount_per,
}
if(item.service)
data.services.push(itemData)
if(item.combo) {
itemData.start_date = item.combo.start_date
itemData.expiry_date = item.combo.expiry_date
data.combos.push(itemData)
}
})
return res.status(200).send({
status: true,
message: "Successfully fetch all items!",
data: data
})
}
}
module.exports = {
getAllItems
}
As you can see from above code. I need queries to return data in a specific form. If it won't be in that form things won't work.
Can someone suggest how can I create stubs for such kind of functions so that structure also be preserved?
Below is the test that I have wrote, But it uses actual db calls.
describe('GET /api/v1/items', function () {
it('should fetch all items orgianized by their type', async () => {
const result = await request(app)
.get('/api/v1/items')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
expect(result)
.to.be.a('Object')
expect(result.body.status)
.to.be.a('Boolean').true
expect(result.body.data, "data should be an Object and every key should an Array")
.to.satisfy(data => {
expect(data).to.be.a('Object')
.to.not.be.null
if(!_.isEmpty(data)) {
expect(data).to.have.any.keys('services', 'combos')
_.forOwn(data, (value, key) => {
expect(data[key]).to.be.a('Array')
})
return true
}
return true
})
})
})
One way you can do that is by stubbing the methods from your models, i.e. Location.findOne and Item.findAll. So your tests could look a bit like the code below:
const sinon = require('sinon');
const Location = require('../models/location'); // Get your location model
const Item = require('../models/item'); // Get your item model
describe('myTest', () => {
let findOneLocationStub;
let findAllItemsStub;
beforeEach(() => {
findOneLocationStub = sinon.stub(Location, 'findOne');
findAllItemsStub = sinon.stub(Item, 'findAll');
});
afterEach(() => {
findOneLocationStub.verifyAndRestore();
findAllItemsStub.verifyAndRestore();
});
it('returns 200 when location not found', () => {
findOneLocationStub.resolves(null);
expects...
});
});
I did not run the test, but something like that should work. But note that I had to split the models into their own file to do the stub. Probably there's a way to do the same using your current implementation.
Another thing I would suggest is having some kind of use case into your method that is responsible for database implementation. Something like:
const getAllItemsUseCase = (params, queryService) => {
if(params.locationId){
let items
const item = await queryService.findOneLocation({
};
So when you call this method from your controller, you can do call:
const getAllItems = async function(req, res) {
const params = {
locationId: req.query.location_id,
// and more parameters
};
const queryService = {
findOneLocation: Location.findOne,
};
const results = await getAllItemsUseCase(params, queryService);
}
This way you will detach your business logic from the controller and you will have a much easier time to mock your query: you just change the methods provided to queryService.
You can find some interesting read from this blog post: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html