Test Driven Development (TDD) with Angular 5, Jasmine and Karma using @angular/cli

In my previous posts, I’ve talked about unit testing AngularJS controllers. The idea was just to unit test the code we have already written. But that’s not the ideal way we should write new functional code, isn’t it?

It should be other way round. Ideally, the development of the code/component should be test driven. What does that really mean is, ideally, we should start writing the unit test case first, let the test fail, write the code as per test case’s expectations and then let the test pass.

There is much content available online for the people who know AngularJS and want to learn Angular 2+. However, not quite good posts are available to learn TDD approach for this technology. This post talk about how to do so. Keep in mind that there is a slight paradigm shift needed for TDD so its okay if you think you are not able to digest the steps at the very first attempt, after 2 – 3 try, I’m sure you are going to start liking this. So let’s jump in! 🙂

Below are the steps to follow while doing TDD (reference: wikipedia)

  1. Add a test
  2. Run all tests and see if the new test fails
  3. Write the code
  4. Run tests
  5. Refactor code

We are going to follow the same for Angular using amazing command line utilities provided by @angular/cli.

We are going to develop an angular 5 component which does addition/subtraction/multiplication. We will be having two properties for two operands and three methods to perform the operations respectively. For each operation, we will go through three basic different scenarios which fits most of the day to day code we write.

Prerequisites: You must be having below tools installed on your machine.

  1. NodeJS
  2. npm
  3. @angular/cli: version 1.7
  4. VSCode (not mandatory,  but the best if you use this)
  5. Basic understanding of Unit Testing in Angular with Jasmine would help.

So what are we waiting for? Let’s start!!!

Create new Angular application to begin. From your command prompt, type below command for that.

ng new ng5-tdd

This command will take awhile and create the basic application structure for you. Once this is done, open it using VSCode and start VSCode command prompt with Ctrl+`.

Create a new component as per below code snippet.

D:\ng5-tdd>ng g c calculator 
                           //g - generate, c - component
 create src/app/calculator/calculator.component.html (29 bytes)
 create src/app/calculator/calculator.component.spec.ts (656 bytes)
 create src/app/calculator/calculator.component.ts (285 bytes)
 create src/app/calculator/calculator.component.css (0 bytes)
 update src/app/app.module.ts (414 bytes)

Many files has got created. Now open calculator.component.spec.ts. Keep in mind that we are doing TDD here! 🙂

Now, let’s add below test case in this file.

 


it('should do addition', () => {
component.param1 = 5;
component.param2 = 7;
component.add();
expect(component.result).toBe(12);
});

After pasting the code your editor should have started screaming by now. 🙂 Yeah. It must. As we haven’t added the code yet. So go ahead and copy-paste-save below code in your calculator.component.ts.


export class CalculatorComponent implements OnInit {
param1: number;
param2: number;
result: number;
constructor() { }
add() {
this.result = this.param1 + this.param2;
}
ngOnInit() {
}
}

Here, as you see, we have added the necessary properties and method for the addition operation. This was just a simple scenario – straight forward. In real life, the story is not that simple, isn’t it?

Usually, such functionalities (in our case, business logic) is either stays in Services, or it stays at sever.

So let’s go ahead and create a service to do some action.

D:\ng5-tdd>ng g service calculator/math
 create src/app/calculator/math.service.spec.ts (362 bytes)
 create src/app/calculator/math.service.ts (110 bytes)


// update TestBed configuration to use MathService as one of the providers
// in the DESCRIBE section
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CalculatorComponent ],
providers: [MathService] // <=== add this line
})
.compileComponents();
}));
// adding another test besides IT code block
it('should call mathService to do subtraction', () => {
const mathSvcInstance = TestBed.get(MathService);
// Remember – what MathService SHOULD return, that's none of you business right now.
// you are testing calculator component – Separation of Concerns
spyOn(mathSvcInstance, 'subtract').and.returnValue(500);
component.param1 = 5;
component.param2 = 7;
component.subtract();
expect(mathSvcInstance.subtract).toHaveBeenCalledWith(component.param1, component.param2);
expect(component.result).toBe(500);
});

Again, you must be getting errors, so go ahead and update the component code to make this test pass.


constructor(private mathSvc: MathService) { }
subtract() {
this.result = this.mathSvc.subtract(this.param1, this.param2);
}

Here we have injected MathService as a dependency. You should have noticed that in the test case, 5 * 7 doesn’t equal to 500. But still we made the test pass. Why? The reason is, the component is simply assigning the return value to the result property. So in the test, we just need to make sure that the value returned by the service is getting assigned at the appropriate place. That’s it!

Now many times service gives asynchronous result. We are going to test that scenario next. Make changes as per below files suggest.


import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class MathService {
constructor() { }
subtract(a: number, b: number): number {
return a + b;
}
multiply(a: number, b: number): Observable<any> {
// you'll do this while doing TDD got this service
// return this.http.post('you-multiplication-api-url', {a: a, b: b});
// as of now, be happy with TDD for component, and return below 🙂
return Observable.of(50);
}
}


import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CalculatorComponent } from './calculator.component';
import { MathService } from './math.service';
import { Observable } from 'rxjs/Rx';
class mockMathService{
subtract(a: number, b: number) { return 0; }
multiply(a: number, b: number) { return Observable.of(0); }
}
describe('CalculatorComponent', () => {
let component: CalculatorComponent;
let fixture: ComponentFixture<CalculatorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CalculatorComponent ],
providers: [
{ provide: MathService, useClass: mockMathService }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CalculatorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should do addition', () => {
component.param1 = 5;
component.param2 = 7;
component.add();
expect(component.result).toBe(12);
});
it('should call mathService to do subtraction', () => {
const mathSvcInstance = TestBed.get(MathService);
spyOn(mathSvcInstance, 'subtract').and.returnValue(500);
component.param1 = 5;
component.param2 = 7;
component.subtract();
expect(mathSvcInstance.subtract).toHaveBeenCalledWith(component.param1, component.param2);
expect(component.result).toBe(500);
});
it('should call mathService to do multiplication', () => {
const mathSvcInstance = TestBed.get(MathService);
spyOn(mathSvcInstance, 'multiply').and.returnValue(Observable.of(35));
component.param1 = 5;
component.param2 = 7;
component.multiply();
fixture.detectChanges();
expect(mathSvcInstance.multiply).toHaveBeenCalledWith(component.param1, component.param2);
expect(component.result).toBe(35);
});
});


import { Component, OnInit } from '@angular/core';
import { MathService } from './math.service';
@Component({
selector: 'app-calculator',
templateUrl: './calculator.component.html',
styleUrls: ['./calculator.component.css']
})
export class CalculatorComponent implements OnInit {
param1: number;
param2: number;
result: number;
constructor(private mathSvc: MathService) { }
add() {
this.result = this.param1 + this.param2;
}
subtract() {
this.result = this.mathSvc.subtract(this.param1, this.param2);
}
multiply() {
this.mathSvc.multiply(this.param1, this.param2)
.subscribe(data => this.result = data);
}
ngOnInit() {
}
}

So after updating the files, you’ll see that all of your tests pass and you have done async programming with TDD.

Conclusion

TDD is a slight paradigm shift to start with. However, you’ll be used to it once you start doing it. And in the agile software development, it is the ideal way a developer should do the implementation.

dexter TDD

How do you like this post? Any thoughts, something you liked the most or anything you want to see improved? Feel free to share your thoughts in the comments below! I’ll be glad to know! See ya next time. Till then, happy TDDing 🙂

Using Karma and Grunt while testing AngularJS code with Jasmine

In my previous post, we’ve seen how to write UI test cases for AngularJS with Jasmine library. I have seen many developers learn this art of improving the quality of UI code by writing test cases, but still very few of them know the tools they are using in this process i.e. grunt, karma (Honestly, I was one of them 🙂 ).

So in this post, we are going to see how do we utilise npm package Karma and task runner Grunt in testing AngularJS code with Jasmine library.

In my previous post, we’ve took a reference of a plunk I  have created.
In the same fashion, here we are going to use a GitHub repository – SimplifyingAngularUTC I have created.

Download it in your local machine as a zip file, extract it and open the folder in your command prompt. Also open the folder in your favourite editor (Sublime text, VSCode, VisualStudio or even in notepad – it doesn’t matter).

You need to have Node.js and npm installed on your local machine to run the scripts.

As you might have noticed, the repository is almost same as per the plunk we have seen in the previous post (point 1, 2 & 3 below).

  1. scripts folder contains AngularJS module definition, controllers and services’ code
  2. vendors folder contains dependent angularjs and angular-mock libraries
  3. tests folder contains our test cases code
  4. package.json
  5. Gruntfile.js
  6. karma.config.js

The other json and js files are the new arrivals, which are actually used when we use Node Package Manager. Let’s look at them one by one.


module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
karma: {
unit: {
configFile: 'karma.conf.js'
}
}
});
grunt.loadNpmTasks('grunt-karma');
grunt.registerTask('default', ['karma']);
};

view raw

Gruntfile.js

hosted with ❤ by GitHub


// Karma configuration
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'vendors/angular.js',
'vendors/angular-mocks.js',
'scripts/*.js',
'tests/*.js'
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
//browsers: ['Chrome', 'PhantomJS', 'Safari', 'Firefox'],
browsers: ['Chrome'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false
});
};

view raw

karma.conf.js

hosted with ❤ by GitHub


{
"name": "SimplifyingAngularUTC",
"dependencies": {
"karma": "^0.12.24"
},
"devDependencies": {
"grunt": "^0.4.5",
"grunt-karma": "^0.10.1",
"karma-chrome-launcher": "^0.1.5",
"karma-firefox-launcher": "^0.1.4",
"karma-jasmine": "^0.1.5",
"karma-phantomjs-launcher": "^0.1.4",
"karma-safari-launcher": "^0.1.1"
}
}

view raw

package.json

hosted with ❤ by GitHub

 

  1. package.json
    • As you can see, this file contains the list of all npm packages are to be installed for the current project.
    • grunt is the JavaScript task runner we are going to use, karma & grunt-karma packages are the dependency used to run Jasmine test cases. Other packages are used to execute the test cases against browsers (remember that we are going to run UI test cases!).
  1. Gruntfile.js
    • This file is used to configure and register the grunt tasks.
    • In the initConfig call, we are telling grunt to load packages from package.json
    • We are also informing the grunt which file to use to configure karma, which in our example, is karma.conf.js
    • In the registerTask call, we have registered karma as the default task. So when we’ll execute just ‘grunt’ command, the test cases will be executed.
  2. karma.conf.js
    • In this config file, we tell the karma which files are going to be used actually to do the testing.
    • As you have seen, angularjs, angular-mocks, our source code of the application and testing related files are mentioned in ‘files‘ parameter as an array.
    • We can also mention which file we want to exclude as ‘exclude‘ parameter.
    • You can even specify on which ‘port‘ you’d like to run the test cases and also against which browser.
    • Note: once you are done installing the packages, you can also generate the karma config file yourself by karma init my.conf.js command. Have a look at the screenshot below. The command will guide you to create it. Easy it is, isn’t it!
    • creating karma config file1

In our plunk example, we were seeing the result in the browser itself. That’s why we had index.html file including angular, mock and jasmine files; and browsing index.html file we are able to see the result (now you’ll relate how karma config is helping us to do the same here).

Now, follow the instructions mentioned in the repository README file and execute the commands in the command prompt.

  1. npm install (installs the npm packages specified in package.json)
  2. npm install -g grunt-cli (installs grunt cli; -g for globally)
  3. npm install -g karma-cli (installs karma cli; -g for globally)

The above commands install…

  1. the packages mentioned in package.json file
  2. grunt-cli and karma-cli packages (to use the command grunt directly from the command prompt)

Once done, execute command ‘grunt‘. You’ll see the result in the command prompt as below. You might need to terminate the process with ctrl+c after they are executed.

As you can see that all the fifteen test cases are executed.

Running grunt command to run test cases

Now, if you want to execute test cases for just a single file, (for example, if you want to do it just for myMathSvcSpec.js file) go to the spec file, add ‘d‘ in front of the ‘describe‘ as per below, save it, run grunt command and you can see that only those test cases are executed which are there in that spec file. The rest are skipped.

Running grunt command to run test cases-skipping other files

If you want to exclude some files while executing the test cases, you can mention in the karma.conf.js file. For example, if you don’t want test cases of myMathSvcSpec.js to be executed along with the others, you can mention it is karma config file as ‘exclude’ property. Have a look at the screenshot below. As you can see, five test cases of myMathSvcSpec are not executed.

Running grunt command to run test cases-skipping specific file

Dexters-Laboratory-PNG-Pic

Hope you had a good learning for the topic reading this post.

How do you like it? Any suggestion, question, or anything you would to say about this.

Let me know in the comments section below.