A challenge testers sometimes have, particularly those working to build up their technical skill set, is how to get involved in various programming language ecosystems. Often people start by trying to learn the language and I’ve found that’s not the most helpful approach for some. Sometimes you are better off starting with supporting tools, which forces you to use other supporting tools. Along the way you learn bits of the language in context. Then you can go back and learn the language in a more reference style. I’ll show what I mean here with using Grunt as a springboard to getting into the JavaScript ecosystem.
Just to reinforce my main point, consider the idea of “getting into Java.” You have to learn the Java language itself, of course, but you also have to learn the JVM on which Java operates, particularly because the JVM mandates aspects like the classpath and the package system. Then, since Java dependencies are such a pain to work with, this means it helps to understand the build tools like Ant or Maven or, more likely, both. Then you might hear about Scala or Clojure or Groovy and, once you learn those are JVM-hosted languages, you wonder if those are what you should focus on. Well, a key point is that all of those languages require running on the JVM and will likely use tools like Ant and Maven. So starting with the build tools can sometimes be a viable path to getting into the ecosystem.
So now let’s talk about using the Grunt build tool to get into JavaScript. Using Grunt requires knowing a few things. You ultimately need some knowledge of JavaScript, Node.js, and NPM (Node.js Package Manager). This means that in order to run Grunt, you need an operating system capable of running Node.js, which is pretty much any variant of Windows, Mac OS X, and Linux. You also need a command-line interface, which all operating systems provide.
I’ll cover all of this as we go, but only to the depth necessary to show you how easy it is to get involved in the JavaScript ecosystem. Yet notice what you’ll get: knowledge of a build tool (Grunt), a server-side tool that easily lets you execute JavaScript (Node.js) and a packaging system that resolves dependencies (NPM). Once you have that foundation, you can then concentrate on various aspects of JavaScript, including the many, many frameworks that exist for it.
Get the Supporting Tools
Make sure you install Node.js. Doing so will generally get you the Node.js Package Manager (NPM) as well. You can use various types of package manager to get both of these. Do note that while NPM is an acronym, it’s common for it to be referenced in all lowercase, as npm. I’ll follow that tradition here.
Check that both tools are installed and work:
$ node --version $ npm --version
Grunt and Node
Grunt is a build tool and/or task runner for JavaScript. Think of it like Make (for C and C++), Rake (for Ruby), or Maven and Ant (both of which work for Java). The reason you need Node.js is that Grunt is written as a Node.js module. Grunt tasks, which do all the work, are also Node.js modules.
Let’s briefly talk about the module system. The Node.js module system is an implementation of a specification referred to as CommonJS. Without getting into lots of detail, CommonJS describes a syntax for JavaScript programs to require, or import, other JavaScript programs. “Other JavaScript programs” here refers to JavaScript files and each of those files is called a module. Essentially all you have to understand is this: Node.js implements CommonJS, which means it provides a simple way for developers to write modular programs in JavaScript.
When using Node.js, what you’ll usually do is download modules from an npm repository. When installing a module, you can either install it locally or globally. Locally means the module is available only to a specific project you are working on whereas globally means it is available to any project you may be working on.
Grunt is broken into separate packages, each serving a specific purpose. Packages? Wait a minute! What happened to modules? I’ll talk about the package/module distinction in a bit. For now, just know that there is a grunt-cli package that gives you a command-line interface for using Grunt. Here’s a key thing to understand: you will install grunt-cli globally but you’ll install grunt locally. This will all be done via npm.
Get Grunt
First install grunt-cli so it is available to you for any of your projects:
$ npm install -g grunt-cli
It’s the “-g” that tells npm to install grunt-cli as a global module. Installing grunt-cli does not install grunt for you. To use Grunt fully, you have to install the grunt package as well. But you won’t do that globally. Instead, you install Grunt into your project as a dependency. The grunt-cli tool you installed globally on your system will then work with the version of grunt that is installed locally your project.
So now let’s create a project directory:
$ mkdir test_project $ cd test_project
From here, you could install Grunt with npm install grunt
. That would install Grunt locally. Installing locally, in a Node.js context, means that a directory called node_modules will be created inside of your project directory. That’s how Node.js stores modules that are local to your project. However, let’s do something just slightly different by using a package descriptor file.
Create Package File
Node.js applications use a file called package.json
to store the metadata about a project as well as provide information about a project’s dependencies. If you are not familiar with the name, JSON refers to JavaScript Object Notation and it’s essentially a standard data format. (Think of it as a variation of XML or YAML, if you are familiar with those.) There are many benefits this file provides to a project but a core one is the ability to list dependencies. Relevant to what we’re doing here, if you create this file, you can add Grunt as a dependency to your project.
So let’s create a package.json file:
$ npm init
This will start a series of prompts regarding what should go in the file. You can just accept all of the defaults or fill in specific values. If you accept the defaults you get a file like this:
1 2 3 4 5 6 7 8 9 10 11 |
{ "name": "test_project", "version": "0.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } |
Packages and Modules
I said I would talk more about packages and how that compares with modules. Now is as good a time as any.
A package is any directory containing a valid package.json file. This is, in fact, the only requirement for a directory to pass as an npm package. This also makes that directory a module. This is where the terminology conflates a little.
A module can be any directory with an index.js file in it. A module can also be any folder with a package.json file, where that package.json file contains a main field. The main field — see the example above — says what the main file for the module is. The default for the main field is index.js. Again, see the above example.
So what does all this tell you? It tells you that a package is a directory that has a package.json file. It also tells you that a module can be a directory with either a package.json file or an index.js file or both. So, in order for someone to use your package in their program, it must be capable of being loaded with the require function, which by definition, means your package must also be a module.
Dependencies
At this point, run this command:
$ npm install
The call to npm will look for the package.json file, parse it, and then install each module listed in the dependencies property. Right now you have no dependencies property in this file so nothing much will happen.
Now that you have this file in place, you can add Grunt as a development dependency like this:
$ npm install grunt --save-dev
You should see some output line that looks someting like this:
grunt@0.4.5 node_modules\grunt
Your version of Grunt may differ but what you’re seeing there is that Grunt is installed into the node_modules directory within the current directory. Grunt was also added to the package.json file as a development dependency. You should see something like this in the file:
1 2 3 |
"devDependencies": { "grunt": "^0.4.5" } |
The devDependencies section lists dependencies that are used only to build an application, as opposed to a dependency required to run the application. Grunt isn’t something an application needs to run. You use Grunt only as a tool for developing an application, hence it being a development dependency.
The caret (^) is based on the idea of Semantic Versioning. What it says is that the version of Grunt in your project will be updated to the most recent major version, which is the first number in the version string. So “^0.4.5” means that any version that starts with “0” will be used but if the major version changes, that will not be used. So if Grunt “1.0.0” comes out, that will not automatically be used in your project.
While the caret keeps you locked to the major version, you will also see the tilde (~) used, as in “~0.4.5”. The tilde is used to match the most recent minor version. That would be the second number in the version string. So, with this example, “~0.4.5” would mean that that any version of Grunt that has “0.4” as its first two values will be used. But a version like “0.5.0” would not.
Try to Grunt
With Grunt installed, you can test things out by running it from the command line:
$ grunt
This fires off the grunt-cli library you installed globally. That then uses the grunt library you installed in your project’s node_modules folder. What you might gather from this is that you could easily use different versions of Grunt on different projects.
As far as the output from the above command, Grunt is telling you that you need something called a Gruntfile in your project. A Gruntfile is a JavaScript file that specifies and configures the tasks you want to be able to run for your project. It’s equivalent in concept to files like Makefile, Rakefile, build.xml, or pom.xml. Grunt is specifically looking for a file called Gruntfile.js in the current working directory but can’t find one.
Create the Gruntfile
The Gruntfile is needed because it defines the configuration and is used to contain and set up the tasks that will be used as part of your build process.
So create the Gruntfile.js and put the following in it:
1 2 3 |
module.exports = function(grunt) { // Do grunt-related things in here }; |
If you’re not familiar with JavaScript, you might have no clue what you are doing here. The structure of the above goes back to the CommonJS specification I talked about earlier. Essentially the module
is an object that represents the module itself. The module object contains the exports object. The exports
object is a plain JavaScript object, which may be augmented to expose functionality to other modules. What’s not easy to see from this is that the exports object is returned as the result of a call to require. In JavaScript require
is a function that’s used to import modules, returning the corresponding exports object.
A good example of how this works is given in the CommonJS examples:
1 2 3 4 |
// example.js var inc = require('./increment').increment; var a = 1; inc(a); |
1 2 3 4 5 |
// increment.js var add = require('./math').add; exports.increment = function(val) { return add(val, 1); }; |
1 2 3 4 5 6 7 8 |
// math.js exports.add = function() { var sum = 0, i = 0, args = arguments, 1 = args.length; while (i < 1) { sum += args[i++]; } return sum; }; |
Here, example.js is the entry point or “main” file. Understanding that require
will return the exports object of the desired file, you can figure out what the above logic does. Starting at example.js, it calls require('./increment')
. When this require function is called, it synchronously executes the increment.js file. The increment.js module in turn, calls require('./math')
. The math.js file augments its exports object with an add function. Once the math.js file completes execution, require returns the math.js module’s exports object, thereby allowing increment.js to use the add function. Subsequently, increment.js will complete its execution and return its exports object to example.js. Finally, example.js uses its new inc function to increment the variable a from 1 to 2. If you were to run example.js with Node.js, you should get an output of 2.
While this example is admittedly simple, the idea of many small programs working together is one of the core guiding principles of Node.js. This helps developers avoid the construction of large monolithic libraries that often do not adequately separate concerns.
Going back to our Gruntfile.js, with the initial code you are simply providing a function with a single parameter. Grunt will then call the function with the grunt
object as the single argument. The grunt object is what you use to interact with Grunt. Think of it as Grunt’s API. This object will allow you to reference the methods that have been exposed — or exported, in JavaScript-speak — for developer use.
Grunt Tasks
A Grunt task is essentially just a JavaScript function, and that’s it. The root of all tasks is the idea of one task being one function. Let’s consider a very simple task. Add the following to your Gruntfile:
1 2 3 4 5 |
module.exports = function(grunt) { grunt.registerTask('default', function() { grunt.log.writeln('Grunt is working.'); }); }; |
What you see there is the simplest way to create a task: you just provide a name and a function to grunt.registerTask and you’ll be able to to execute that task. Since the task I provided here is called ‘default’ that means Grunt will run that task if you don’t specify a task at the command line. Execute this command:
$ grunt
The output you’ll see is:
Running "default" task Grunt is working. Done, without errors.
Let’s add another task. This one will take a parameter. Add the following to your Gruntfile:
1 2 3 4 5 6 7 8 9 |
module.exports = function(grunt) { grunt.registerTask('default', function() { grunt.log.writeln('Grunt is working.'); }); grunt.registerTask('say', function(name) { grunt.log.writeln('Say: ' + name); }); }; |
You can run this with the following:
$ grunt say:Hello
Notice how the parameter you specify to the task is separated from the task by a colon. The output will be:
Running "say:Hello" (say) task Say: Hello Done, without errors.
You can also take in more parameters for tasks that might require such. Add the following to Gruntfile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
module.exports = function(grunt) { grunt.registerTask('default', function() { grunt.log.writeln('Grunt is working.'); }); grunt.registerTask('say', function(name) { grunt.log.writeln('Say: ' + name); }); grunt.registerTask('add', function(first, second) { var answer = Number(first) + Number(second); grunt.log.writeln(first + ' + ' + second + ' is ' + answer); }); }; |
You can run this with:
$ grunt add:1:2
Here notice how multiple parameters are each separated by a colon. The output will be:
Running "add:1:2" (add) task 1 + 2 is 3 Done, without errors.
What I hope you see here is that you can use any valid JavaScript that you would like. For example, you can augment the last task, to make certain that the numbers entered are actually numbers. Change your add task to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
grunt.registerTask('add', function(first, second) { if (isNaN(Number(first))) { grunt.warn('The first argument must be a number.'); } if (isNaN(Number(second))) { grunt.warn('The second argument must be a number.'); } var answer = Number(first) + Number(second); grunt.log.writeln(first + ' + ' + second + ' is ' + answer); }); |
You can also have a task that calls other tasks, thus introducing a chain of dependencies into your build process. A likely and common scenario is a ‘test’ task that calls a ‘compile’ task that may call a task that sets up the appropriate director structure. In this case, let’s just keep it simple and have a task called ‘all’ that calls all of the tasks we have set up already:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
module.exports = function(grunt) { grunt.registerTask('default', function() { grunt.log.writeln('Grunt is working.'); }); grunt.registerTask('say', function(name) { grunt.log.writeln('Say: ' + name); }); grunt.registerTask('add', function(first, second) { if (isNaN(Number(first))) { grunt.warn('The first argument must be a number.'); } if (isNaN(Number(second))) { grunt.warn('The second argument must be a number.'); } var answer = Number(first) + Number(second); grunt.log.writeln(first + ' + ' + second + ' is ' + answer); }); grunt.registerTask('all', ['default', 'say:Hello', 'add:1:2']); }; |
Configuration
Now let’s try something a little different. Specifically we’re going to set up a configuration. To keep this example from becoming too conflated with detail, clear out all the contents of your Gruntfile.js and replace it with this:
1 2 3 4 5 6 7 |
module.exports = function(grunt) { grunt.initConfig({ meaning: { ofLife: 42 } }); }; |
Here I’m initializing the configuration with an object. Now add the following task to your Gruntfile:
1 2 3 4 5 6 7 8 9 10 11 12 |
module.exports = function(grunt) { grunt.initConfig({ meaning: { ofLife: 42 } }); grunt.registerTask('silly', function() { var meaning = grunt.config.get('meaning'); grunt.log.writeln("Meaning of life is " + meaning.ofLife); }); }; |
Here I’m registering a simple task, which uses the configuration I set up. I use grunt.config.get() to get a reference to ‘meaning’. Once I have that reference, I can use it to call a property (‘ofLife’) and thus get the value of that property. Run the task like this:
$ grunt silly
Incidentally, while you’ll normally see the above initConfig approach, you could replace the entire grunt.initConfig block with this:
1 |
grunt.config.set('meaning', { ofLife: 42 }); |
That would get you the same result.
And There You Are…
By learning the build tool Grunt, which admittedly I’ve only scratched the surface of, you’ve exposed yourself to some JavaScript as well as how to set up and use Node.js and npm. Along the way, you had to understand a bit about how and how why these tools work the way they do.
A nice thing about having a working Node.js implementation is that you can use it to run JavaScript files that you write. This is often a challenge for people who think they have to run their JavaScript via browser developer tools in order to learn. Not true. You can create a basic JavaScript file (with .js extension) and run it via Node.js simply by doing this:
$ node my_file.js
This gives you freedom to now explore blogs, books, articles, whatever and put the JavaScript you read about there to the test. Further, once you start to get into more advanced JavaScript, you’ll have a server you can use (Node.js) as well as a build tool (Grunt) to help you put it all together.
Welcome to the JavaScript ecosystem!