Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

The First Step

Save for later
  • 16 min read
  • 04 Feb 2015

article-image

The First Step

In this article by Tim Chaplin, author of the book AngularJS Test-driven Development, provides an initial introductory walk-through of how to use TDD to build an AngularJS application with a controller, model, and scope. You will be able to begin the TDD journey and see the fundamentals in action. Now, we will switch gears and dive into TDD with AngularJS. This article will be the first step of TDD. This article will focus on the creation of social media comments. It will also focus on the testing associated with controllers and the use of Angular mocks to AngularJS components in a test.

(For more resources related to this topic, see here.)

Preparing the application's specification

Create an application to enter comments. The specification of the application is as follows:

  • Given I am posting a new comment, when I click on the submit button, the comment should be added to the to-do list
  • Given a comment, when I click on the like button, the number of likes for the comment should be increased

Now that we have the specification of application, we can create our development to-do list. It won't be easy to create an entire to-do list of the whole application. Based on the user specifications, we have an idea of what needs to be developed. Here is a rough sketch of the UI:

first-step-img-0

Hold yourself back from jumping into the implementation and thinking about how you will use a controller with a service, ng-repeat, and so on. Resist, resist, resist! Although you can think of how this will be developed in the future, it is never clear until you delve into the code, and that is where you start getting into trouble. TDD and its principles are here to help you get your mind and focus in the right place.

Setting up the project

I will provide a list in the following section for the initial actions to get the project set up.

Setting up the directory

The following instructions are specific to setting up the project directory:

  1. Create a new project directory.
  2. Get angular into the project using Bower:

    bower install angular

  3. Get angular-mocks for testing using Bower:

    bower install angular-mocks

  4. Initialize the application's source directory:

    mkdir app

  5. Initialize the test directory:

    mkdir spec

  6. Initialize the unit test directory:

    mkdir spec/unit

  7. Initialize the end-to-end test directory:

    mkdir spec/e2e

Once the initialization is complete, your folder structure should look as follows:

first-step-img-1

Setting up Protractor

In this article, we will just discuss the steps at a higher level:

  1. Install Protractor in the project:

    $ npm install protractor

  2. Update Selenium WebDriver:

    $ ./node_modules/protractor/bin/webdriver-manager update

    Make sure that Selenium has been installed.

  3. Copy the example chromeOnly configuration into the root of the project:

    $ cp ./node_modules/protractor/example/chromeOnlyConf.js .

  4. Configure the Protractor configuration using the following steps:
    1. Open the Protractor configuration.
    2. Edit the Selenium WebDriver location to reflect the relative directory to chromeDriver:

      chromeDriver: './node_modules/protractor/selenium/chromedriver',

    3. Edit the files section to reflect the test directory:

      specs: ['spec/e2e/**/*.js'],

  5. Set the default base URL:

    baseUrl: 'http://localhost:8080/',

Excellent! Protractor should now be installed and set up. Here is the complete configuration:

exports.config = { chromeOnly: true, chromeDriver: './node_modules/protractor/selenium/chromedriver', capabilities: { 'browserName': 'chrome' }, baseUrl: 'http://localhost:8080/', specs: ['spec/e2e/**/*.js'], };

Setting up Karma

Here is a brief summary of the steps required to install and get your new project set up:

  1. Install Karma using the following command:

    npm install karma -g

  2. Initialize the Karma configuration:

    karma init

  3. Update the Karma configuration:

    files: [ 'bower_components/angular/angular.js', 'bower_components/angular-mocks/angular-mocks.js', 'spec/unit/**/*.js' ],

Now that we have set up the project directory and initialized Protractor and Karma, we can dive into the code. Here is the complete karma.conf.js file:

module.exports = function(config) { config.set({ basePath: '', frameworks: ['jasmine'], files: [ 'bower_components/angular/angular.js', 'bower_components/angular-mocks/angular-mocks.js', 'spec/unit/**/*.js' ], reporters: ['progress'], port: 9876, autoWatch: true, browsers: ['Chrome'], singleRun: false }); };

Setting up http-server

A web server will be used to host the application. As this will just be for local development only, you can use http-server. The http-server module is a simple HTTP server that serves static content. It is available as an npm module. To install http-server in your project, type the following command:

$ npm install http-server

Once http-server is installed, you can run the server by providing it with the root directory of the web page. Here is an example:

$ ./node_modules/http-server/bin/http-server

Now that you have http-server installed, you can move on to the next step.

Top-down or bottom-up approach

From our development perspective, we have to determine where to start. The approaches that we will discuss in this article are as follows:

  • The bottom-up approach: With this approach, we think about the different components we will need (controller, service, module, and so on) and then pick the most logical one and start coding.
  • The top-down approach: With this approach, we work from the user scenario and UI. We then create the application around the components in the application.

There are merits to both types of approaches and the choice can be based on your team, existing components, requirements, and so on. In most cases, it is best for you to make the choice based on the least resistance. In this article, the approach of specification is top-down, everything is laid out for us from the user scenario and will allow you to organically build the application around the UI.

Testing a controller

Before getting into the specification, and the mind-set of the feature being delivered, it is important to see the fundamentals of testing a controller. An AngularJS controller is a key component used in most applications.

A simple controller test setup

When testing a controller, tests are centered on the controller's scope. The tests confirm either the objects or methods in the scope. Angular mocks provide inject, which finds a particular reference and returns it for you to use. When inject is used for the controller, the controllers scope can be assigned to an outer reference for the entire test to use. Here is an example of what this would look like:

describe('',function(){ var scope = {}; beforeEach(function(){ module('anyModule'); inject(function($controller){ $controller('AnyController',{$scope:scope}); }); }); });

In the preceding case, the test's scope object is assigned to the actual scope of the controller within the inject function. The scope object can now be used throughout the test, and is also reinitialized before each test.

Initializing the scope

In the preceding example, scope is initialized to an object {}. This is not the best approach; just like a page, a controller might be nested within another controller. This will cause inheritance of a parent scope as follows:

<body ng-app='anyModule'> <div ng-controller='ParentController'> <div ng-controller='ChildController'> </div> </div> </body>

As seen in the preceding code, we have this hierarchy of scopes that the ChildController function has access to. In order to test this, we have to initialize the scope object properly in the inject function. Here is how the preceding scope hierarchy can be recreated:

inject(function($controller,$rootScope){ var parentScope = $rootScope.$new(); $controller('ParentController',{$scope:parentScope}); var childScope = parentScope.$new(); $controller('AnyController',{$scope: childScope}); });

There are two main things that the preceding code does:

  • The $rootScope scope is injected into the test. The $rootScope scope is the highest level of scope that exists.
  • Each level of scope is created with the $new() method. This method creates the child scope.

In this article, we will use the simplified version and initialize the scope to an empty object; however, it is important to understand how to create the scope when required.

Bring on the comments

Now that the setup and approach have been decided, we can start our first test. From a testing point of view, as we will be using a top-down approach, we will write our Protractor tests first and then build the application. We will follow the same TDD life cycle we have already reviewed, that is, test first, make it run, and make it better.

Test first

The scenario given is in a well-specified format already and fits our Protractor testing template:

describe('',function(){ beforeEach(function(){ }); it('',function(){ }); });

Placing the scenario in the template, we get the following code:

describe('Given I am posting a new comment',function(){ describe('When I push the submit button',function(){ beforeEach(function(){ }); it('Should then add the comment',function(){ }); }); });

Following the 3 A's (Assemble, Act, Assert), we will fit the user scenario in the template.

Assemble

The browser will need to point to the first page of the application. As the base URL has already been defined, we can add the following to the test:

beforeEach(function(){ browser.get('/'); });

Now that the test is prepared, we can move on to the next step, Act.

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at £15.99/month. Cancel anytime

Act

The next thing we need to do, based on the user specification, is add an actual comment. The easiest thing is to just put some text into an input box. The test for this, again without knowing what the element will be called or what it will do, is to write it based on what it should be.

Here is the code to add the comment section for the application:

beforeEach(function(){ ... var commentInput = $('input'); commentInput.sendKeys('a comment'); });

The last assemble component, as part of the test, is to push the Submit button. This can be easily achieved in Protractor using the click function. Even though we don't have a page yet, or any attributes, we can still name the button that will be created:

beforeEach(function(){ ... var submitButton = element.all(by.buttonText('Submit')).click(); });

Finally, we will hit the crux of the test and assert the users' expectations.

Assert

The user expectation is that once the Submit button is clicked, the comment is added. This is a little ambiguous, but we can determine that somehow the user needs to get notified that the comment was added. The simplest approach is to display all comments on the page. In AngularJS, the easiest way to do this is to add an ng-repeat object that displays all comments. To test this, we will add the following:

it('Should then add the comment',function(){ var comments = element(by.repeater('comment in comments')).first(); expect(comment.getText()).toBe('a comment'); });

Now, the test has been constructed and meets the user specifications. It is small and concise. Here is the completed test:

describe('Given I am posting a new comment',function(){ describe('When I push the submit button',function(){ beforeEach(function(){ //Assemble browser.get('/'); var commentInput = $('input'); commentInput.sendKeys('a comment'); //Act //Act var submitButton = element.all(by.buttonText('Submit')). click(); }); //Assert it('Should then add the comment',function(){ var comments = element(by.repeater('comment in comments')).first(); expect(comment.getText()).toBe('a comment'); }); }); });

Make it run

Based on the errors and output of the test, we will build our application as we go.

  1. The first step to make the code run is to identify the errors. Before starting off the site, let's create a bare bones index.html page:

    <!DOCTYPE html> <html> <head> <title></title> </head> <body> </body> </html>

    Already anticipating the first error, add AngularJS as a dependency in the page:

    <script type='text/javascript' src='bower_components/angular/angular.js'></script> </body>

  2. Now, starting the web server using the following command:

    $ ./node_modules/http-server/bin/http-server -p 8080

  3. Run Protractor to see the first error:

    $ ./node_modules/.bin/protractor chromeOnlyConf.js

  4. Our first error states that AngularJS could not be found:

    Error: Angular could not be found on the page http://localhost:8080/ :
    angular never provided resumeBootstrap

    This is because we need to add ng-app to the page. Let's create a module and add it to the page.

The complete HTML page now looks as follows:

<!DOCTYPE html> <html> <head> <title></title> </head> <body> <script src="bower_components/angular/angular.js"></script> </body> </html>

Adding the module

The first component that you need to define is an ng-app attribute in the index.html page. Use the following steps to add the module:

  1. Add ng-app as an attribute to the body tag:

    <body ng-app='comments'>

  2. Now, we can go ahead and create a simple comments module and add it to a file named comments.js:

    angular.module('comments',[]);

  3. Add this new file to index.html:

    <script src='app/commentController.js'></script>

  4. Rerun the Protractor test to get the next error:

    $ Error: No element found using locator: By.cssSelector('input')

The test couldn't find our input locator. You need to add the input to the page.

Adding the input

Here are the steps you need to follow to add the input to the page:

  1. All we have to do is add a simple input tag to the page:

    <input type='text' />

  2. Run the test and see what the new output is:

    $ Error: No element found using locator: by.buttonText('Submit')

  3. Just like the previous error, we need to add a button with the appropriate text:

    <button type='button'>Submit</button>

  4. Run the test again and the next error is as follows:

    $ Error: No element found using locator: by.repeater('comment in comments')

This appears to be from our expectation that a submitted comment will be available on the page through ng-repeat. To add this to the page, we will use a controller to provide the data for the repeater.

Controller

As we mentioned in the preceding section, the error is because there is no comments object. In order to add the comments object, we will use a controller that has an array of comments in its scope. Use the following steps to add a comments object in the scope:

  1. Create a new file in the app directory named commentController.js:

    angular.module('comments') .controller('CommentController',['$scope', function($scope){ $scope.comments = []; }])

  2. Add it to the web page after the AngularJS script:

    <script src='app/commentController.js'></script>

  3. Now, we can add commentController to the page:

    <div ng-controller='CommentController'>

  4. Then, add a repeater for the comments as follows:

    <ul ng-repeat='comment in comments'> <li>{{comment}}</li> </ul>

  5. Run the Protractor test and let's see where we are:

    $ Error: No element found using locator: by.repeater('comment in comments')

    Hmmm! We get the same error.

  6. Let's look at the actual page that gets rendered and see what's going on. In Chrome, go to http://localhost:8080 and open the console to see the page source (Ctrl + Shift + J). You should see something like what's shown in the following screenshot:

    first-step-img-2

    Notice that the repeater and controller are both there; however, the repeater is commented out. Since Protractor is only looking at visible elements, it won't find the repeater.

  7. Great! Now we know why the repeater isn't visible, but we have to fix it. In order for a comment to show up, it has to exist on the controller's comments scope. The smallest change is to add something to the array to initialize it as shown in the following code snippet:

    .controller('CommentController',['$scope',function($scope){ $scope.comments = ['anything']; }]);

  8. Now run the test and we get the following:

    $ Expected 'anything' to be 'a comment'.

Wow! We finally tackled all the errors and reached the expectation. Here is what the HTML code looks like so far:

<!DOCTYPE html> <html> <head> <title></title> </head> <body ng-app='comments'> <div ng-controller='CommentController'> <input type='text' /> <ul> <li ng-repeat='comment in comments'> {{comment.value}} </li> </ul> </div> <script src='bower_components/angular/angular.js'></script> <script src='app/comments.js'></script> <script src='app/commentController.js'></script> </body> </html>

The comments.js module looks as follows:

angular.module('comments',[]);

Here is commentController.js:

angular.module('comments') .controller('CommentController',['$scope', function($scope){ $scope.comments = []; }])

Make it pass

With TDD, you want to add the smallest possible component to make the test pass. Since we have hardcoded, for the moment, the comments to be initialized to anything, change anything to a comment; this should make the test pass. Here is the code to make the test pass:

angular.module('comments') .controller('CommentController',['$scope', function($scope){ $scope.comments = ['a comment']; }]); …

Run the test, and bam! We get a passing test:

$ 1 test, 1 assertion, 0 failures

Wait a second! We still have some work to do. Although we got the test to pass, it is not done. We added some hacks just to get the test passing. The two things that stand out are:

  • Clicking on the Submit button, which really doesn't have any functionality
  • Hardcoded initialization of the expected value for a comment

The preceding changes are critical steps we need to perform before we move forward. They will be tackled in the next phase of the TDD life cycle, that is, make it better (refactor).

Summary

In this article, we walked through the TDD techniques of using Protractor and Karma together. As the application was developed, you were able to see where, why, and how to apply the TDD testing tools and techniques. With the bottom-up approach, the specifications are used to build unit tests and then build the UI layer on top of that. In this article, a top-down approach was shown to focus on the user's behavior. The top-down approach tests the UI and then filters the development through the other layers.

Resources for Article:


Further resources on this subject: