How to Write Unit Tests for Flux Stores using Mocha, Chai, and Sinon
Apr 9, 2020 • 11 Minute Read
Introduction
Stores in Flux are comparable to models in MVC applications. Although they do not serve as a single model or collection, they store and maintain the domain within the application. You could say that stores retrieve data from the models of the said domain and use it to present the API with the state of the application.
In this guide, you'll learn how to create a store. This case study imagines that you have a blogging platform and you wish to code a list of posts for it. The guide covers two features:
- Showing your published posts and the ability to sort them by date
- Creating new drafts and listing the old ones
Getting Started
This guide uses Mocha, Chai, and Sinon as a test runner, an asserting library, and a spy on your methods respectively. Start as follows:
var expect = require('chai').expect;
describe('Post List Store', function() {
it ('exists', function() {
expect(PostListStore).to.exist;
});
});
The above code is a simple test. Read on to bring your PostListStore to life.
var PostsListStore = {};
module.exports = PostsListStore;
Showing and Sorting Posts
Now, try to code a test so that the store can reveal the list of published posts to the rest of the application.
Before you start, you need to understand the type and structure of your raw data, i.e., the data you will use when you test the code. As an example in this guide, this is your data:
var Posts = [
{
title: 'Creating Flux Store driven by tests',
author: 'cparker',
excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris at.',
published_at: 'Nov 12 2019',
},
{
title: 'The most beautiful draft ever made by ...',
author: 'cparker',
excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris at.',
published_at: null
},
{
title: 'Best free stock photos',
author: 'cparker',
excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris at.',
published_at: 'Nov 1 2019'
},
{
title: 'Spookiest Halloween costumes',
author: 'cparker',
excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris at.',
published_at: 'Oct 30 2019'
}
];
Use the data above as your in-memory database. The posts array must be available when the store queries for data. In a real-life application, replace this array with your model or collection or both.
var expect = require('chai').expect;
var mockery = require('mockery');
var Posts = require('./mockedPosts.js');
var PostsListStore;
describe('Post List Store', function() {
beforeEach(function() {
mockery.enable({
warnOnReplace: true,
warnOnUnregistered: false,
useCleanCache: true,
});
mockery.registerMock('./posts.js', Posts);
PostsListStore = require('../src/PostsListStore.js');
});
it ('exists', function() {
expect(PostsListStore).to.exist;
});
describe ('get published posts', function() {
it ('returns only posts with a published date', function() {
var posts = PostsListStore.getPublishedPosts();
posts.forEach(function(post) {
expect(post.published_at).to.not.be.null;
});
});
});
afterEach(function() {
mockery.disable();
});
});
Next, use the following code for the test to pass:
var Posts = require('./posts.js');
var PostsListStore = {
getPublishedPosts: function() {
return Posts.filter(function(post) {
return post.published_at;
});
},
};
Stores fundamentally provide a read-only API. In other words, theoretically speaking, everything including dependency injection can now open an API on the object of your test. This means you can re-inject a mutated model into the store and bypass its capacity.
The guide employs Mockery, a dynamic lib that allows you to mock the required modules. You just need to put your mocked posts array in place of the required “posts.js”.
Another option is using Jest. However, Jest mocks everything. A better approach is knowing precisely what you are mocking. Mocking can be a sign that you have created a dependency on the subject of your test. Such signs can help when it comes to the confidence of your test.
Now you can retrieve the published posts. Next you have to sort them by date.
You may think about checking whether each post's published date is smaller than the one that came before it:
it ('returned posts are sorted by date', function() {
var posts = PostsListStore.getPublishedPosts();
var previous_date = new Date();
posts.forEach(function(post) {
var published_at = new Date(post.published_at);
expect(published_at.getTime()).to.be.below(previous_date.getTime());
previous_date = published_at;
});
});
This is not an efficient approach. Try using a sort call instead:
var Posts = require('./posts.js');
var PostsListStore = {
getPublishedPosts: function() {
var posts = Posts.filter(function(post) {
return post.published_at;
});
posts.sort(function(a_post, another_post) {
return new Date(another_post.published_at) - new Date(a_post.published_at);
});
return posts;
}
};
module.exports = PostsListStore;
You have created the first feature and learned how to mock data in order to test your store. Let's move on the next feature.
Creating New Drafts and Displaying the Full List
Assuming drafts are not published yet, their published date is null:
describe ('get the drafts', function() {
it ('returns posts without a published_at date', function() {
var posts = PostsListStore.getDrafts();
posts.forEach(function(post) {
expect(post.published_at).to.be.null;
});
});
});
This test is simple enough:
getDrafts: function() {
return Posts.filter(function(post) {
return post.published_at === null;
});
},
Read on to learn how to add a new draft and indicate the change in the store state to the subscribed components.
Remember that stores are read-only. This means that you cannot add a new draft by calling the addDraft() method as it may result in the collection mutation. On the other hand, views in Flux would dispatch an event:
it ('catches dispatched event and creates a new draft', function() {
Dispatcher.dispatch({
actionType: ActionTypes.NEW_DRAFT,
draft: {
title: 'Productivity tips with Git',
author: 'cparker',
excerpt: ''
}
});
var drafts = PostsListStore.getDrafts();
var new_draft_is_in_array = false;
drafts.forEach(function(draft) {
new_draft_is_in_array = draft.title == 'Productivity tips using Git';
});
expect(new_draft_is_in_array).to.be.true;
});
Next, you must register the store to the dispatcher and create a draft once NEW_DRAFT is triggered:
Dispatcher.register(function(payload) {
switch(payload.actionType) {
case ActionTypes.NEW_DRAFT:
var draft = payload.draft;
draft.published_at = null;
Posts.push(draft);
break;
}
});
The model now is mutated by the dispatcher’s register callback.
Now the modification in your data must be broadcasted to the other subscribed components, otherwise they will not be aware that the state of the application has changed.
it ('a callback can be subscribed to the Store changes', function() {
var callback = sinon.spy();
PostsListStore.addChangeListener(callback);
PostsListStore.emitChange();
expect(callback.calledOnce).to.be.true;
});
Next, introduce the EventEmitter properties to your store:
var Posts = require('./posts.js');
var Dispatcher = require('./dispatcher.js');
var ActionTypes = require('./actionTypes.js');
var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var CHANGE_EVENT = 'change';
var PostsListStore = assign({}, EventEmitter.prototype, {
getDrafts: function() {
return Posts.filter(function(post) {
return post.published_at === null;
});
},
getPublishedPosts: function() {
var posts = Posts.filter(function(post) {
return post.published_at;
});
posts.sort(function(a_post, another_post) {
return new Date(another_post.published_at) - new Date(a_post.published_at);
});
return posts;
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
emitChange: function() {
this.emit(CHANGE_EVENT);
}
});
But you also need to ensure that you can avoid these events by unsubscribing:
it ('a callback can be unsubscribed to the Store changes', function() {
var callback = sinon.spy();
PostsListStore.addChangeListener(callback);
PostsListStore.emitChange();
PostsListStore.removeChangeListener(callback);
PostsListStore.emitChange();
expect(callback.calledOnce).to.be.true;
});
You can achieve this by writing a method that simply reverses the addChangeListener:
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
Finally, ensure that CHANGE_EVENT is emitted whenever a new draft is created:
it ('emits a change event when a draft has been created', function() {
var callback = sinon.spy();
PostsListStore.addChangeListener(callback);
Dispatcher.dispatch({
actionType: ActionTypes.NEW_DRAFT,
draft: {}
});
expect(callback.calledOnce).to.be.true;
});
Now, once a draft is added, you can emit the changes as follows:
Dispatcher.register(function(payload) {
switch(payload.actionType) {
case ActionTypes.NEW_DRAFT:
var draft = payload.draft;
draft.published_at = null;
Posts.push(draft);
PostsListStore.emitChange();
break;
}
});
Conclusion
This guide used the example of a blogging platform to provide you with some simple tests and introduced a modest store with interesting features.