Fun with Stamps. Episode 17. Easy 100% unit test coverage in JS
Hello. I’m developer Vasyl Boroviak and welcome to the seventeenth episode of Vasyl Boroviak presents Fun with Stamps.
TL;DR
Using this simple technique you can cover basically any JavaScript code with unit test. You would not need proxyquire
or any other module mocking. You would not need messing around with setTimeout
or any other builtin JavaScript API. The code and the tests will still be easy to read and maintain.
And, of course, we will utilise stamps to make it even easier. The main part — unit tests — are in the middle of this article.
All the source code can be found here: https://github.com/koresar/fws-ep17
Let’s write a node.js app
The node.js application code below is rather complicated to quality cover it with unit tests. It was chosen to show you how even the most complex code can be covered in JavaScript using stamps.
A production ready node.js service typically looks like this. I’m using the express-generator CLI to generate our app.js
.
As you can see there is already a lot of dependencies which might cause troubles while writing unit (not integration!) tests for that file:
- External modules
express
,debug
,jade
, etc. - Built in modules
path
andhttp
. - Built in global variables
require
,process
,__dirname
andsetImmediate
. - I/O, such as sockets and file system.
Your task
You should make sure:
- no one ever share static files other than those inside the
./public
directory; - no one ever removes that
extended: false
because it’s a security concern of this service; - we never return stack trace inside an error response payload. Again, because security matters to us;
- no one ever removes that
setImmediate
because it’s a data consistency concern.
Yes, we are going to have all that listed above. But additionally:
- we won’t depend on
sinon
orproxyquire
etc; - we will do only few small changes to the code
- thus, the code will still be readable
- and easy to change and maintain as it is now.
To have that all we will use the simple technique called early dependency injection utilising stamps.
The main and only refactoring
To create our app2.js
I’m going to add this.
to few variables. So, instead of keeping them in the function scope we will save them to an object. Wait saying “Booo!” You have to see how the unit tests look after the refactoring. It’s important!
Take a look at how we prepared our code for the dependency injection. As you can see we pushed all the problematic dependencies to the this.
object using stamp’s props
. And we moved the two route handlers to the stamp’s methods
down below.
Also, the module does not export a function any more, but a stamp. The stamp will start the service when an object instance is created from it.
So, now we can change dependencies just before starting the service. Like so:
let App = require('./app')
App = App.props({
http: myMockedHttpModule
})
App() // start the service
In the code above I replaced the actual http
module with something like a mock object:
let myMockedHttpModule = {
createServer() {
return {
on() {},
listen() {}
};
}
};
Now you can test the “Application” and the “Routing” parts of the app (see the comments separating the file onto three sections) without opening any sockets! Just use that mocked App
from the code above in all your tests.
Unit tests
“Application” part of the service
- Making sure we share nothing but the
./public
folder. - Making sure the
extended
is alwaysfalse
inbodyParser.urlencoded()
.
At the top of this file we are creating an http
dependency which does not open any ports. Instead of the actual .listen()
method we put an empty function.
As you can see we are setting the http
dependency only once (that’s called the early dependency injection) and then transparently reusing it in the all the tests of this file. So that the it()
tests look clean.
Look closer at the tests. We mock only the only relevant dependency — express
or bodyParser
. This hugely improves readability!
The first test makes sure that we do actually share the ./public
directory.
The second test makes sure that any time we share a directory we actually share the /public
folder(s). No matter how many times the .static()
function is called.
The third test makes sure that urlencoded({extended})
option is false
.
It is hard to over stress the point that the tests are not bloated, that they are simple, and anyone with the basic JavaScript knowledge can read and amend them as needed.
“Routing” part of the service
- Making sure the error stack trace is not exposed when running in “production” environment.
If you read this 5 minute article — Episode 1. Stamp basics — you will find that every stamp has the .compose
property. It’s a function which also serves as the metadata holder (aka “stamp descriptor”).
Philosophy of stamps: you are allowed and encouraged to extract and manipulate a stamp’s metadata.
At the top we retrieve the two route handling methods from the metadata — handleNotFound
and handleGenericError
. And then we simply unit tests them as regular functions.
That’s how you can extract each individual method (or property, or initializer, or static property, etc) and test it without creating an instance of that stamp.
“Server” part of the service
- Making sure we exit the node.js process only after the
setImmediate
settles the I/O of the process.
This is the most complicated test. We mock three things: the http
module, the setImmediate
JavaScript global function, and the process
global node.js variable.
The done()
function is executed only after we made sure that the setImmediate
and process.exit
were actually called.
This test would have been simpler if we would move few other variables to the props
of the stamp. However, my point here is to show you that a code of any complexity can be covered with simple unit tests; and that you don’t need any sophisticated NPM modules for mocking or dependency injection.
All you need is to try stamps.
More complex testing scenarios
Any future complications of the app2.js
will still be easy to unit test.
For example, what if you would need to open a Postgres database connection before opening the HTTP port?
Easy! Just push it to the stamp’s props
like we did with the http
module dependency. (Try it by forking this repository.)
But, if you would attempt writing this unit test using the original app.js
approach you’d need to put more effort mocking the database dependency.
Conclusion
It’s difficult to unlearn the typical way we do software development. Classes or pure functions are both great ways to code in JavaScript. Although, I do believe that by using stamps we can all be more productive as developers.
When I code, my typical .js
files export a single thing — a stamp. There is a number of positives in this approach. I’ll list them in the order of importance.
- Most important. Your unit tests are simple, readable, and maintainable. It’s hard to underestimate the importance of this in the long run.
- Second. You don’t need
proxyquire
,nock
,sinon
, or similar mocking libraries. And the unit testing code is KISS (Keep It Stupid Simple) and DRY (Don’t Repeat Yourself) enough. - Third. After our small refactoring the original application code does not change much. The code readability stays the same.
- And last. The tests above will run under any compatible testing framework (mocha, jasmine, jest). Try it! Also, if you replace
describe
andit
with tape or ava syntax — the tests will still run.
Have fun with stamps!
- Episode 1. Stamp basics
- Episode 2. Dependency injection in FP
- Episode 3. Comparing with the ES2015 classes
- Episode 4. Implementing stamps yourself in 30 LOC
- Episode 5. Composition design pattern
- Episode 6. Statics — properties on stamps
- Episode 7. Early and late dependency injection
- Episode 8. Tracking and overriding composition
- Episode 9. Detaching compose()
- Episode 10. My stamp mental model
- Episode 11. Interfering composition
- Episode 12. New @stamp home
- Episode 13. Method collision control
- Episode 14. New @stamp/it as a replacement of Stampit
- Episode 15. The @stamp/ modules ecosystem
- Episode 16. TypeScript mix-in classes vs Stamps
- Episode 17. Easy 100% unit test coverage in JS (this article)
- Episode 18. Dependency injection paradise
- Episode 19. Java/C# abstract methods in JavaScript
- Episode 20. Stampit v4
- Episode 21. Private data in JavaScript. 4 ways using stamps
- Episode 22. JavaScript instanceof as composable stamp
- Episode 23. New stampit.js.org with all the docs
- Episode 24. New “name” feature