Introduction

With the many tools available to aid in developing AngularJS applications, many people have the impression that it’s an extremely complicated framework, which is not at all the case. That’s one of the main reasons I started this tutorial series.

In part one we covered the basics of the AngularJS framework and started out by writing our first application. This post is designed for beginners. If you’re a more experienced AngularJS developer, you might be more interested in demystifying directives or a story of AngularJS in use at a growing startup.

In this tutorial, we’re going to set aside the application logic layer and learn how to conduct proper AngularJS project setup, including scaffolding, dependency management, and preparing it for testing (both unit and end-to-end). We’ll do this using these AngularJS tools: Yeoman, Grunt, and Bower. Then, we’ll review the process of writing and running Jasmine tests using Karma.

If you work with JavaScript, it’s highly probable that you already know of at least some of these tools, even if you’re new to Angular. But to help ensure a common baseline, I’ll avoid making any assumptions. Let’s briefly review each of these technologies and what it’s useful for:

Karma (previously known as Testacular) is Google’s JavaScript test runner and the natural choice for testing AngularJS. In addition to allowing you to run your tests on real browsers (including phone/tablet browsers), it is also test framework agnostic; which means that you can use it in conjunction with any test framework of your choice (such as Jasmine, Mocha, or QUnit, among others).

Jasmine will be our test framework of choice, at least for this post. Its syntax is quite similar to that of RSpec, if you’ve ever worked with that. (If you haven’t, don’t worry; we’ll check it out in greater detail later in this tutorial.)

Grunt is a task runner that helps automate several repetitive tasks, such as minification, compilation (or build), testing, and setting up a preview of your AngularJS application.

Bower is a package manager that helps you find and install all your application dependencies, such as CSS frameworks, JavaScript libraries, and so on. It runs over git, much like Rails bundler, and avoids the need to manually download and update dependencies.

Yeoman is a toolset containing 3 core components: Grunt, Bower, and the scaffolding tool Yo. Yo generates boilerplate code with the help of generators (which are just scaffolding templates) and automatically configures Grunt and Bower for your project. You can find generators for almost any JavaScript framework (Angular, Backbone, Ember, etc.), but since we’re focusing here on Angular, we’re going to use the generator-angular project.

So, where do we start?

Well, the first thing we’ll need to do is install the tools we’re going to need.

If you don’t have git, node.js, and npm installed already, go ahead and install them.

Then we’ll go to the command line and run the following command to install the tools of Yeoman:

npm install -g yo grunt-cli bower

Oh, and don’t forget, we’re going to use the AngularJS generator so you’ll need to install it as well:

npm install -g generator-angular

OK, now we’re ready to…

Scaffold/generate our AngularJS application

Last time, we manually borrowed our boilerplate code from the angular-seed project. This time, we’ll let yo (in conjunction with generator-angular) do that for us.

All we need to do is create our new project folder, navigate to it and run:

yo angular

We’ll be presented with some options, such as whether or not to include Bootstrap and Compass. For now, let’s say no to Compass and yes to Bootstrap. Then, when prompted about which modules to include (resource, cookies, sanitize and route), we’ll select only angular-route.js .

Our project scaffold should now be created (it may take a minute), integrated with Karma and all pre-configured.

Note: Bear in mind that we’re restricting the modules here to the ones we used in the application we built in part one of this tutorial. When you’re doing this for your own project, it will be up to you to determine which modules you’ll need to include.

Now, since we’re going to be using Jasmine, let’s add the karma-jasmine adapter to our project:

npm install karma-jasmine --save-dev

In case we want our tests to be executed on a Chrome instance, let’s also add the karma-chrome-launcher :

npm install karma-chrome-launcher --save-dev

OK, if we did everything right, our project file tree now should now look like this:

Our static application code goes into the app/ directory and the test/ directory will contain (yup, you guessed it!) our tests. The files we see on the root are our project configuration files. There’s a lot to be learned about each one of them, but for now we’ll just stick with the default configuration. So let’s run our app for the first time, which we can do simply with the following command:

grunt serve

And voila! Our app should now pop up in front of us!

A little about Bower for AngularJS

Before getting into the really important part (i.e., the testing), let’s take a minute to learn a bit more about Bower. As mentioned earlier, Bower is our package manager. Adding a lib or plugin to our project can simply be done using the bower install command. For example, to include modernizr , all we need to do is the following (within our project directory, of course):

bower install modernizr

Note, though, that while this does make modernizr part of our project (it will be located in the app/bower_components directory), we are still responsible for including it in our application (or managing when it should be included) as we would need to do with any manually added lib. One way to do this would be to simply add the following <script> tag to our index.html :

<script src="bower_components/modernizr/modernizr.js"></script>

Alternatively, we can use the bower.json file to manage our dependencies. After carefully following every step until now, the bower.json file should look like this:

{ "name": "F1FeederApp", "version": "0.0.0", "dependencies": { "angular": "1.2.15", "json3": "~3.2.6", "es5-shim": "~2.1.0", "jquery": "~1.11.0", "bootstrap": "~3.0.3", "angular-route": "1.2.15" }, "devDependencies": { "angular-mocks": "1.2.15", "angular-scenario": "1.2.15" } }

The syntax is fairly self-explanatory, but more information is available here.

We can then add any additional new dependencies that we want, and then all we need is the following command to install them:

bower install

Now let’s write some tests!

OK, now it’s time to actually pick up from where we left off in part one and write some tests for our AngularJS app.

But first, there’s a little problem we need to address: Although the developers of generator-angular based their project template on the angular-seed project (which is the official Angular boilerplate), for some reason I don’t really understand, they decided to change the app folder naming conventions (changing css to styles , js to scripts , and so on).

As a result, the app we originally wrote now has paths that are inconsistent with the scaffold we just generated. To get around this, let’s download the app code from here and work with that version from this point on (it’s mostly the exact same app we originally wrote, but with the paths updated to match the generator-angular naming).

After downloading the app, navigate to the tests/spec/controllers folder and create a file named drivers.js containing the following:

describe('Controller: driversController', function () { // First, we load the app's module beforeEach(module('F1FeederApp')); // Then we create some variables we're going to use var driversController, scope; beforeEach(inject(function ($controller, $rootScope, $httpBackend) { // Here, we create a mock scope variable, to replace the actual $scope variable // the controller would take as parameter scope = $rootScope.$new(); // Then we create an $httpBackend instance. I'll talk about it below. httpMock = $httpBackend; // Here, we set the httpBackend standard reponse to the URL the controller is // supposed to retrieve from the API httpMock.expectJSONP( "http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK").respond( {"MRData": {"StandingsTable": {"StandingsLists" : [{"DriverStandings":[ { "Driver": { "givenName": 'Sebastian', "familyName": 'Vettel' }, "points": "397", "nationality": "German", "Constructors": [ {"name": "Red Bull"} ] }, { "Driver": { "givenName": 'Fernando', "familyName": 'Alonso' }, "points": "242", "nationality": "Spanish", "Constructors": [ {"name": "Ferrari"} ] }, { "Driver": { "givenName": 'Mark', "familyName": 'Webber' }, "points": "199", "nationality": "Australian", "Constructors": [ {"name": "Red Bull"} ] } ]}]}}} ); // Here, we actually initialize our controller, passing our new mock scope as parameter driversController = $controller('driversController', { $scope: scope }); // Then we flush the httpBackend to resolve the fake http call httpMock.flush(); })); // Now, for the actual test, let's check if the driversList is actually retrieving // the mock driver array it('should return a list with three drivers', function () { expect(scope.driversList.length).toBe(3); }); // Let's also make a second test checking if the drivers attributes match against // the expected values it('should retrieve the family names of the drivers', function () { expect(scope.driversList[0].Driver.familyName).toBe("Vettel"); expect(scope.driversList[1].Driver.familyName).toBe("Alonso"); expect(scope.driversList[2].Driver.familyName).toBe("Webber"); }); });

This is the test suite for our driverscontroller . It may look like a lot of code, but most of it is actually just mock data declaration. Let’s take a quick look at the really important elements:

The describe() method defines our test suite.

method defines our test suite. Each it() is a proper test spec.

is a proper test spec. Every beforeEach() function is executed right before each of the tests.

The most important (and potentially confusing) element here is the $httpBackend service that we instantiated on the httpMock variable. This service acts as a fake back-end and responds to our API calls on the test runs, just like our actual server would do in production. In this case, using the expectJSONP() function, we set it to intercept any JSONP requests to the given URL (the same one we use to get the info from the server) and instead, return a static list with three drivers, mimicking the real server response. This enables us to know for sure what is supposed to come back from the controller. We can therefore compare the results with those expected, using the expect() function. If they match, the test will pass.

Running the tests is simply done with the command:

grunt test

The test suite for the driver details controller ( drivercontroller ) should be fairly similar to the one we just saw. I recommend you try to figure it out for yourself as an exercise (or you can just take a look here, if you’re not up to it).

What about end-to-end AngularJS tests?

The Angular team recently introduced a new runner for end-to-end tests called Protractor. It uses webdriver to interact with the application running in the browser and it also uses the Jasmine testing framework by default, so the syntax will be highly consistent with that of our unit tests.

Since Protractor is a fairly new tool, though, it’s integration with the Yeoman stack and generator-angular still requires a fair amount of configuration work. With that in mind, and my intention to keep this tutorial as simple as possible, my plan is to dedicate a future post exclusively to covering end-to-end testing in AngularJS in-depth.

Conclusion

At this point in the tutorial series, we’ve learned how to scaffold our Angular app with yo , manage it’s dependencies with bower , and write/run some tests using karma and protractor . Bear in mind, though, that this tutorial is meant only as an introduction to these AngularJS tools and practices; we didn’t analyze any of them here in great depth.

Our goal has simply been to help get you started down this path. From here, it’s up to you to go on and learn all you can about this amazing framework and suite of tools.

Addendum: Some (important) notes from the author

After reading this tutorial, some people may ask, “Wait. Aren’t you supposed to do all this stuff before you actually start coding your app? Shouldn’t this have been part one of this tutorial?”

My short answer to that is no. As we saw in part one, you don’t actually need to know all this stuff to code your first Angular app. Rather, most of the tools we’ve discussed in this post are designed to help you optimize your development workflow and practice Test-Driven Development (TDD).

And speaking of TDD, the most basic concept of TDD is certainly a sound one; namely, write your tests before you write your code. Some people, though, take that concept way too far. TDD is a development practice, not a learning method. Accordingly, writing your tests before writing your code does make a lot of sense, whereas learning how to write your tests before learning how to code does not.

I personally think this is the main reason why the official Angular tutorials may feel so convoluted and can be nearly impossible to follow for people with no previous front-end MVC/TDD experience. That is one of the main reasons why I started this tutorial series.

My personal advice for those learning to navigate the AngularJS world is: Don’t be too hard on yourself. You don’t need to learn everything all at once (despite people telling you otherwise!). Depending on your prior experience with other front-end/testing frameworks, AngularJS can be pretty hard to understand initially. So learn all you need to learn until you are able to write your own simple apps and then, once you’re comfortable with the basics of the framework, you can concern yourself with selecting and applying the long-term development practices that work best for you.

Of course, that is my humble opinion and not everyone will agree with that approach (and the Angular dev team may send a hired killer after me once I publish this), but that’s my vision and I’m pretty sure that are lots of people out there who will agree with me.