Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 31d47c7

Browse files
committedMar 18, 2015
0 parents  commit 31d47c7

File tree

6 files changed

+3679
-0
lines changed

6 files changed

+3679
-0
lines changed
 

‎README.md

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
[![Gittip Donate Button](http://img.shields.io/gratipay/igorribeirolima.svg)](https://gratipay.com/igorribeirolima/)
2+
3+
# JS unit testing using dependency injection
4+
5+
You probably know that to do JavaScript testing is good and some hurdles to overcome is how to test our code in a manner to *(i)* inject mocks for other modules, *(ii)* to leak private variables or *(iii)* override variables within the module.
6+
7+
[rewire](https://github.com/jhnns/rewire) is a tool for helping us on overcoming these hurdles. It provides us an easy way to dependency injection for unit testing and adds a special setter and getter to modules so we can modify their behaviour for better unit testing. What [rewire](https://github.com/jhnns/rewire) does is to not load the file and eval the contents to emulate the load mechanism.
8+
9+
To get started with dependency injection, we'll create [a twitter rest api server](https://gist.github.com/igorlima/b31f1a26a5b100186a98) 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](https://github.com/jhnns/rewire) also on the client-side take a look at [client-side bundlers](https://github.com/jhnns/rewire#client-side-bundlers).
10+
11+
## An example
12+
13+
This example is a public HTTP API to retrieve Twitter user timelines. It has basically two files: [server.js](https://gist.github.com/igorlima/b31f1a26a5b100186a98#file-server-js) and [twitter.js](https://gist.github.com/igorlima/b31f1a26a5b100186a98#file-twitter-js).
14+
15+
The first file creates [an basic instance](http://expressjs.com/starter/hello-world.html) of [express](http://expressjs.com) and defines [a route for a GET request method](http://expressjs.com/starter/basic-routing.html), which is ``/twitter/timeline/:user``.
16+
17+
The second one is a module responsible for retrieving data from Twitter. It requires:
18+
19+
* [**twit**](https://github.com/ttezel/twit): Twitter API Client for node
20+
* [**async**](https://github.com/caolan/async): is a utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript
21+
* [**moment**](http://momentjs.com): a lightweight JavaScript date library for parsing, validating, manipulating, and formatting dates.
22+
23+
Part of these modules will be mocked and overridden in our tests.
24+
25+
## Running the example
26+
27+
This example is already running [in a cloud](https://social-media-rest-api.herokuapp.com). So you can reach the urls below and see it working:
28+
29+
* [igorribeirolima timeline](https://social-media-rest-api.herokuapp.com/twitter/timeline/igorribeirolima)
30+
* [strongloop timeline](https://social-media-rest-api.herokuapp.com/twitter/timeline/strongloop)
31+
* [tableless timeline](https://social-media-rest-api.herokuapp.com/twitter/timeline/tableless)
32+
33+
To run it locally, clone [this gist](https://gist.github.com/igorlima/b31f1a26a5b100186a98) by ``git clone https://gist.github.com/b31f1a26a5b100186a98.git twitter-rest-api-server`` and set five environment variables. Those envs are listed below. For secure reason I won't share my token. To get yours, access [Twitter developer documentation](https://dev.twitter.com/overview/documentation), [create a new app](https://apps.twitter.com) and set up your credentials.
34+
35+
For [Mac users](http://stackoverflow.com/questions/7501678/set-environment-variables-on-mac-os-x-lion), you can simply type:
36+
37+
```
38+
export TwitterConsumerKey="xxxx"
39+
export TwitterConsumerSecret="xxxx"
40+
export TwitterAccessToken="xxxx"
41+
export TwitterAccessTokenSecret="xxxx"
42+
export MomentLang="pt-br"
43+
```
44+
45+
For [Windows users](http://stackoverflow.com/questions/21606419/set-windows-environment-variables-with-commandline-cmd-commandprompt-batch-file), do:
46+
47+
```
48+
SET TwitterConsumerKey="xxxx"
49+
SET TwitterConsumerSecret="xxxx"
50+
SET TwitterAccessToken="xxxx"
51+
SET TwitterAccessTokenSecret="xxxx"
52+
SET MomentLang="pt-br"
53+
```
54+
55+
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``.
56+
57+
![running express app example locally](http://i1368.photobucket.com/albums/ag182/igorribeirolima/running%20express%20app%20example%20locally_zpsndaidg4w.png)
58+
59+
## Writing unit tests
60+
61+
[Mocha](http://mochajs.org) is the JavaScript test framework running we gonna use. It makes asynchronous testing simple and fun. [Mocha](http://mochajs.org) 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](https://nodejs.org/api/assert.html) module.
62+
63+
Imagine you want to test this code [twitter.js](https://gist.github.com/igorlima/b31f1a26a5b100186a98#file-twitter-js):
64+
65+
```javascript
66+
var Twit = require('twit'),
67+
async = require('async'),
68+
moment = require('moment'),
69+
T = new Twit({
70+
consumer_key: process.env.TwitterConsumerKey || '...',
71+
consumer_secret: process.env.TwitterConsumerSecret || '...',
72+
access_token: process.env.TwitterAccessToken || '...',
73+
access_token_secret: process.env.TwitterAccessTokenSecret || '...'
74+
}),
75+
76+
mapReducingTweets = function(tweet, callback) {
77+
callback(null, simplify(tweet));
78+
},
79+
80+
simplify = function(tweet) {
81+
var date = moment(tweet.created_at, "ddd MMM DD HH:mm:ss zz YYYY");
82+
date.lang( process.env.MomentLang );
83+
return {
84+
date: date.format('MMMM Do YYYY, h:mm:ss a'),
85+
id: tweet.id,
86+
user: {
87+
id: tweet.user.id
88+
},
89+
tweet: tweet.text
90+
};
91+
};
92+
93+
module.exports = function(username, callback) {
94+
T.get("statuses/user_timeline", {
95+
screen_name: username,
96+
count: 25
97+
}, function(err, tweets) {
98+
if (err) callback(err);
99+
else async.map(tweets, mapReducingTweets, function(err, simplified_tweets) {
100+
callback(null, simplified_tweets);
101+
});
102+
})
103+
};
104+
```
105+
106+
To do that in a easy and fun way, let load this module using [rewire](https://github.com/jhnns/rewire). So within your test module [twitter-spec.js](https://gist.github.com/igorlima/b31f1a26a5b100186a98#file-twitter-spec-js):
107+
108+
```javascript
109+
var rewire = require('rewire'),
110+
assert = require('assert'),
111+
twitter = rewire('./twitter.js'),
112+
mock = require('./twitter-spec-mock-data.js');
113+
```
114+
115+
[rewire](https://github.com/jhnns/rewire) acts exactly like *require*. Just with one difference: Your module will now export a special setter and getter for private variables.
116+
117+
```javascript
118+
myModule.__set__("path", "/dev/null");
119+
myModule.__get__("path"); // = '/dev/null'
120+
```
121+
122+
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.
123+
124+
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.
125+
126+
```javascript
127+
describe('twitter module', function(){
128+
129+
describe('simplify function', function(){
130+
var simplify;
131+
132+
before(function() {
133+
simplify = twitter.__get__('simplify');
134+
});
135+
136+
it('should be defined', function(){
137+
assert.ok(simplify);
138+
});
139+
140+
describe('simplify a tweet', function(){
141+
var tweet, mock;
142+
143+
before(function() {
144+
mock = mocks[0];
145+
tweet = simplify(mock);
146+
});
147+
148+
it('should have 4 properties', function() {
149+
assert.equal( Object.keys(tweet).length, 4 );
150+
});
151+
152+
describe('format dates as `MMMM Do YYYY, h:mm:ss a`', function() {
153+
154+
describe('English format', function() {
155+
before(function() {
156+
revert = twitter.__set__('process.env.MomentLang', 'en');
157+
tweet = simplify(mock);
158+
});
159+
160+
it('should be `March 6th 2015, 2:29:13 am`', function() {
161+
assert.equal(tweet.date, 'March 6th 2015, 2:29:13 am');
162+
});
163+
164+
after(function(){
165+
revert();
166+
});
167+
168+
});
169+
170+
describe('Brazilian format', function() {
171+
before(function() {
172+
revert = twitter.__set__('process.env.MomentLang', 'pt-br');
173+
tweet = simplify(mock);
174+
});
175+
176+
it('should be `Março 6º 2015, 2:29:13 am`', function() {
177+
assert.equal(tweet.date, 'Março 6º 2015, 2:29:13 am');
178+
});
179+
180+
after(function(){
181+
revert();
182+
});
183+
184+
});
185+
186+
});
187+
188+
});
189+
190+
});
191+
192+
describe('retrieve timeline feed', function() {
193+
var revert;
194+
before(function() {
195+
revert = twitter.__set__("T.get", function( api, query, callback ) {
196+
callback( null, mocks);
197+
});
198+
});
199+
200+
describe('igorribeirolima timeline', function() {
201+
var tweets;
202+
before(function(done){
203+
twitter('igorribeirolima', function(err, data) {
204+
tweets = data;
205+
done();
206+
});
207+
});
208+
209+
it('should have 19 tweets', function() {
210+
assert.equal(tweets.length, 19);
211+
});
212+
213+
});
214+
215+
after(function() {
216+
revert();
217+
});
218+
219+
});
220+
});
221+
```
222+
223+
`__set__` returns a function which reverts the changes introduced by this particular `__set__` call.
224+
225+
226+
## Running unit tests
227+
228+
Before we get into the test and walk through it, let install mocha CLI by ``npm install -g mocha``. It will support us on running our tests just typing ``mocha twitter-spec.js``. The following is an image that illustrates the test result.
229+
230+
![image that ilustrates unit tests running](http://i1368.photobucket.com/albums/ag182/igorribeirolima/running%20unit%20tests_zpshqazy5po.png)
231+
232+
Take a look on this [video](http://showterm.io/3c970843502e140bcfabd#slow) and see step by step in detail everything discussed so far.
233+
234+
## Conclusion
235+
236+
As you can see it's not painful on overcoming hurdles like *(i)* injecting mocks for other modules, *(ii)* leaking private variables or *(iii)* overriding variables within the module. That's it folks. Hope you catch the idea on how simple and fun is to do dependency injection for unit testing. Thanks for reading.

‎package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "twitter-rest-api",
3+
"version": "0.0.0",
4+
"dependencies": {
5+
"connect": "2.9.0",
6+
"express": "~3.4.7",
7+
"twit": "~1.1.11",
8+
"async": "~0.2.9",
9+
"moment": "~2.5.1",
10+
"rewire": "2.3.1"
11+
},
12+
"engines": {
13+
"node": ">=0.10.25"
14+
}
15+
}

‎server.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
var express = require('express'),
2+
tweets = require('./twitter.js'),
3+
response = function(req, res, err, data) {
4+
if (req.query.callback) {
5+
res.jsonp(err || data);
6+
} else {
7+
res.json(err || data);
8+
}
9+
};
10+
11+
var app = express()
12+
.use(express.static(__dirname + './'))
13+
.get('/twitter/timeline/:user', function(req, res) {
14+
tweets(req.params.user, function(err, data) {
15+
response(req, res, err, data);
16+
});
17+
})
18+
.listen(process.env.PORT || 5000);

‎twitter-spec-mock-data.js

Lines changed: 3255 additions & 0 deletions
Large diffs are not rendered by default.

‎twitter-spec.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
var rewire = require('rewire'),
2+
assert = require('assert'),
3+
twitter = rewire('./twitter.js'),
4+
mocks = require('./twitter-spec-mock-data.js');
5+
6+
describe('twitter module', function(){
7+
describe('simplify function', function(){
8+
var simplify;
9+
10+
before(function() {
11+
simplify = twitter.__get__('simplify');
12+
});
13+
14+
it('should be defined', function(){
15+
assert.ok(simplify);
16+
});
17+
18+
describe('simplify a tweet', function(){
19+
var tweet, mock;
20+
21+
before(function() {
22+
mock = mocks[0];
23+
tweet = simplify(mock);
24+
});
25+
26+
it('should have 4 properties', function() {
27+
assert.equal( Object.keys(tweet).length, 4 );
28+
});
29+
30+
it('should have date property', function() {
31+
assert.ok(tweet.date);
32+
});
33+
34+
it('should have id property', function() {
35+
assert.ok(tweet.id);
36+
});
37+
38+
it('should have user property', function() {
39+
assert.ok(tweet.user);
40+
});
41+
42+
it('should have id property within user', function() {
43+
assert.ok(tweet.user.id);
44+
});
45+
46+
it('should have tweet property', function() {
47+
assert.ok(tweet.tweet);
48+
});
49+
50+
describe('format dates as `MMMM Do YYYY, h:mm:ss a`', function() {
51+
var revert;
52+
53+
describe('English format', function() {
54+
before(function() {
55+
revert = twitter.__set__('process.env.MomentLang', 'en');
56+
tweet = simplify(mock);
57+
});
58+
59+
it('should be `March 6th 2015, 2:29:13 am`', function() {
60+
assert.equal(tweet.date, 'March 6th 2015, 2:29:13 am');
61+
});
62+
63+
after(function(){
64+
revert();
65+
});
66+
});
67+
68+
describe('Brazilian format', function() {
69+
before(function() {
70+
revert = twitter.__set__('process.env.MomentLang', 'pt-br');
71+
tweet = simplify(mock);
72+
});
73+
74+
it('should be `Março 6º 2015, 2:29:13 am`', function() {
75+
assert.equal(tweet.date, 'Março 6º 2015, 2:29:13 am');
76+
});
77+
78+
after(function(){
79+
revert();
80+
});
81+
82+
});
83+
});
84+
85+
});
86+
87+
});
88+
89+
describe('retrieve timeline feed', function() {
90+
var revert;
91+
before(function() {
92+
revert = twitter.__set__("T.get", function( api, query, callback ) {
93+
callback( null, mocks);
94+
});
95+
});
96+
97+
describe('igorribeirolima timeline', function() {
98+
var tweets;
99+
before(function(done){
100+
twitter('igorribeirolima', function(err, data) {
101+
tweets = data;
102+
done();
103+
});
104+
});
105+
106+
it('should have 19 tweets', function() {
107+
assert.equal(tweets.length, 19);
108+
});
109+
110+
});
111+
112+
after(function() {
113+
revert();
114+
});
115+
116+
});
117+
});

‎twitter.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
var Twit = require('twit'),
2+
async = require('async'),
3+
moment = require('moment'),
4+
T = new Twit({
5+
consumer_key: process.env.TwitterConsumerKey || '...',
6+
consumer_secret: process.env.TwitterConsumerSecret || '...',
7+
access_token: process.env.TwitterAccessToken || '...',
8+
access_token_secret: process.env.TwitterAccessTokenSecret || '...'
9+
}),
10+
11+
mapReducingTweets = function(tweet, callback) {
12+
callback(null, simplify(tweet));
13+
},
14+
15+
simplify = function(tweet) {
16+
var date = moment(tweet.created_at, "ddd MMM DD HH:mm:ss zz YYYY");
17+
date.lang( process.env.MomentLang );
18+
return {
19+
date: date.format('MMMM Do YYYY, h:mm:ss a'),
20+
id: tweet.id,
21+
user: {
22+
id: tweet.user.id
23+
},
24+
tweet: tweet.text
25+
};
26+
};
27+
28+
module.exports = function(username, callback) {
29+
T.get("statuses/user_timeline", {
30+
screen_name: username,
31+
count: 25
32+
}, function(err, tweets) {
33+
if (err) callback(err);
34+
else async.map(tweets, mapReducingTweets, function(err, simplified_tweets) {
35+
callback(null, simplified_tweets);
36+
});
37+
})
38+
};

0 commit comments

Comments
 (0)
Please sign in to comment.