Skip to content
Charlie CedarIsle Calvert edited this page Feb 22, 2015 · 1 revision

Angular

In this page we cover some facts about Angular and Jasmine.

Declaring Controllers

We should probably end up putting all our controllers inside the scope of a particular module, using DOT notation:

angular.module('myApp', []).controller('myController', function() {

Here is a more complete example:

angular.module('myApp', []).controller('MileController', function($scope) {
    $scope.hint = "Enter a number of miles";
    
    $scope.miles = 0;
    
    $scope.convertMilesToInches = function() {
        return $scope.miles * 5280 * 12;  
    };    
});

When you declares controllers like this, then you want to use the module call in your Jasmine unit tests:

beforeEach(module('myApp'));

beforeEach(inject(function($rootScope, $controller) {
	mileController = $rootScope.$new();
	$controller('MileController', { $scope: mileController }); 
}));

DOM Manipulation

The general rule is fairly simple:

Scope

The scope in Angular is a means of working with the templates in our HTML. If we were using jQuery, we might write code like this in a Controller to update and track code in an input control:

$('#foo').val('bar');
var userInput = $('#foo').val(); // Yields string bar

In the simplest, most reductive possible terms, that is what scope does for you. In Angular html templates we write something that might include this code:

<input type="text" ng-model='foo'>

In our Angular controllers, we write:

$scope.foo = 'bar'

This is another way of writing the first jquery statement shown above. Here is a way to write the second:

var userInput = $scope.foo;

Now lets talk about buttons and clicks.

In jQuery we write:

$('#myButton').click(function() {});

In Angular we write something like this in the HTML:

<button ng-click('buttonHandler()')>My Button</button>

And then in the controller:

$scope.buttonHandler = function() {};

Calling Methods in Factories

Let's go back to the basic examples found here:

Angular Modules

Look at the code for the main module:

angular.module('elvenApp', ['tools'])
.controller('BoatController', function($scope, boat, sailboat) { 'use strict';
	$scope.simple = "Simple Boat";
	$scope.boatType = boat.getDescription();
	$scope.sailBoat = sailboat.getDescription();
	$scope.getNine = function() {
		return sailboat.getNine();
	};
});

As you can see, we are calling several function that are located in our factory. For instance, we are calling getDescription and getNine.

Here are the implementations for the factories:

angular.module('tools', [])
.factory('boat', function() {  'use strict';
	this.Boat = (function() {
		var description = "I'm a boat.";

		function Boat() {

		}

		Boat.prototype.getDescription = function() {
			return description;
		};

		return Boat;
	})();

	return new this.Boat();
})
.factory('sailboat', function() { 'use strict';
	this.SailBoat = (function() {
		var description = "I'm a sailboat";

		function SailBoat() {

		}

		SailBoat.prototype.getNine = function() {
			return 9;
		};

		SailBoat.prototype.getDescription = function() {
			return description;
		};

		return SailBoat;
	})();

	return new this.SailBoat();
});

All that was really needed to make this work was:

  1. Factory code that compiled and was well formed.
  2. Controller code that used the tools module, and that injected instances of the objects found in the tools mod:
angular.module('elvenApp', ['tools'])
.controller('BoatController', function($scope, boat, sailboat) { 'use strict';

Once we have included the tools module, then we can easily inject the two factories called boat and sailboat. Now we can call methods on those objects:

boat.getDescription();
sailBoat.getNine();

Select

Set it up like this:

$scope.chartSelect = {
    "type": "select",
    "name": "Service",
    "value": "PieChart",
    "values": [ "PieChart", "BarChart", "ColumnChart", 
         "AreaChart", "LineChart", "Table"]
};

The HTML should look like this:

<select ng-model="chartSelect.value" 
     ng-options="v for v in chartSelect.values" ng-change="chartTypeUpdate()">
</select>

Jasmine Matchers

There are many Jasmine matchers.

You can look for an exact match like the === operator:

it("expects 1 + 1 to equal 2", function() {
	expect(1+1).toBe(2);
});

For a less precise match like the == operator:

it("expects 1 + 1 to equal 2", function() {
	expect(1+1).toEqual(2);
});

Or a more forigiving match for floating point numbers:

it("1.799 is close to 1.8", function() {
	expect(1.799).toBeCloseTo(1.8);
});

Here are some of the more important Jasmine matchers and a hopefully reasonable effort to define what they do:

  • toBe: This is very precise, like using ===.
  • toBeDefined: Is it not undefined
  • toBeCloseTo: Compare two floating point numbers.
  • toBeFalsy: Is it false, an empty string, null, undefined, etc
  • toBeGreaterThan: Is one number larger than another number
  • toBeLessThan: Is one number less than another
  • toBeNull: Test for null
  • toBeUndefined: Is the value "undefined".
  • toBeTruthy: Is it true or something equivalent.
  • toContain: Search an array for a value
  • toEqual: Less precise than toBe, like using == rather than ===
  • toMatch: Compare strings with regular expressions
  • toThrow: Does an expression throw an exception?

Use toThrow Matcher

Sometimes you want to prove that trying to do some particular action will raise an exception. Jasmine has the toThrow matcher to handle these cases. When calling toThrow there is a bit of a gotcha. To get over this hurdle, you have to use a an anonymous function, as shown below.

Consider this example. We have a method called tryToCallNew which is set up to always thrown an exception. To use toThrow we must create an anonymous function, call createError and test if it returns the error we expect:

function createError() {
    try {
        throw new Error("Intentional error");
    } catch(e) {
        throw new Error('error');
    }
}

it("throws an exception", function() {        
    expect(function() { tryToCallNew(); }).toThrow(new Error('error'));
});

Even though the method created throws an error, our test passes.

Let's do the same thing, but cause the error a different way:

var objectMethod = {
    a: 1
};

function tryToCallNew() {
    try {
        new objectMethod();
    } catch(e) {
        throw new Error('error');
    }
}

it("cannot be used with new", function() {        
    expect(function() { tryToCallNew(); }).toThrow(new Error('error'));
});

You can't call new on object like the one we created. So our attempt to do so raises an error. But our test passes because it expects the attempt to raise the error.

Here is another example of how to use toThrow. In this case, we assume that calling new objectMethod() raises a TypeError because objectMethod is not a function:

it("cannot be used with new", function() {        
    expect(function() { new objectMethod(); }).toThrow(new TypeError('object is not a function'));
});

Unit Test Names

I'm belatedly realizing that we can establish better naming conventions in our unit tests.

We don't seem to be using this variable:

var pc = null;
...
pc = $controller('MileController', { $scope: npcController });

So we can just eliminate it:

$controller('MileController', { $scope: npcController });

There is usually a better name for $mockScope:

var $mockScope = null;
...
$mockScope = $rootScope.$new();
$controller('MileController', { $scope: $mockScope });

We can call it mileController in a case like this, since that is what it ends up holding:

var mileController = null;
...
mileController = $rootScope.$new();
$controller('MileController', { $scope: mileController });

Dialogs in Unit Tests

We can handle that $dialog in a way slightly different from the one I outlined to you. Here is what I suggested before:

describe("mycontrollertest", function() {'use strict'; var npcController = null;
var $dialog = null;

beforeEach(inject(function($rootScope, $controller) {
    npcController = $rootScope.$new();
    pc = $controller('NPCController', { $scope: npcController, $dialog: $dialog }); 
}));

Apparently we can do this:

describe("mycontrollertest", function() {'use strict'; var npcController = null;

beforeEach(inject(function($rootScope, $controller) {
    npcController = $rootScope.$new();
    pc = $controller('NPCController', { $scope: npcController, $dialog: null }); 
}));

In this example I declare $dialog to be null, and I don't need to declare it as global to our object.

###Some Basic Mocking {#basicMock}

Here are some tests that provide the first instance we have seen of creating a mock object:

beforeEach(inject(function($rootScope, $controller) {
	gameBoard = $rootScope.$new();
	gameEventService = { towerBroadcast: function() { return true; } };
	elfgameService = $rootScope.$new();
	$controller('GameBoard', { 
		$scope: gameBoard, 
		gameEventService: gameEventService, 
		elfgameService: elfgameService 
	});
}));

Notice this line from the code shown above:

gameEventService = { 
	towerBroadcast: function() { return true; } 
};

This code mocks our event service by simply returning true rather than actually send the message. This line looks as though it is retreiving a real gameEventService object, but it just using our mock:

$controller('GameBoard', { 
	$scope: gameBoard, 
	gameEventService: gameEventService, 
	elfgameService: elfgameService 
});

Now we can write tests that depend on making calls to the towerBroadcast method of our gameEventService:

it("Check ElfGame Width", function() {
	var actual = elfgameService.reportEvent();
	expect(actual).toEqual(true);
});

This code calls reportEvent which in turn calls gameEventServer.towerBroadcast.

###JSON from Server {#jsonFromServer}

Here is how to retrieve JSON from a server.

var getDataJson = $http.get('data.json');

getDataJson.success(function(data, status, headers, config)  {
	$scope.data = data;
});
	
getDataJson.error(function(data, status, headers, config) {
	throw new Error('Oh no! An Error!');
});

###Validating Angular HTML

Angular Starter Projects

In JsObjects on GitHub, there are several starter project for working with Angular, MongoDb, Karma, Jasmine and Grunt. These projects are quite useful as they will help you get over the fussy coding required to get all your tools in place.

If you use these projects a few times, you should soon reach the state where you can pull one down, and start Grunt JsHint, and Karma continual testing in less than a minute. The projects ship with sample unit tests, but you might even be able to add your first new unit test in that time. They provide a great jump start for people who have a moderate knowledge of how to create and test projects using Angular, Jasmine, Karma and Grunt with JsHint.

Elf Ruble and Angular

There is an add on (a Ruble) for Aptana that will allow you to create Elven Angular Projects and other things. See the ReadMe for details:

Angular Git Starter Projects in Aptana

There is a second way to get the projects that are stored in the Elf Ruble. This sections describes how to pull them directly from GitHub.

You can use the projects described above via the File | New Web Site command in Aptana Studio. When used that way, they act as new Project Templates that extend the power of Aptana by allowing you automatically create projects that support Angular, Jasmine, Karam, Grunt and JsHint.

If you have not done so already, open up the HTML bundle in Aptana. There are two possible ways to do this. Pick the one that works on your system.

  1. Commands | HTML | Edit this Bundle
  2. Commands | Other | HTML | Edit this Bundle

It may take a moment, but eventually you should see a new folder called HTML in your workspace.

Open the templates directory and find the file called project_templates.rb. Paste the code shown below into the bottom of it. Please note the line feed after the final end. That is needed or the IDE will complain.

project_template "Elvenware Angular Unit Test Project " do |t|
  t.type = :web
  t.tags = ['Web']
  t.icon = "templates/HTML5_Logo_64.png"
  t.id = "com.elvenware.project.template.web.html5"
  t.location = "git://github.com/charliecalvert/AngularTest.git"
  t.description = "Remote template. Requires network access."
  t.replace_parameters = false
  t.tags = ['Web']  
end

project_template "Elvenware Angular Jasmine Karma Project " do |t|
  t.type = :web
  t.tags = ['Web']
  t.icon = "templates/HTML5_Logo_64.png"
  t.id = "com.elvenware.project.template.web.html5"
  t.location = "git://github.com/charliecalvert/AngularKarma.git"
  t.description = "CSC Remote template. Requires network access."
  t.replace_parameters = false
  t.tags = ['Web']  
end

project_template "Elvenware Angular Mongo Bootstrap Project " do |t|
  t.type = :web
  t.tags = ['Web']
  t.icon = "templates/HTML5_Logo_64.png"
  t.id = "com.elvenware.project.template.web.html5"
  t.location = "git://github.com/charliecalvert/AngularMongoBootstrapTest.git"
  t.description = "CSC Remote template. Requires network access."
  t.replace_parameters = false
  t.tags = ['Web']  
end

Restart Aptana. Select File | New | Web Project. Select the project called Elvenware Angular Unit Test Project. Create the project as usually, filling in the name of the project. Run the two HTML files and confirm that they work.

Note that the template for the project is stored on GitHub, so you have to be connected for this to work. That's a drawback, but there are obvious benefits to pulling from a repository that I can easily update.

Since this project was pulled from GitHub, it includes a .git folder. You should consider removing this folder if you do not want to use git, or if this folder is embedded inside another git repository.

After you have restarted Aptana, you can use these Project templates to create new projects.

If you have some basic knowledge of Grunt and Karma, then you can use these tools with these projects. To get started, run npm install in the root directory for the project:

npm install

Next, start Karma by typing karma start:

karma start

Periodically, you should go to the command line in the root directory for this folder and run grunt jshint.

grunt jshint

You should then examine the result.xml file to look for any problems in your code.

Also, read the README.md files for these projects.

Instructions for the Angular Three Assignment

With thanks to Margie Calvert for helping to assemble this information.

This exercise creates a very simple function inside an angular module, then hooks it up appropriately with a controller, a unit test, and an index page. It uses Charlie's Aptana Ruble to get started.

Use the Elvenware Angular Jasmine Karma project. This contains the necessary files to run Karma on the code you generate, as you generate it.

After you install the project in Aptana, set up the following files.

###FourModule.js file

Use a new JavaScript Template (File -> New From Template -> JavaScript -> JavaScript Template) to create a blank javascript page. Call it FourModule.js and save it in the Source directory of your project.

Use this code in the module:

angular.module("fourModule", [])
.factory('fourFactory', function() {'use strict';
	return {
		getFour : function(){
		return 4;
		}
	};
});

###TestMain.js file

Now set up the Unit Test. Go to TestMain.js, and make these changes:

Locate this line of code near the the top of the file.

describe("Test Main", function() {'use strict';

Create a variable called (var fourFactory = null;). This code will be somewhere beneath this declaration: var MainController = null;

Then find the following lines of code:

(beforeEach(function() {
	module('mainModule'); 
	// Insert your code here.

Inset the following: module('fourModule');

Scroll down past this code:

beforeEach(inject(function($rootScope, 
	$controller, $injector) {
	mainController = $rootScope.$new();
	$controller('mainController', {
	$scope : mainController
	// Insert your code here
});

Type this:

fourFactory = $injector.get('fourFactory');

Go below the })); and type this code:

it("gets the number four", function() {
	var actual = fourFactory.getFour();
	expect(actual).toEqual(4);
});

###karma.conf.js

Open up karma.conf.js, which is in the Tests folder. You will see something like this after the first little bit:

files: [   
	'Library/angular.js',
	'Library/angular-mocks.js',
	'Tests/TestMain.js',
	'Source/Main.js',
	'Source/NewModule.js',
	'Source/EightModule.js',
	'Source/TenModule.js',
	'Source/OneModule.js',
	'Source/ThreeModule.js', 
],

Add 'Source/FourModule.js' to the list. I already added a few other modules, so your list will look different.

###Main.js

Add the module into the list in the brackets. Any time you add a module, add the name here. The first line will look something like this before you change anything.

angular.module('mainModule', ['newModule', 
	'eightModule', 'tenModule', 
	'oneModule', 'threeModule'])

In my code, I already added quite a few modules, which you will not have. Don't forget to put the name of your module in quotes and don't forget to use a comma to separate any modules in the brackets.

So now in my code it looks like this:

angular.module('mainModule', ['newModule', 
	'eightModule', 'tenModule', 'oneModule', 
	'threeModule', 'fourModule'])

The second line looks something like this:

.controller('mainController', function($scope, 
	newFactory, eightFactory, tenFactory, 
	oneFactory, threeFactory) { 'use strict';

Add fourFactory to the list.

.controller('mainController', function($scope, 
	newFactory, eightFactory, tenFactory, oneFactory, 
	threeFactory, fourFactory) { 'use strict';

The next line is

$scope.name = "mainController";

Somewhere under that, put this code:

$scope.getFour = fourFactory.getFour();

This is also where you would write a function for the mainController. In my code things look like this:

$scope.add = function(a, b) {
	return a + b;
};

$scope.getNine = newFactory.getNine();
$scope.getEight = eightFactory.getEight();
$scope.getTen = tenFactory.getTen();
$scope.getOne = oneFactory.getOne();
$scope.getThree = threeFactory.getThree();
$scope.getFour = fourFactory.getFour();

Then way at the bottom you will see });

###index.html

Add this code in the

section:
<script src = "Source/FourModule.js"> </script>

In the body you will see a div id tag:

<div id="textDisplay" data-ng-controller="mainController">

Below that, put your display instructions:

<p>Get Four:  {{getFour}}</p>

Now try running index.html. It should show the results of your work.

Using Karma

Karma is a wrapper around unit testing frameworks. It helps automate the way we run our tests. It is commonly used with AngularJs. It once had a name so absurd that I refuse to repeat it here. The name change is fairly recent, so you may find references to the old name here and there.

To install Karma:

npm install -g karma

Test from command line to see if it is installed:

>karma --version
Karma version: 0.10.2

In the command terminal in Aptana, navigate to the directory where you have your project. You will be starting in your Users/myName directory. So if the project is in the isit320 directory, you might have to cd to Documents/isit320/currentprojectfolder.

run

npm install

and then type

karma start

###Coverage

Code coverage let's you know what code in your program is not covered by unit tests.

First install coverage tool, which is called Istanbul:

npm install -g istanbul

In some cases, you may already have a package.json that includes karma-coverage, so just rerun npm install. However, of other projects, you can install coverage and save a reference for it in your package.json file by typing the following:

npm install karma-coverage --save-dev

When you are done, you can open up package.json and find the entry for karma-coverage.

Then you need to modify three parts of karma.conf.js:

  • preprocessors
  • reporters
  • plugins

In the preprocessors section of karma.conf.js:

preprocessors: {
	'Source/**/*.js': ['commonjs', 'coverage'],
	},

When defining your coverage support, remember that it is up to you to tell coverage where the files are that are being tested. You don't have to point to the test files, just the files that are being tested. For most of our programs, that means doing something like this in the preprocessors statement:

'Source/**/*.js'

When you get it right, you should see Coverage produce an HTML file for each JavaScript file in your Source directory.

Add on support for your reports:

reporters: ['progress', 'coverage', 'junit'],

And in your plugins at the bottom of karam.conf.js and in karam-coverage:

plugins: [      
      'karma-jasmine',
      'karma-coverage',
      'karma-chrome-launcher',
      'karma-firefox-launcher',
      'karma-junit-reporter',
      'karma-commonjs'
    ]

The results end up in a folder called coverage in a series of HTML files. Open the files in your browser.

Coverage of Simpler Controller

Grunt

You can also use Grunt to run jshint.

grunt jshint

Mocking Objects with $httpBackend

Read the README.md file for JsonFromServer:

The example demonstrates how to proceed.

Mocking Mongo Data:

See TestMongoTower.js.

Clone this wiki locally