Unit testing is one of the important and most often overlooked process in development. When we started development in Silverlight, we were so excited that with clear model, view and viewmodel separation, we were all set to create a fully tested code. Before you knew it, we created production code with Silverlight and shipped with 0% unit test. So the bottom line is, even if there are ways to create unit test, it is up to us to create unit tests. Well, creating unit tests is one thing and creating proper unit test is different thing. I remember creating unit tests, with reading data from database, calling bunch of methods to massage data before actually calling the method to test. I learned it hard way that, it was not right unit test. So there are two aspects of unit tests we need to remember, one is to write unit test and another is to write proper unit test. In this blog, lets look at the unit testing in Angular. Here is where I am going to break off from Silverlight and start to focus on Angular. One side note, because of the nature of Javascript when developing big application, do yourself a favor make sure, it have 100% coverage.
AngularJS has a decent but incomplete document on unit testing. Hopefully they will come around and finish it. We will look at straight forward unit testing with simple controller. If you have noticed so far, our controller is nothing but pure Javascript. It does not have any references to anything else like DOM manipulation or XFR calls. So our testing so far will be nothing but simple Javascript testing. It turned out there are ton of unit testing frameworks available for Javascript. As a Javascript beginner myself, I was looking for some testing framework which looked familiar and finally I settled on Jasmine. I made this decision purely on my previous RSpec experience. Once I get a handle on that, I will explore the other options available. If you would follow this link, it has fantastic one page reference to all the features available in Jasmine. It should not take more than an hour to go through and try them all out.
Lets see how can we create unit test for one of our sample code.
Problem: Given two numbers(integer or float, positive or negative), add them.
Approach: As a developer, I am still learning to approach this problem with TDD in mind. So to do TDD, you first write out the test cases your function need to handle before writing the actual code. So what are the possible test cases?
- It should add two valid integer numbers.
- It should add one integer and one float.
- it should add two float numbers.
- It should add when the numbers are zeros.
- It should add when one number is zero.
- It should add two negative numbers.
- It should add when one number is negative number.
- If first parameter is non numeric number, do not add.
- If second parameter is non numeric, do not add.
- If both the input are non numeric, do not add.
The proper TDD is only to write unit test based on the requirements. If you look at the (8-10), it was not mentioned in the requirement then why would we need to handle that situation? So don’t write those test cases.
So based on our requirement, I came up with 7 (skipped 3) test cases. Following TDD, write test first, make it fail and then write the code to pass. I came across two different approaches as well. One was to write the first test, make it fail and then write the code to make it pass and then go to your next test. The second approach was to write all the tests first and write the code to make them pass. I like earlier approach than later one. As I said before, I am going to use Jasmine and you can follow the install instructions in Jasmine to get Jasmine going and then follow along here; Here is the directory structure of my application
Under main web site, I have two folders, one for the app code and another folder for the testing. This way, we will not mix the production code with testing code and will not distribute the test code to production either. The test folder has spec folder, this is where all the unit tests housed. In the root of the test folder we have SpecRunner.html (we will see that in a minute), which when we run, will run all all the tests specified in the specrunner.
Iteration 1:
Unit Test:
First we are going to write our test code and we call it addition.spec.js; and the code looks like the following to accommodate our first test;
1 describe("Addition", function () {
2
3 var $scope, ctrl;
4
5 beforeEach(inject(function ($rootScope, $controller) {
6 $scope = $rootScope.$new();
7 ctrl = $controller('additionCtrl', {
8 $scope: $scope
9 });
10 }));
11
12 it("should add two integer numbers.", function () {
13 $scope.add(2, 3);
14 expect($scope.Result).toEqual(5);
15 });
16 });
17
Just to test 2 line of code we ended up writing close to 16 lines of code. This is because, we want to bring angular context and scope in to the testing as if it is running through angular. It is done with angular-mock.js, this will get added to specrunner.html in the next step. For now, lets look at the line (5-10). It is important to understand, how does the test run. When Jasmine runs, it will first look for any ‘beforeEach’ (line 5) and if found, in our case, there is one and it get executed before every test. A test is nothing but the one starts with ‘it’, like at line (12).
Right now in our application, we are using default application and module and we are only creating a controller. As you know, in our controller code, we pass $scope from angular context to the controller. So to test our code, we need create mock context and that is what the controller have to use. To achieve this, before each test code, we will call inject method and for the giving rootScope, we create a mock Angular scope and that is passed to the controller in test as the scope. For now, you can use this as a template for writing our test and accessing the scope variable inside the test.
Lets look at the test itself, removing all the angular part out of it, which you will repeat in all your test suit once anyway. Without angular part in it, the test by itself will look like the following;
1 describe("Addition", function () {
2 it("should add two integer numbers.", function () {
3 $scope.add(2, 3);
4 expect($scope.Result).toEqual(5);
5 });
6 });
So here is the rundown of this test;
- All the test starts with a test suite with ‘describe’ (line 1). Each test suite is given a suit name (Addition, in our case).
- If each test needs some common things to happen, like initializing, then call ‘beforeEach’ (line 5 in the previous complete test code).
- Every test, starts with ‘it’. ‘it’ has two parts (line 2), one is the name of the test and second one the test itself.
- In every test, after some action, validate the test result by ‘expect’ (line 4). ‘expect’ also has three parts, source, target and comparison operator. In our example, source is $scope.Result, target is ‘5’ and comparison operator is ‘toEqual’.
I would strongly recommend anyone interested in testing with Jasmine to read through their fantastic documentation with example.
Jasmine Spec Runner:
Now that the first test unit test code completed, lets add the information required to run the unit test in Jasmine. We run Specrunner.html to the tests we want to run.
1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
2 "http://www.w3.org/TR/html4/loose.dtd">
3 <html>
4 <head>
5 <title>Jasmine Spec Runner</title>
6
7 <link rel="shortcut icon" type="image/png" href="http://searls.github.com/jasmine-all/jasmine_favicon.png">
8 <link rel="stylesheet" type="text/css" href="http://searls.github.com/jasmine-all//jasmine.css">
9 <script type="text/javascript" src="http://searls.github.com/jasmine-all/jasmine-all-min.js"></script>
10
11 <script type="text/javascript" src="http://code.angularjs.org/1.0.6/angular.js"></script>
12 <script type="text/javascript" src="http://code.angularjs.org/1.0.6/angular-mocks.js"></script>
13
14 <!-- include source files here... -->
15 <script type="text/javascript" src="../App/Scripts/additionCtrl.js"></script>
16
17 <!-- include spec files here... -->
18 <script type="text/javascript" src="spec/addition.spec.js"></script>
19 </head>
20
21 <body>
22 </body>
23 </html>
24
Line (6 – 12) are all the required javascripts. Based on your application, you will add more library scripts here.
Line (15) specified which is the controller under test. This is pointing to the production code under ‘App’ folder.
Line (18) specifies, which are all the unit tests to run. In our case we have only one unit test spec to run.
App Setup:
You can run the test as it is now and it will fail with error like unable to find additionalCtrl etc., that is our red to green approach anyway.
Now that all the hooks are in place, open the windows explorer, find Specrunner.html and double click it. It will open a browser and run the test. This test will fail since we did not add any controller or view. So lets add them.
view:
1 <!DOCTYPE html>
2 <html ng-app>
3 <head>
4 <title></title>
5 </head>
6 <body>
7 <div ng-controller="additionCtrl">
8 First Number: <input ng-model="NumberOne"/><br/>
9 Second Number: <input ng-model="NumberTwo"/>
10 <button ng-click="add(NumberOne, NumberTwo)">Add</button><br/>
11 Result: {{Result}}
12 </div>
13 <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js"></script>
14 <script src="Scripts/additionCtrl.js"></script>
15 </body>
16 </html>
This is nothing more than vie, which we have seen before.
Controller:
Since we are following TDD, lets try make sure we stick to the minimum code to make the test pass.
1 function additionCtrl($scope) {
2 $scope.NumberOne = 0;
3 $scope.NumberTwo = 0;
4 $scope.Result = 0;
5 $scope.add = function (a, b) {
6 $scope.Result = parseInt(a) + parseInt(b);
7 };
8 }
This code takes two objects and convert it to int and then add them. As you would expect nothing special and still javascript function. Now if you would run the test should run successfully.
Now that our first test is successful, lets write the second test.
Iteration 2:
It should add one integer and one float. Add the following code to the addition.spec.js
1 it("should add one integer and one float numbers.", function () {
2 $scope.add(2, 3.5);
3 expect($scope.Result).toEqual(5.5);
4 });
When this test is run, we would expect it to pass but it will fail since we are doing parseInt, which is converting the number to int thus losing the fraction part. So now the test is RED, go back to and fix the code to make it run. Instead of using parseInt, use Number function, which will retain the fraction.
Here is the modified controller
1 function additionCtrl($scope) {
2 $scope.NumberOne = 0;
3 $scope.NumberTwo = 0;
4 $scope.Result = 0;
5 $scope.add = function (a, b) {
6 $scope.Result = Number(a) + Number(b);
7 };
8 }
Now if you run specrunner.html the test pass. You keep adding test and make sure all test pass.
While learning unit testing with Angular, I ran into bunch of problems, thanks to @karlgold for helping me sort things out.