-
Notifications
You must be signed in to change notification settings - Fork 52
Angular
In this page we cover some facts about Angular and Jasmine.
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 });
}));
The general rule is fairly simple:
- Let Angular manipulate the DOM
- If you must manipulate the DOM, do it in a directive
- JsObject Directive Example
- Directives
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() {};
Let's go back to the basic examples found here:
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:
- Factory code that compiled and was well formed.
- 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();
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>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?
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'));
});
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 });
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
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.
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:
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.
- Commands | HTML | Edit this Bundle
- 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.
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.
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.

You can also use Grunt to run jshint.
grunt jshint
Read the README.md file for JsonFromServer:
The example demonstrates how to proceed.
Mocking Mongo Data:
See TestMongoTower.js.