I write test for AngularJS application in Sinon & Chai. I do $httpbackend request and I have a fake array of users - backendUsersResponse. It's available in my .spec.js but it's not filled in yet when I call the function openSettings and so my users are an empty array in my controller.
How & where can I tell my code to wait till array is filled in? How do you test async javascript stuff like this?
Thanks in advance!
Controller code:
export default class UserAccountsController {
constructor(UserModel, userSettingsDialog) {
this.UserModel = UserModel;
this.userSettingsDialog = userSettingsDialog;
this.users = [];
UserModel.query()
.then((users) => {
this.users = users;
});
}
openSettings(user) {
const userToUpdate = user || new this.UserModel();
this.userSettingsDialog.openDialog(userToUpdate)
.then((data) => {
const cmd = data.value;
if (cmd === 'saveSettings') {
const isNew = angular.isUndefined(userToUpdate.id);
userToUpdate.$save();
if (isNew) {
this.users.push(userToUpdate);
}
}
});
}
}
UserAccountsController.$inject = [ 'UserModel', 'userSettingsDialog'];
Test
import { expect } from 'chai';
import sinon from 'sinon/pkg/sinon';
describe('userAccounts Controller', () => {
'use strict';
beforeEach(angular.mock.module('myproject'));
let $httpBackend = null;
let $q = null;
let UserModel = null;
let userSettingsDialog = null;
beforeEach(inject(function(_$httpBackend_, _UserModel_, _$q_) {
$httpBackend = _$httpBackend_;
UserModel = _UserModel_;
$q = _$q_;
}));
describe('openSettings', () => {
let $controller;
let backendUsersResponse;
beforeEach(inject(function(_$controller_) {
userSettingsDialog = {
openDialog: sinon.stub(),
};
$controller = _$controller_;
backendUsersResponse = {
users: [
{
id: 1,
name: 'John',
role: 'admin',
},
{
id: 2,
name: 'Diego',
role: 'editor',
},
],
};
}));
it('should check if new user is pushed to an array', () => {
// arrange
const ctrl = $controller('UserAccountsController', {
userSettingsDialog: userSettingsDialog,
});
const user = new UserModel({
name: 'Lander',
});
const data = {
value: 'saveSettings',
};
$httpBackend.expectGET('api/users').respond(200, backendUsersResponse);
userSettingsDialog.openDialog.withArgs(user).returns($q.resolve(data));
$httpBackend.expectPOST('api/users').respond(200, user);
// act
ctrl.openSettings(user);
$httpBackend.flush();
// assert
expect(userSettingsDialog.openDialog).to.have.been.called;
expect(user.name).to.equal('Lander');
expect(ctrl.users).not.to.be.empty;
expect(ctrl.users.length).to.be(3); //<-- this one fails, array length is always 2, user is not added
});
});
});
Related
In my calendar.spec.js, I have:
const { google } = require('googleapis')
const googleCalendar = google.calendar('v3')
...
before(() => {
sinon.stub(googleCalendar.calendarList, 'list').resolves({ data: true })
})
after(() => {
googleCalendar.calendarList.list.restore()
})
In my calendar.js, I have:
const { google } = require('googleapis')
const googleCalendar = google.calendar('v3')
let { data } = await googleCalendar.calendarList.list({
auth: oauth2Client
})
But it doesn't appear to be stubbed. It goes ahead and tries to connect to Google Calendar. What am I doing wrong?
You can mock the entire googleapis module with mock-require.
const mock = require('mock-require');
mock('googleapis', {
google: {
calendar: () => ({
calendarList: {
list: () => {
return Promise.resolve({
data: {
foo: 'bar'
}
});
}
}
})
}
});
Once you mocked it, your module will consume the mocked module instead of the original so you can test it. So if you module is exposing a method that calls the API, something like that:
exports.init = async () => {
const { google } = require('googleapis');
const googleCalendar = google.calendar('v3');
let { data } = await googleCalendar.calendarList.list({
auth: 'auth'
});
return data;
}
The test will be
describe('test', () => {
it('should call the api and console the output', async () => {
const result = await init();
assert.isTrue(result.foo === 'bar');
});
});
Here is a small repo to play with it: https://github.com/moshfeu/mock-google-apis
This is the code to test:
const AWS = require('aws-sdk');
const { APPLICATIONS, NOTIFICATION_FREQUENCIES } = require('./config');
exports.createHandler = ({ notificationService }) => async (event, context) => {
try{
Object.values(APPLICATIONS).forEach(async appId => {
const notifications = await notificationService
.getNotificationsByApplication(appId);
const dailyNotifications =notifications.filter(
e =>
e.frequency === NOTIFICATION_FREQUENCIES.DAILY,
);
console.log('dailyNo', dailyNotifications);
const dailyTemplate = notificationService.prepareDailyTemplate(
dailyNotifications
);
console.log('dailyTemplate', dailyTemplate);
notificationService.notifyToAdmin(dailyTemplate);
});
}
catch(err) {
console.log(err);
}
};
And this is my test using sinon:
const sinon = require('sinon');
const { APPLICATIONS, NOTIFICATION_FREQUENCIES } = require('../lib/config');
describe('Daily notifier tests', () => {
it('should prepare daily template for each of the applications', () => {
const notificationService = require('../lib/notificationService').createHandler({
commands: {},
simpleMailService: {},
});
const notifications = [
{
type: 'create_order',
frequency: NOTIFICATION_FREQUENCIES.DAILY,
},
{
type: 'create_order',
frequency: NOTIFICATION_FREQUENCIES.DAILY,
},
{
type: 'create_order',
frequency: NOTIFICATION_FREQUENCIES.MONTHLY,
},
];
const template = 'some html template as string';
sinon.stub(notificationService, 'getNotificationsByApplication').resolves(notifications);
sinon.stub(notificationService, 'prepareDailyTemplate').returns(template);
sinon.stub(notificationService, 'notifyToAdmin');
const sut = require('../lib/dailyNotifier').createHandler({
notificationService,
});
const event = {};
const context = {};
sut(event, context);
const dailyNotifications = [
{
type: 'create_order',
frequency: NOTIFICATION_FREQUENCIES.DAILY,
},
{
type: 'create_order',
frequency: NOTIFICATION_FREQUENCIES.DAILY,
}
];
sinon.assert.calledOnce(notificationService.prepareDailyTemplate);
sinon.assert.calledWith(notificationService.notifyToAdmin, template);
});
});
According to sinon the method prepareDailyTemplate is not called at all (0 times), but when I execute the test I can even see the console.log 'dailyTemplate', which means that the method has been executed once.
The error message:
AssertError: expected prepareDailyTemplate to be called once but was called 0 times
What I am doing wrong?
sut is an async function created by createHandler so it returns a Promise.
You just need to await the Promise that it returns:
it('should prepare daily template for each of the applications', async () => { // <= async
// ...
await sut(event, context); // <= await
// ...
sinon.assert.calledOnce(notificationService.prepareDailyTemplate); // Success!
});
I'm learning double tests with a simple example:
const Database = require('./Database')
const setupNewUser = (info, callback) => {
const user = {
name: info.name,
nameLowercase: info.name.toLowerCase()
}
try {
Database.save(user, callback)
} catch (err) {
callback(err)
}
}
module.exports = setupNewUser
I have a function that takes an object and a callback:
const Database = {
save: (user, callback) => {
callback(user)
}
}
module.exports = Database
How can test that save is called with both an object and a callback. Below is what I'm trying:
it('it calls Database.save with a callback', () => {
const saveSpy = sinon.spy(Database, 'save')
const arg = {
name: info.name,
nameLowercase: info.name.toLowerCase()
}
setupNewUser(info, function() {})
//I'm able to assert that save is called with arg sinon.assert.calledWith(saveSpy, arg)
sinon.assert.calledWith(saveSpy, arg, function() {}) //This is failing
})
You should .stub your API calls vs .spy. I would also recommend using sinon.sandbox so your test cleanup is easy to manage. Read about it here
describe('Setup new user', function() {
const sandbox = sinon.createSandbox();
afterEach(function() {
sandbox.restore();
});
it('should call Database.save with a callback', function(){
const databaseSaveStub = sandbox.stub(Database, 'save');
const user = {
name: 'Some Name',
nameLowercase: 'some name'
};
const callbackFn = sandbox.spy();
setupNewUser(user, callbackFn);
sinon.assert.calledOnce(Database.save);
sinon.assert.calledWith(databaseSaveStub, user, callbackFn);
});
});
I have created a method that grabs the image data based on another method. As you can see the uploadAvatar() method stores the object back from Cloudinary into a constructor object this._userImage;. Then my getUserImage() method should return the user image data but instead it returns {} that when I log it to the console :P.
ImageUploader.js
'use strict';
const cloudinary = require('cloudinary');
class ImageUploader {
constructor(imageObj) {
this.imageObj = imageObj;
this._apiKey = "key";
this._apiSecret = "secret";
this.config = cloudinary.config({
cloud_name: 'name',
api_key: this._apiKey,
api_secret: this._apiSecret
});
this._userImage = {}; // where user image object should be stored
}
* uploadAvatar(path) {
cloudinary.uploader.upload(path, (data) => {
this._userImage = data; // where I overwrite the constructor obj
});
}
getUserImage() {
return this._userImage; // this returns `{}` when I log it in UsersController.js
}
}
module.exports = ImageUploader;
UsersController.js
'use strict'
const Validator = require("../Helpers/ValidatorHelper.js");
const ImageUploader = require("../Helpers/ImageUploader.js");
class UsersController {
* registerView (request, response) {
yield response.sendView('users.register');
}
* register (request, response) {
const user = request.only('display_name', 'username', 'email', 'password', 'confirm_password', 'console');
var avatar = request.file('avatar');
let validator = new Validator(user, avatar);
let imageUpload = new ImageUploader(avatar);
let avatarStatus = yield validator.validateAvatar();
var img;
if (avatarStatus) { // is true
yield imageUpload.uploadAvatar(avatar.file.path);
} else { // is false
// pick a random avatar
}
console.log(imageUpload.getUserImage());
return response.status(200).json({
user: user,
avatar: imageUpload.getUserImage()
});
}
}
module.exports = UsersController
Not sure but here you could lost your this context.
cloudinary.uploader.upload(path, (data) => {
this._userImage = data; // where I overwrite the constructor obj
});
Check if 'this' there still links to your UsersController object
I know we should avoid using mixins , but let's use it for this case.
I defined a few logging in/out functions as a mixins module and I require it in a number of components.
var loginStore = require('../../stores/LoginStore'); // this is the problem ?
var AuthMixins = {
statics: {
willTransitionTo(transition) {
console.log(loginStore.id);
if (!loginStore.isLoggedIn()) {
transition.redirect('login');
}
}
},
_getLoginState() {
return {
userLoggedIn: loginStore.isLoggedIn()
};
},
componentDidMount() {
this.changeListener = this._onChange;
loginStore.addChangeListener(this.changeListener);
},
_onChange() {
this.setState(this._getLoginState());
},
getInitialState() {
return {
userLoggedIn: this._getLoginState()
};
},
componentWillUnmount: function() {
loginStore.removeChangeListener(this.changeListener);
}
};
module.exports = AuthMixins;
LoginStore.js :
var AppDispatcher = require('../dispatchers/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var LoginStore = (function() {
var _cred = null,
_user = null,
id = Date.now();
var eventEmitter = new EventEmitter();
var _listenToLogin = function(action) {
if (action.actionType === 'LOGIN_USER') {
_cred = action.cred;
_user = action.user;
eventEmitter.emit('CHANGE');
} else if (action.actionType === 'LOGOUT_USER') {
_cred = null;
_user = null;
eventEmitter.emit('CHANGE');
}
};
AppDispatcher.register(_listenToLogin.bind(this));
var user = function() {
return _user;
};
var cred = function() {
return _cred;
};
var addChangeListener = function(cb) {
eventEmitter.on('CHANGE', cb);
};
var removeChangeListener = function(cb) {
eventEmitter.removeListener('CHANGE', cb);
};
var isLoggedIn = function() {
return (!!_user);
};
return ({
id: id,
user: user,
cred: cred,
isLoggedIn: isLoggedIn,
addChangeListener: addChangeListener,
removeChangeListener: removeChangeListener
});
})();
module.exports = LoginStore;
As you can see I want LoginStore to be Singleton , so when I access it from different parts of the code , it has the same state.
The following is how I use mixins :
About.js :
'use strict';
var React = require('react');
var AuthMixins = require('./mixins/LoginMixins');
var About = React.createClass({
displayName: 'About',
mixins: [AuthMixins],
getDefaultProps: function() {
return {
message: 'Default Prop for About page'
};
},
render: function() {
return (<div>
{this.props.message}
</div>);
}
});
module.exports = About;
Now, the problem is that every time About is loaded , a new instance of LoginStore is loaded. (not respecting this fact that we want it to be singleton) in return it shows that the user is not logged in.