4. Modify the seed app (manually modifying the app is good because it will allow us to know the structure intimately, before proceeding to add more items)
Angularjs has been designed to allow us to effectively do TDD. It allows us to do both unit testing as well as end-to-end testing. Currently our project does not have any tests... we will rectify that shortly. Here is our plan:
This opens a browser window & all the files will be watched. Any changes you make will run all the tests. This significantly improves our workflow and allows to achieve TDD. The debug console should allow you to see the test results.
Lets start by writing a simple test. Our first test is to ensure that our Device Service should exist. We will be using jasmine to describe our tests & write expectations of behavior. Our simple test is listed below: The test/unit/serviceSpec.js file already sets up the module in beforeEach so its available later.
We can start to add additional tests for the service. Lets add a test to see if our query functionality is working as expected. Inject the Device service & expect the data lenght to be equal to 7:
Angularjs makes testing controllers pretty straightforward too, once you understand certain basic concepts. Define modules for both services & controllers so that they are available for this spec:
describe('controllers', function(){
beforeEach(function(){
module('cotd.controllers');
module('cotd.services');
});
The first unit test case is to make sure the controller scope has the right number of devices. Remember, the scope is bound in the template, so verifying that it is accurate is a good idea:
it('listDeviceController should have 6 devices in the model',
inject(function($rootScope, $controller, Devices) {
var scope = $rootScope.$new();
var ctrl = $controller('deviceListController', {$scope: scope}, Devices);
// check the number of devices
expect(scope.devices.length).toEqual(7);
}));
Unit test for Adding a device & verifying that the scope accounts for the add:
it('addDeviceController should have 8 devices in the model after add',
inject(function($rootScope, $controller, $location, Devices) {
var scope = $rootScope.$new();
var ctrl = $controller('addDeviceController', {$scope: scope}, $location, Devices);
var item = {id:7, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"};
scope.add(item);
// make sure the flag is true
expect(scope.add).toBeTruthy();
// check actual add effected the devices list
expect(Devices.query().length).toEqual(8);
}));
Lets test the edit functionality too:
it('editDeviceController should successfully edit devices in the model',
inject(function($rootScope, $controller, $location, $routeParams, Devices) {
var scope = $rootScope.$new();
var ctrl = $controller('editDeviceController', {$scope: scope}, $routeParams, $location, Devices);
var item = {id:0, name: "iphone", assetTag:"a23456", owner:"qa", desc:"iOS4.3"};
// testing for update flag
expect(scope.add).toBeFalsy();
// confirm original lenght
expect(Devices.query().length).toEqual(7);
// confirm description
expect(Devices.query()[0].desc).toEqual('iOS4.2');
// update
scope.update(item);
// validate update
expect(Devices.query()[0].desc).toEqual('iOS4.3');
expect(Devices.query()[0].owner).toEqual('qa');
}));
Writing End To End Tests
Writing and executing End to End (e2e) tests is a powerful mechanism that angularjs provides which is extremely valuable for large-scale applications. Angular consructs uses jquery and matchers to help us write pretty quick e2e tests. The Karma runner also makes it easy to run the tests in multiple browsers.
E2E tests starts the browser and uses that as a client which has an added advantage from other unit testing frameworks.
describe('my app', function() {
beforeEach(function() {
browser().navigateTo('../../app/index.html');
});
it('should automatically redirect to / when location hash/fragment is empty', function() {
expect(browser().location().url()).toBe("/");
});
describe('DisplayList', function() {
beforeEach(function() {
browser().navigateTo('#/');
});
This E2E test allows navigates to the displayDevices page and can use jquery DOM matching to test for specific conditions. For example, below, it tests for item.owner column in the table.
it('should render DisplayDevices when user navigates to /#', function() {
expect(element('[ng-view] a').text()).
toMatch(/Would you like to add a new device/);
expect(element('[ng-view] th').text()).
toMatch(/Name/);
// counting number of rows
expect(repeater('tbody tr').count()).toBe(7);
// matching the resulting elements in a column
expect(repeater('tbody tr').column("item.owner"))
.toEqual(["dev","dev","qa","dev","dev","qa","dev"]);
});
// clicking one item in the table using :eq(0)
it("clicking remove link should remove device", function(){
element('tbody tr:eq(0) .icon-trash').click();
expect(repeater('tbody tr').count()).toBe(6);
});
// clicking a link should take you to add device URL, selection by id
it("clicking add Device link should take you to right screen", function(){
element('#addlink').click();
expect(browser().location().url()).toBe('/add');
});
});
Here is the snippet to do E2E test for adding a new device:
// test for Add & subsequent expectation that the devices list has the added value
it('should add a Device when user enters values and submits them', function() {
input('device.name').enter('TestName');
input('device.assetTag').enter('TestAssetTag');
input('device.owner').enter('TestOwner');
input('device.desc').enter('TestDescription');
element('#addDevice').click();
// counting number of rows
expect(repeater('tbody tr').count()).toBe(8);
// matching the resulting elements in a column
expect(repeater('tbody tr').column("item.owner"))
.toEqual(["dev","dev","qa","dev","dev","qa","dev", "TestOwner"]);
});
Here is the test snippet for modifying & testing the modified element. We are navigating to the first record "#/edit/0' using the browser control. Then we use a very similar pattern to enter values and test the result.
describe('updateDeviceView', function() {
beforeEach(function() {
browser().navigateTo('#/edit/0');
});
// test for update & subsequent expectation that the devices list has the updated value
it('should update a Device when user enters values and submits them', function() {
input('device.name').enter('TestName');
input('device.assetTag').enter('TestAssetTag');
input('device.owner').enter('TestOwner');
input('device.desc').enter('TestDescription');
element('#updateDevice').click();
// counting number of rows
expect(repeater('tbody tr').count()).toBe(7);
// matching the resulting elements in a column
expect(repeater('tbody tr').column("item.owner"))
.toEqual(["TestOwner","dev","qa","dev","dev","qa","dev"]);
});
});
With this we complete our testing. We refactored our code base using angular-seed which helps us tremendously to organize our code & provides a full functional workflow environment to do TDD. To download the latest code, you can checkout the following version from git.
git checkout v1.4
Next up is to retrieve the data from an external REST based backend. We will build a Node Express Mongo backend which integrates with our angularjs frontend.