Gulp
What is Gulp?
"Gulp is a tool for task automation in Node."
Pedro is right! Gulp is a excellent tool to have in your projects! All of them! No exceptions! Use it! If you don't know, this is the time to learn it! It is very powerful. When I started studying Node I got confused! There is lot to learn! I was afraid to look into one more tool of this huge stack because it seemed hard and awkward. I couldn't see the benefits of it, I could do everything myself. But once I learned it I saw the light.
Why use it? So you can automate all the little boring tasks that you have to do on your code every day. Just to name a few:
- Compile a distribution version;
- Concatenate, uglify, minify CSS/JS files;
- Bounce server after changes;
- Run tests;
- Run coverage tool;
- Move files around;
- Install libraries;
- Make Coffee.
And those ones are just the ones that came up in my mind right now, we have a bunch of community plugins for us to use, and they keep growing. Non-stop. Seriously, it is non stop, it is kind of crazy.
Gulpfile
Now that I've built into your soul that Gulp is cool lets start understanding how it works: First, we need to install it:
$ npm install gulp --save-dev
We are already all set to go. We have Gulp installed. Now we have to tell Gulp what he can do. To do that we need to create a file named gulpfile.js in our root folder (This file must be named this way, Gulp will look for this file once executed).
└── nodeCode ├── src ├── package.json └── gulpfile.js
Ok, we have to know what to add to this new file of ours. First lets take a look into what this file should have inside.
'use strict'; //imports const gulp = require('gulp'); gulp.task('default', () => { //... }); gulp.task('task2', () => //... );
Here we can see how Gulp works. With this configuration I can execute Gulp using NPM, adding new commands under the "script" attribute in package.json. I have two tasks configured, default and task1. To execute a task with gulp you have to do this syntax:
$ gulp <task_name>
However, everything under the default task will be executed without the need of naming it, meaning that I can execute:
$ gulp
And the task default will be run. Neat, right?
As you can see Gulp uses a code syntax to configure its tasks. This way, if you want to use any other package or module, you just need to install it with NPM and import it using require. For instance, if I want to
Running a Task
Enough talk. First thing, lets get right of this cursed task that is to bounce our server every time we do some changes. Lets automate it. First, we will install nodemon. However, we need nodemon for Grunt which can be found at grunt-nodemon.
$ npm install grunt-nodemon --save-dev
Now that it is installed we will configure nodemon to watch and bounce our server once it is changed:
module.exports = function(grunt) { grunt.initConfig({ nodemon: { express: { script: 'index.js', options: { watch: ['./'], cwd: 'src' } } } }); grunt.loadNpmTasks('grunt-nodemon'); grunt.registerTask('default', ['nodemon']); };
Grunt is ready, although there is something we need to do if we want to execute Grunt tasks. We need to have a way to use it. Since we've installed grunt-cli and grunt locally we can use NPM to execute grunt.
{ "name": "lecture5", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "grunt" }, "author": "Andre Botelho", "license": "ISC", "dependencies": { "body-parser": "^1.15.1", "cookie-parser": "^1.4.3", "cors": "^2.7.1", "express": "^4.13.4", "jquery": "^3.1.0" }, "devDependencies": { "grunt": "^1.0.1", "grunt-cli": "^1.2.0", "grunt-nodemon": "^0.4.2" } }
And finally we call npm start and run grunt. Pay attention that Gruntfile.js is in the root of your project. The task will be watching all changes on file ./src/index.js.
$ npm start
Now we can change our server the way we like it! We have a bunch of cool packages to add and play with! Don't be shy and search for the really useful ones!
Unit Test
Concept
Unit testing. Yes, Unit testing. As usual lets take a look on what it means.
"In computer programming, unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use."
- Wikipedia
Or other words, it is a code that tests your whole code by snippets that were defined as a testable units. You can test method by method, class by class, flow by flow, integration, you name it. And Node has some neat tools for testing. We can even test client-side application with it.
Since we have to start from somewhere I will show you Mocha with Grunt. Yes, automated tests, baby!
Tech Stack
As we are building an API the stack that we will use is:
- Mocha for our test framework;
- Should as our BDD assertion library;
- Supertest as our library to make requests in the test environment;
Configuring Gulp
Everything should be installed with NPM, nice and easy. You can save those libraries with --save-dev. Installed everything? Go to go? NICE! Lets configure our gruntfile.
'use strict'; module.exports = function(grunt) { grunt.initConfig({ nodemon: { express: { script: 'server.js', options: { watch: ['./'] } } }, mochaTest: { test: { options: { reporter: 'spec' }, src: ['tests/*.js'] } }, concurrent: { test: { tasks: ['nodemon:express', 'mochaTest'], options: { logConcurrentOutput: true } } } }); grunt.loadNpmTasks('grunt-nodemon'); grunt.loadNpmTasks('grunt-mocha-test'); grunt.loadNpmTasks('grunt-concurrent'); grunt.registerTask('default', ['nodemon:express']); grunt.registerTask('testServer', ['mochaTest']); };
Lets begin to explain: We've included the mochaTest task with two sub-tasks: test and coverage. First in the test sub-task, we defined two options inside options: reporter as "spec" (this is the default value that grunt-mocha adds for us without any explanation) and require as "blanketWrapper". This last one is for Blanket do its job and see how much of the code we've covered by the tests. blanketWrapper is the name of a file that we need to create to wrap Blanket inside Mocha's Grunt task.
var path = require('path'); var srcDir = path.join(__dirname, 'src'); require('blanket')({ // Only files that match the pattern will be instrumented pattern: srcDir });
Last but not least, the src option. This one will define where Mocha can find the test code to be executed.
The coverage sub-task is defining how we will report the test status. Inside options we defined that we will report in a coverage html template by reporter: 'html-cov', quietly (meaning that Blanket should not print Mocha's output into this file and the name of this coverage file should be coverage.html. We told Blanket where to find the test files too.
Writing Tests
Now for the test file. We will use our Dog Service from Lecture 3: Middleware. We have three endpoints:
- http://localhost:8080/dog
- http://localhost:8080/goodDog
- http://localhost:8080/wiggle
'use strict'; const supertest = require('supertest'); const should = require('should'); let dogData = { name: 'testDog' }, updateDog = { index: 0, name: 'testDog2' }; describe('Dog Service', function() { let url = 'http://localhost:8080'; let request = supertest(url); describe('/dog', function () { it('should create a dog', function(done){ request.post('/dog') .send(dogData) .expect(201, done); }); it('should update the test dog', function(done){ request.put('/dog') .send(updateDog) .expect(200, done); }); }); describe('/dog/:doggyId', function () { it('should delete the first dog', function(done){ request.delete('/dog/0') .expect(200) .end(function(error, response){ if(error) return done(error); let dog = response.body; dog.should.not.be.empty; done(); }); }); }); describe('/wiggle/:doggyId', function () { it('should wiggle the first dog\'s tail', function(done){ request.get('/wiggle/0') .expect(200) .end(function(error, response){ if(error) return done(error); let dogWiggle = response.body; dogWiggle.should.not.be.empty; dogWiggle.msg.should.not.be.empty; dogWiggle.msg.should.be.an.instanceOf(String); done() }); }); }); describe('/goodDog', function () { it('should say that the first dog is a good dog', function(done){ request.get('/goodDog/0') .expect(200) .end(function(error, response){ if(error) return done(error); let goodDog = response.body; goodDog.should.not.be.empty; goodDog.msg.should.not.be.empty; goodDog.msg.should.be.an.instanceOf(String); done(); }); }); }); });
WOW! That is huge! (That's what she said). But, wait a minute... its is readable! Yes sir! Using the power of this test stack we can use BDD do write our tests and anyone can understand what is going on! That is awesome!
Ok, one step at a time. First lines are the imports that we need to run the tests, easy. Then we have some mock data to test everything. And next we have describe(). Ok, do not despair. We are only defining a scope. Inside this we can set up some configurations before and after our tests. For instance, we define the server URL that we are testing, bootstrap the Supertest library using request(url); and then we call before() and after methods.
Those methods are defining what will happen before and after all the actual tests (We can define actions to be done before and after EACH test using beforeEach() and afterEach() respectively). And finally we define the tests.
Each it() is a test defined. As we are testing APi calls we do not know when those will end. Therefore we tell mocha to give us a callback named done so we can call it when we finish testing the results that came with the request's response. And the rest is pretty readable! See for yourself, you literally read what the test is doing.
Everything ready to be executed! Go running to your terminal en execute and type:
$ grunt testServer
Blanket has created a file with its report, take a look and see what it created for us.
Please, there is no need to hold your tears of happiness, let it flow.
Exercises
Keep using your code which you developed on last lecture
Exercise 1:
Using the server which you were working until now, create a Gulp task to use nodemon to run it.
Exercise 2:
Using the same code from last exercise create an API Tests for your routes (Using the same stack as shwon here).
- supertest
- mocha
- should
Exercise 3:
Using the same code from last exercise create a task to run your test cases automatically.
Exercise 4:
Now, instead of running an API test (which is very expensive), create unit tests to test your controllers. This way you wont need to run your server to test your code. hint: maybe you will need to decouple your controllers from your route code.
Now that you have these new unit tests, create a task to run them (keep you API test task)
Exercise 8:
Challenge: add a coverege report for your tests. Hint: gulp coverage