I'm trying to test React with Flux code using Jest. I'm reasonably new to unit testing.
I think I'm doing something wrong with Mocking my dependancies (to be honest the mocking thing kind of confuses me).
Here is what I'm having trouble with:
//LoginStore-test.js
jest.dontMock('../../constants/LoginConstants');
jest.dontMock('jsonwebtoken');
jest.dontMock('underscore');
jest.dontMock('../LoginStore');
describe("login Store", function(){
var LoginConstants = require('../../constants/LoginConstants');
var AppDispatcher;
var LoginStore;
var callback;
var jwt = require('jsonwebtoken');
var _user = {
email: 'test#test.com'
};
//mock actions
var actionLogin = {
actionType: LoginConstants.LOGIN_USER,
'jwt': jwt.sign(_user, 'shhh', { expiresInMinutes: 60*5 })
};
beforeEach(function(){
AppDispatcher = require('../../dispatchers/AppDispatcher');
LoginStore = require('../LoginStore');
callback = AppDispatcher.register.mock.calls[0][0];
});
...
it('should save the user', function(){
callback(actionLogin);
var user = LoginStore.getUser();
expect(user).toEqual(_user);
});
});
});
LoginStore.js file:
var AppDispatcher = require('../dispatchers/AppDispatcher');
var BaseStore = require('./BaseStore');
var LoginConstants = require('../constants/LoginConstants.js');
var _ = require('underscore');
var jwt = require('jsonwebtoken');
//initiate some variables
var _user;
var _jwt;
var LoginStore = _.extend({}, BaseStore, {
getUser: function(){
return _user;
}
});
AppDispatcher.register(function(action){
switch(action.actionType){
case LoginConstants.LOGIN_USER:
//set the user
_user = jwt.decode(action.jwt);
//save the token
_jwt = action.jwt;
break;
//do nothing with the default
default:
return true;
}
LoginStore.emitChange();
return true;
});
module.exports = LoginStore;
The jsonwebtoken functionality doesn't seem to be working at all. If I log actionLogin.jwt it just returns undefined. Any idea what I'm doing wrong here?
Cheers
After a bit of searching around, and actually trying to figure out a different issue I found the answer. just add
"jest": {"modulePathIgnorePatterns": ["/node_modules/"]}
to your package.json file
Related
My app is a Fluxible / React application.
I have the following spec that attempts to test a LoginForm. Embedded components have been stubbed using rewire. I referenced http://fluxible.io/api/components.html#testing.
The first spec it("renders") passes. However, when I try to do more tests as shown in the commented code, the test fails.
I am unable to assert on LoginForm's state or trigger simulated events using TestUtils on the component. Are there any ways to do that?
import React from 'react/addons';;
import { createMockComponentContext } from 'fluxible/utils';
import createStore from 'fluxible/addons/createStore';
var rewire = require("rewire");
var rewireModule = require("../../helpers/rewire-module");
// stub inner components with LoginForm
// `rewire` instead of `require`
var LoginForm = rewire("../../../src/components/auth/login-form");
// Replace the required module with a stub component.
rewireModule(LoginForm, {
FormattedMessage: React.createClass({
render: function() { return <div />; }
}),
NavLink: React.createClass({
render: function() { return <div />; }
})
});
describe('LoginForm', function() {
var context;
var TestUtils;
var provideContext;
var connectToStores;
var MockIntlStore;
var MockAuthStore;
var noop = function(){};
var component;
beforeEach(function(){
MockIntlStore = createStore({
storeName: 'IntlStore',
getMessage: noop,
getState: function(){
return {}
}
});
MockAuthStore = createStore({
storeName: 'AuthStore'
});
context = createMockComponentContext({
stores: [MockIntlStore, MockAuthStore]
});
// React must be required after window is set
TestUtils = React.addons.TestUtils
provideContext = require('fluxible/addons/provideContext');
connectToStores = require('fluxible/addons/connectToStores');
// Wrap with context provider and store connector
LoginForm = provideContext(connectToStores(LoginForm, [MockIntlStore, MockAuthStore], function (stores) {
return {
};
}));
component = TestUtils.renderIntoDocument(
<LoginForm context={context} />
);
});
it("renders", function() {
var foundComponent = TestUtils.findRenderedDOMComponentWithClass(
component, 'login-form');
expect(foundComponent).toBeDefined();
});
// TODO fluxible wraps components so we cant reach the inner component to assert on state and trigger event handlers
// it("should have an initial state", function() {
// let initialState = {
// username: '',
// pass: ''
// }
// expect(component.state).toEqual(initialState);
// });
});
When you use provideContext and connectToStores, your component is wrapped. You have done it right to find the component using TestUtils. findRenderedDOMComponentWithClass, Simply use the foundComponent for test, that is what is being tested. i.e.
...
var foundComponent = TestUtils.findRenderedDOMComponentWithClass(
component, 'login-form');
expect(foundComponent.state).toEqual(initialState);
...
If you're still looking for a solution:
var tbg = React.createElement(x, { di: serviceLocator });
var renderer = React.addons.TestUtils.createRenderer();
var rtbg = renderer.render(tbg);
Then your method is here:
renderer._instance._instance.myMethod
Where myMethod is a function member of component x
I would like to stub the save method available to Mongoose models. Here's a sample model:
/* model.js */
var mongoose = require('mongoose');
var userSchema = mongoose.Schema({
username: {
type: String,
required: true
}
});
var User = mongoose.model('User', userSchema);
module.exports = User;
I have some helper function that will call the save method.
/* utils.js */
var User = require('./model');
module.exports = function(req, res) {
var username = req.body.username;
var user = new User({ username: username });
user.save(function(err) {
if (err) return res.end();
return res.sendStatus(201);
});
};
I would like to check that user.save is called inside my helper function using a unit test.
/* test.js */
var mongoose = require('mongoose');
var createUser = require('./utils');
var userModel = require('./model');
it('should do what...', function(done) {
var req = { username: 'Andrew' };
var res = { sendStatus: sinon.stub() };
var saveStub = sinon.stub(mongoose.Model.prototype, 'save');
saveStub.yields(null);
createUser(req, res);
// because `save` is asynchronous, it has proven necessary to place the
// expectations inside a setTimeout to run in the next turn of the event loop
setTimeout(function() {
expect(saveStub.called).to.equal(true);
expect(res.sendStatus.called).to.equal(true);
done();
}, 0)
});
I discovered var saveStub = sinon.stub(mongoose.Model.prototype, 'save') from here.
All is fine unless I try to add something to my saveStub, e.g. with saveStub.yields(null). If I wanted to simulate an error being passed to the save callback with saveStub.yields('mock error'), I get this error:
TypeError: Attempted to wrap undefined property undefined as function
The stack trace is totally unhelpful.
The research I've done
I attempted to refactor my model to gain access to the underlying user model, as recommended here. That yielded the same error for me. Here was my code for that attempt:
/* in model.js... */
var UserSchema = mongoose.model('User');
User._model = new UserSchema();
/* in test.js... */
var saveStub = sinon.stub(userModel._model, 'save');
I found that this solution didn't work for me at all. Maybe this is because I'm setting up my user model in a different way?
I've also tried Mockery following this guide and this one, but that was way more setup than I thought should be necessary, and made me question the value of spending the time to isolate the db.
My impression is that it all has to do with the mysterious way mongoose implements save. I've read something about it using npm hooks, which makes the save method a slippery thing to stub.
I've also heard of mockgoose, though I haven't attempted that solution yet. Anyone had success with that strategy? [EDIT: turns out mockgoose provides an in-memory database for ease of setup/teardown, but it does not solve the issue of stubbing.]
Any insight on how to resolve this issue would be very appreciated.
Here's the final configuration I developed, which uses a combination of sinon and mockery:
// Dependencies
var expect = require('chai').expect;
var sinon = require('sinon');
var mockery = require('mockery');
var reloadStub = require('../../../spec/utils/reloadStub');
describe('UNIT: userController.js', function() {
var reportErrorStub;
var controller;
var userModel;
before(function() {
// mock the error reporter
mockery.enable({
warnOnReplace: false,
warnOnUnregistered: false,
useCleanCache: true
});
// load controller and model
controller = require('./userController');
userModel = require('./userModel');
});
after(function() {
// disable mock after tests complete
mockery.disable();
});
describe('#createUser', function() {
var req;
var res;
var status;
var end;
var json;
// Stub `#save` for all these tests
before(function() {
sinon.stub(userModel.prototype, 'save');
});
// Stub out req and res
beforeEach(function() {
req = {
body: {
username: 'Andrew',
userID: 1
}
};
status = sinon.stub();
end = sinon.stub();
json = sinon.stub();
res = { status: status.returns({ end: end, json: json }) };
});
// Reset call count after each test
afterEach(function() {
userModel.prototype.save.reset();
});
// Restore after all tests finish
after(function() {
userModel.prototype.save.restore();
});
it('should call `User.save`', function(done) {
controller.createUser(req, res);
/**
* Since Mongoose's `new` is asynchronous, run our expectations on the
* next cycle of the event loop.
*/
setTimeout(function() {
expect(userModel.prototype.save.callCount).to.equal(1);
done();
}, 0);
});
}
}
Have you tried:
sinon.stub(userModel.prototype, 'save')
Also, where is the helper function getting called in the test? It looks like you define the function as the utils module, but call it as a method of a controller object. I'm assuming this has nothing to do with that error message, but it did make it harder to figure out when and where the stub was getting called.
I have the following code in my route for receiving webocket updates of my models. The problem is that when this line executes
setTimeout(self.stompConnect, 10000);
I no longer have access to the Ember.Route Ember object at the top of the stompConnect method.
var self = this; //no longer pointing to my route
How can I maintain the ember context across the callback in a third pary library like this? This has nothing to do with websockets or the library because I had the same issue with another third party library that had a callback.
I guess I need to use .bind() or something but I don't know the correct syntax.
stompClient : null,
activate : function() {
this.stompConnect();
},
stompConnect : function() {
var self = this;
var connectCallback = function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/models/update', function(payload){
var model = JSON.parse(JSON.parse(payload.body));
var modelName = Object.keys(model)[0];
var modelPayload = model[modelName];
self.store.push(modelName, modelPayload);
});
};
var errorCallback = function (error) {
console.log('STOMP: ' + error);
setTimeout(self.stompConnect, 10000); //when stompConnect() is called, the ember context is lost :(
console.log('STOMP: Reconecting in 10 seconds');
};
var url = ... ;
var socket = new SockJS(url);
var stompClient = Stomp.over(socket);
stompClient.connect({}, connectCallback, errorCallback);
this.set('stompClient', stompClient);
},
deactivate : function() {
this.get('stompClient').disconnect();
},
Basically you have a callback inside a callback. So context needs to be passed in both callbacks. self will work in errorCallBack but needs to be set again to work in stompConnect. I would rather suggest using run.later to setTimeOut. So Here goes the code.
stompClient : null,
activate : function() {
this.stompConnect();
},
stompConnect : function() {
var self = this;
var connectCallback = function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/models/update', function(payload){
var model = JSON.parse(JSON.parse(payload.body));
var modelName = Object.keys(model)[0];
var modelPayload = model[modelName];
self.store.push(modelName, modelPayload);
});
};
var errorCallback = function (error) {
console.log('STOMP: ' + error);
Ember.run.later(this, this.stompConnect, 1000);
//or you can also use
//setTimeout(this.stompConnect.bind(this), 10000); when stompConnect() is called, the ember context is lost :(
console.log('STOMP: Reconecting in 10 seconds');
};
var url = ... ;
var socket = new SockJS(url);
var stompClient = Stomp.over(socket);
stompClient.connect({}, connectCallback, errorCallback.bind(this));
this.set('stompClient', stompClient);
},
deactivate : function() {
this.get('stompClient').disconnect();
}
I prefer using .bind() rather var self = this;. But it depends.
Ember 1.5.1
Ember-Data 1.0 beta 7
I've tried to modify the DS.ActiveModelAdapter's findMany so it'll get in chunks of 40... this is because I can't use the links feature and it seems to be generating 400 errors because it has too many ids in the URL its creating.
I tried using this adapter, but I keep getting error messages that look like this:
Error: Assertion Failed: Error: no model was found for 'super'
Here's my Adapter:
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
findMany: function(store, type, ids) {
self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
var idsPerRequest = 40;
var totalIdsLength = ids.length;
var numberOfBins = Math.ceil( totalIdsLength / idsPerRequest ); // number per bin
var bins = [];
ids.forEach( function(someId, index) {
var thisBinIndex = index % numberOfBins;
var thisBin = Ember.A( bins[thisBinIndex] );
thisBin.pushObject(someId);
bins[thisBinIndex] = thisBin;
});
var requestPromises = bins.map(function(binOfIds) {
return self.ajax(self.buildURL(type.typeKey), 'GET', { data: { ids: binOfIds } });
});
Ember.RSVP.all(requestPromises).then(function(resolvedBinRequests) {
var resolvedObjects = Em.A([]);
resolvedBinRequests.forEach(function(resolvedBin) {
resolvedObjects.addObjects(resolvedBin);
});
resolve(resolvedObjects);
}, function(error) {
reject(error);
});
});
}
});
Can anyone help me out with this? It'd be really appreciated. Am I just missing something obvious or have I perhaps done something silly?
Thanks in advance!
[edit] Okay so further to this I've figured out why it's not working, and that's because the response that's coming back is a promise for the JSON payload, but what I'm doing is joining multiples of these into an array and returning that... which obviously won't be right... but what I need to do is merge the arrays inside the objects returned into one, I think (in concept)... I'm not really sure how to do this in actuality, though... I've tried various things, but none of them seem to work well... :(
I'm not sure how much control you have over the back-end, but this seems like a perfect use case for using links instead of returning all of the ids.
App.Foo = DS.Model.extend({
bars: DS.hasMany('bar', {async:true})
});
App.Bar = DS.Model.extend({
name: DS.attr()
});
Then when you query for foo your json returns a link instead of a list of ids
{
foo: {
id:1,
links: {
bars: '/foo/1/bars' // or anything, you could put /bars?start=1&end=9000
}
}
}
Here's an example with 1000 relationship records hitting a simple endpoint:
http://emberjs.jsbin.com/OxIDiVU/579/edit
Okay so I finally worked out how to make this work.
I'll share my answer here for future posterity ;-)
Of interest is that the required response had to be a promise and it had to contain a straight up JS object, so I "munged" all the responses into one JS object and manually built the pluralized camelized type key... I wasn't sure how else to do this. So... sorry it's so hacky, but this actually works and lets me fix my app for now until the "links" feature is working again.
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
findMany: function(store, type, ids) {
self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
var idsPerRequest = 40;
var totalIdsLength = ids.length;
var numberOfBins = Math.ceil( totalIdsLength / idsPerRequest ); // number per bin
var bins = [];
ids.forEach( function(someId, index) {
var thisBinIndex = index % numberOfBins;
var thisBin = Ember.A( bins[thisBinIndex] );
thisBin.pushObject(someId);
bins[thisBinIndex] = thisBin;
});
// build an array of promises, then resolve using Ember.RSVP.all
var requestPromises = bins.map(function(binOfIds) {
return self.ajax(self.buildURL(type.typeKey), 'GET', { data: { ids: binOfIds } });
});
// build the required return object, which is a promise containing a plain JS object
// note this can't be an Ember object
Ember.RSVP.all(requestPromises).then(function(resolvedBinRequests) {
var pluralizedDecamelizedTypeKey = type.typeKey.decamelize().pluralize();
var resolvedObjects = Em.A([]);
var returnObject = {};
returnObject[pluralizedDecamelizedTypeKey] = resolvedObjects;
resolvedBinRequests.forEach(function(resolvedBin) {
var theArray = resolvedBin[pluralizedDecamelizedTypeKey];
resolvedObjects.addObjects(theArray);
});
var responsePromise = Ember.RSVP.Promise.cast(returnObject);
resolve(responsePromise);
}, function(error) {
reject(error);
});
});
}
});
After some feedback I updated this response to attempt to extract the response payloads in the serializer instead of attempting to mimic the store's logic in the adapter.
http://emberjs.jsbin.com/wegiy/60/edit
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
findMany: function(store, type, ids) {
// build an array of promises, then resolve using Ember.RSVP.all
var idsPerRequest = 40;
var totalIdsLength = ids.length;
var numberOfBins = Math.ceil( totalIdsLength / idsPerRequest ); // number per bin
var bins = [];
ids.forEach( function(someId, index) {
var thisBinIndex = index % numberOfBins;
var thisBin = Ember.A( bins[thisBinIndex] );
thisBin.pushObject(someId);
bins[thisBinIndex] = thisBin;
});
var requestPromises = bins.map(function(binOfIds) {
return self.ajax(self.buildURL(type.typeKey), 'GET', { data: { ids: binOfIds } });
});
return Ember.RSVP.all(requestPromises);
}
});
App.ApplicationSerializer = DS.ActiveModelSerializer.extend({
extractFindMany: function(store, type, responsePayloads) {
// responsePayloads is the resolved value from the Ember.RSVP.all(requestPromises) promise
var serializer = this;
var extractedResponses = responsePayloads.map(function(payload) {
return serializer.extractArray(store, type, payload);
});
// extractedResponses is an array of arrays. We need to flatten it into 1 array.
return [].concat.apply([], extractedResponses);
}
});
I'm pretty new to angular and been wanting to test drive and I've hit a snag mocking out $window. The item in which I'm attempting to test is very simple but important -- I need to know if localStorage is there or not and need to be able to fake out $window to do so.
The code is very basic so far and what I have is this for the service ...
'use strict';
mainApp.factory('somedataStorage',function($window) {
var local = $window.localStorage;
return {
hasLocal: function() {
return local != undefined;
},
};
});
How I'm testing it is this ...
(function () {
var fakeWin = {
localStorage: null
};
var $injector = angular.injector(['ngMock', 'ng', 'mainApp']);
//var $window = $injector.get('$window');
var init = {
setup: function () {
//this.$window = fakeWin;
},
}
module('LocalStorageTests', init);
test("if localstorage isn't there, say so", function () {
var $service = $injector.get('somedataStorage' /*, {$window: fakeWin} */);
ok(!$service.hasLocal, "no local storage");
});
})();
So what am I missing?