Editor: Check out this guest blog post by Igor Ribeiro Lima on how to perform JavaScript unit testing using dependency injection.
You probably already know that to do JavaScript testing well, you need to make sure you are testing the following:
- Injecting mocks for other modules
- Leaking private variables
- Overriding variables within the module
rewire is a tool for helping test for the above. It provides an easy way to perform dependency injection, plus adds a special `setter` and `getter` to modules so you can modify their behaviour. What rewire doesn’t do is load the file and evaluate the contents to emulate the require mechanism. It actually uses Node’s own `require` to load the module.
To get started with dependency injection, we’ll create a twitter rest api server and do unit tests using mocks and overriding variables within modules. This example will focus on back-end unit testing, but if you want to use rewire also on the client-side take a look at client-side bundlers.
An example
This example is a public HTTP API that retrieves Twitter user timelines. It has basically two files: server.js and twitter.js.
The first file creates an basic instance of express and defines a route for a GET request method, which is /twitter/timeline/:user
.
The second file is a module responsible for retrieving data from Twitter. It requires:
- twit: Twitter API Client for node
- async: is a utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript
- moment: a lightweight JavaScript date library for parsing, validating, manipulating, and formatting dates.
Part of these modules will be mocked and overridden in our tests.
Running the example
This example is already running in a cloud. So you can reach the urls below and see it working:
To run it locally, clone this repo with:
`git clone [email protected]:igorlima/twitter-rest-api-server.git twitter-rest-api-server`
…and set five environment variables. Those envs are listed below. For security reasons I won’t share my token. To get yours, access Twitter developer documentation, create a new app and set up your credentials.
For Mac users, you can simply type:
export TwitterConsumerKey="xxxx"
export TwitterConsumerSecret="xxxx"
export TwitterAccessToken="xxxx"
export TwitterAccessTokenSecret="xxxx"
export MomentLang="pt-br"
For Windows users, do:
SET TwitterConsumerKey="xxxx"
SET TwitterConsumerSecret="xxxx"
SET TwitterAccessToken="xxxx"
SET TwitterAccessTokenSecret="xxxx"
SET MomentLang="pt-br"
After setting up the environment variables, go to twitter-rest-api-server
folder, install all node dependencies by npm install
, then run via terminal node server.js
. It should be available at the port 5000
. Go to your browser and reach http://localhost:5000/twitter/timeline/igorribeirolima
.
Writing unit tests
Mocha is the JavaScript test framework we’ll use in this example. It makes asynchronous testing simple and fun. Mocha allows you to use any assertion library you want, if it throws an error, it will work! In this example we are gonna utilize Node’s regular assert module.
Let’s say you want to test the twitter.js code:
var Twit = require('twit'),
async = require('async'),
moment = require('moment'),
T = new Twit({
consumer_key: process.env.TwitterConsumerKey || '...',
consumer_secret: process.env.TwitterConsumerSecret || '...',
access_token: process.env.TwitterAccessToken || '...',
access_token_secret: process.env.TwitterAccessTokenSecret || '...'
}),
mapReducingTweets = function(tweet, callback) {
callback(null, simplify(tweet));
},
simplify = function(tweet) {
var date = moment(tweet.created_at, "ddd MMM DD HH:mm:ss zz YYYY");
date.lang( process.env.MomentLang );
return {
date: date.format('MMMM Do YYYY, h:mm:ss a'),
id: tweet.id,
user: {
id: tweet.user.id
},
tweet: tweet.text
};
};
module.exports = function(username, callback) {
T.get("statuses/user_timeline", {
screen_name: username,
count: 25
}, function(err, tweets) {
if (err) callback(err);
else async.map(tweets, mapReducingTweets, function(err, simplified_tweets) {
callback(null, simplified_tweets);
});
})
};
To do that in a easy and fun way, let’s load this module using rewire. So, within your test module twitter-spec.js:
var rewire = require('rewire'),
assert = require('assert'),
twitter = rewire('./twitter.js'),
mock = require('./twitter-spec-mock-data.js');
rewire acts exactly like `require`. With just one difference: Your module will now export a special `setter` and `getter` for private variables.
myModule.__set__("path", "/dev/null");
myModule.__get__("path"); // = '/dev/null'
This allows you to mock everything in the top-level scope of the module, like the twitter module for example. Just pass the variable name as first parameter and your mock as second.
You may also override globals. These changes are only within the module, so you don’t have to be concerned that other modules are influenced by your mock.
describe('twitter module', function(){
describe('simplify function', function(){
var simplify;
before(function() {
simplify = twitter.__get__('simplify');
});
it('should be defined', function(){
assert.ok(simplify);
});
describe('simplify a tweet', function(){
var tweet, mock;
before(function() {
mock = mocks[0];
tweet = simplify(mock);
});
it('should have 4 properties', function() {
assert.equal( Object.keys(tweet).length, 4 );
});
describe('format dates as `MMMM Do YYYY, h:mm:ss a`', function() {
describe('English format', function() {
before(function() {
revert = twitter.__set__('process.env.MomentLang', 'en');
tweet = simplify(mock);
});
it('should be `March 6th 2015, 2:29:13 am`', function() {
assert.equal(tweet.date, 'March 6th 2015, 2:29:13 am');
});
after(function(){
revert();
});
});
describe('Brazilian format', function() {
before(function() {
revert = twitter.__set__('process.env.MomentLang', 'pt-br');
tweet = simplify(mock);
});
it('should be `Março 6º 2015, 2:29:13 am`', function() {
assert.equal(tweet.date, 'Março 6º 2015, 2:29:13 am');
});
after(function(){
revert();
});
});
});
});
});
describe('retrieve timeline feed', function() {
var revert;
before(function() {
revert = twitter.__set__("T.get", function( api, query, callback ) {
callback( null, mocks);
});
});
describe('igorribeirolima timeline', function() {
var tweets;
before(function(done){
twitter('igorribeirolima', function(err, data) {
tweets = data;
done();
});
});
it('should have 19 tweets', function() {
assert.equal(tweets.length, 19);
});
});
after(function() {
revert();
});
});
});
__set__
returns a function which reverts the changes introduced by this particular __set__
call.
Running unit tests
Before we get into the test and walk through it, let’s install the `mocha` CLI with npm install -g mocha
. It will allow us to run our tests by just typing mocha twitter-spec.js
. The following is an image that illustrates the test result.
Check out this video and see step by step in detail everything discussed so far.
Conclusion
As you can see, with rewire it’s not too painful to test:
- injecting mocks for other modules
- leaking private variables
- overriding variables within the module
That’s it folks. I hope you learned just how simple and fun is to do dependency injection unit testing. Thanks for reading!