.tdd with angular
28 Jun 2014Introduction
I find it difficult to use TDD methodology while creating browser based applications. In Rails there are many options but the basis of a Rails application is different than an application where the majority of the logic is in the browser. With Rails I have a single request and response for the majority of use cases whereas using a client side Javascript framework gives me the freedom to have pages updated without refreshing the browser.
Client side application testing raises an issue with how to fake out interactions of someone using the application. Many times I would feel satisfied with Unit tests and clicking over the site 17 times for every change I made. Without the full suite of tests I found myself falling prey to the same bug on multiple occasions. Using client side javascript frameworks was fun but in the end I created software which was more prone to bugs until now.
Recently I was introduced to Angular.js while working on a small startup project. I chose Angular primarily because it has many pieces which are designed around the idea of being testable. The first I came across was
which is similar to the project WebMock for Ruby based projects. The fact that they made a HTTP mock framework core in their design perked my interest.1
$httpbackend
Building a project using TDD and Angular turned out to be an absolute joy and I want to share how I approached it.
An example project I setup while writing this can be found on My GitHub.
Initial Setup
I began my project by looking at some common modules found in a few popular Angular projects.
- coffee-script
- Of course
- karma
- Runs the tests with a chrome browser in the background
- karma-coffee-preprocessor
- Turn coffee into JS tests
- karma-chrome-launcher
- “with a chrome browser”
- karma-jasmine
- This was an accidental choice, usually I use Mocha but after experiencing Jasmine I prefer it due to how easy it was to setup and run. Mocha has more choices but keeping it simple was helpful later.
- protractor
- For running end-to-end tests, these are larger tests than the Karma tests and take longer to execute.
- http-server
- Seriously easy to have a directory hosted in a minimal web server.
- bower
- It is nice to have a list of random third-party JS libraries in one spot. Otherwise a jQuery update turns into two days of frustration.
With those dependencies installed I began by setting up my directory structure to be used with the tests. Each library will do this setup different so I tried to follow a layout I have seen in a number of Angular projects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/project-root/web
- .gitignore
- bower.json
- package.json
- app
- index.html
- js
- app.coffee
- controllers.coffee
- models.coffee
- partials
- github.html
- bower_components
- test
- karma.conf.js
- protractor-conf.js
- unit
- github.coffee
- e2e
- scenarios.coffee
- bower_components
- node_modules
NOTE I don’t fully support the idea of all controllers, models or services located in a single file. I prefer them separated out but to start with this layout worked well. Using Grunt can come in handy putting together all the individual controllers, models and services.
Tests
The testing in Angular applications is broken down to a few different frameworks working together. Each framework can be used independently but for this example I am using them together. For most projects I find that I need each piece eventually.
- Test Runner
- A test runner will bootstrap a test environment and run the actual tests with the program code in scope. For this project I am using Karma but there are many more runners available.
- Test Framework
- A test framework gives methods to evaluate if a given assertion is true or not. Many test frameworks attempt to be as human readable as possible. For this project I used Jasmine but there are many to choose from.
- End to End (e2e)
- The e2e tests build on top of the unit tests by testing features as they are connected together. Commonly these tests may be considered integration tests or behaviors. For this project I found Protractor to be an easy solution since it is setup to work with Angular. There are other choices but for someone new to Angular I feel this is the best option.
Unit Tests
The unit tests are ran via Karma and are written in Jasmine. Each test is supposed to be as simple and isolated as possible. I believe that unit tests should never rely on external services or share state between calls unless absolutely necessary.
Since I rely heavily on unit tests I like to make sure they are easy to run and fast.
Configure NPM Commands for Unit Testing
Once I had a basic layout I went about adding commands to be used with NPM in order to keep myself from having to recall numerous command line options. To start with I setup the testing related commands in the
file so that I can run 1
package.json
and not worry about running installation steps before.1
npm test
- pretest
- This script is executed before running the test script. It is usually a good call to make sure a fresh
is ran before tasks.1
npm install
- test
- This task runs the unit tests or specs. I use the binary under
to avoid having to install Karma globally.1
node_modules
- test-single-run
- Karma by default starts and watches code for changes; any changes start the tests again. This will run Karma once against the current tests then exit. I find this task useful when checking tests after a merge.
Integration or End-to-End (e2e) Tests
The integration tests take a little longer to run which shouldn’t matter. My “tdd-loop” follows more of a “bdd-tdd-loop” where I start at a vague description of the feature I want to see and then drill down to the individual components which make up that feature. This means that I can have an integration test which fails for an hour while I am writing the components which make up that feature.
Configure NPM Commands for End-to-End Testing
At this point I want the e2e tests to run as automatically as the unit tests do. In order to do that I update the
again to add in the new tests.1
package.json
- prestart
- This will install modules and then compile any coffee-script files. The coffee-script compilation shouldn’t be done like this in production and instead be replaced by a deployment task. For now this is a simple way to get the coffee-script compiled.
- start
- Since this application is all client side we start the application by starting a web server and serving the HTML.
- preupdate-webdriver
- Before updating the webdriver component we need to make sure all the required modules are installed.
- update-webdriver
- This will download the standalone Selenium server used in the tests in order to create a real browser session.
- preprotractor
- We need to make sure Selenium is installed before running the actual e2e tests, this will run the task above.
- protractor
- The last step for an e2e test is running protractor which will then execute the individual e2e tests against a real browser session controlled by Selenium.
NOTE With Protractor I have to have the server running which I accomplish by doing an
in an alternate terminal session.1
npm start
Build a Feature
I think it is important to focus on a feature when writing software. By focusing on a feature I am able to connect the people I am building the software to the machine running the software.
For these tests I am going to start with the feature idea of showing my GitHub name and location.
First e2e Test
I am starting out by creating a test which describes opening up the default page and showing my GitHub username.
This test uses
in order to open the browser to the applications entry page. In Protractor this will return a promise, when that promise is resolved I write the main test using Jasmine.1
browser.get(...
- expect
- A Jasmine helper.
- element
- Element is coming from Protractor and associated with the rendered element in on the HTML page.
- By
- This is usually
when using Protractor with JS tests but1
by
is a keyword in coffee-script. In order to avoid the name collision I used1
by
instead, this method creates a selector to find the element on the page. In this case I am looking for an element which has an id of1
By
.1
githubName
- getText
- Instead of checking that the element is an element I want to check the content of that element. This method is a shortcut to get the actual text rendered on the page.
- toBe
- This is also from Jasmine and used for equality checks.
At this point the test will fail so I am going to move on to writing a unit test before implementing the actual code.
First Unit Test
At this point I know my feature requires some information from GitHub in order for the e2e test to pass. In my e2e task I am OK with the external service being hit directly but in this case I don’t want to go out to GitHub each time my tests run. To get around this issue I take advantage of
.1
$httpBackend
There are no tests yet so I am going to make a test for a non-existent model which I will use to retrieve my GitHub information.
- jasemine.createSpy
- This is used to create a fake function so that we can check if it was called appropriately in the
call.1
.then
- then
- I want the
to return a promise which can then be used in the test.1
getUserInfo
- $httpBackend.flush()
- Until
is called the HTTP requests will not be allowed to return.1
.flush()
- toHaveBeenCalledWith
- Using the spy created above we are now able to check what the parameters were to it when it was called during the resolution of the promise.
This test still won’t pass but it describes the basic layout of what I need in order to implement the feature I described above. In a larger project the amount of
calls may get too large. There are a few solutions which can be used to place all the 1
$httpBackend
calls into a single place or a factory.1
$httpBackend
Complete Cycle
At this point I have some tests but no implementation. Reading these tests gives me the basic layout of what I need to create. I need to create a class with one class method which returns a promise; that promise is passed in an object with a
and 1
name
when it is resolved.1
location
Now the tests should pass. None of this code is great but it passes the tests. Later I would hope that pieces like the hard coded URL would get removed and replaced with settings written by Grunt.
With the feature implemented all my tests should pass and I can go about adding a new feature and following the same cycle.
Conclusion
Testing in Angular is something I enjoy a great deal. It is easy to get setup while being robust in features.