Unit testing is a very important part of the software development process, especially as your applications become more and more complex. Making sure your code is robust and adaptable is an important aspect of being professional. This is no less true with your AngularJS application.
If you are reading this, you are most likely already sold on unit testing but here’s some benefits and reasons for writing and maintaining your unit tests:
Here at Arroyo Labs we use the latest version of Angular CLI a lot. It’s a great way to start a project very quickly provides a great start for your application. It also gives you some boilerplate unit tests for each component and service you create using the CLI.
While we have explained Jasmine and KarmaJS in past blog articles, here is a quick overview explaining how these frameworks work together:
To create a unit test, you need to include a consistent environment in which to run your code under test and we need to control the required classes and variables that go into creating this environment. Thanks to the magic of dependency injection, we can mock up some classes used by our classes and code.
Let’s take a look at the ‘top’ portion of a unit test we use in our user-admin project (you can find the whole unit test in our repo):
/**
* Import some basic testing classes and utilities from AngularJS's
* core testing code. This is the minimum required code to create
* a basic unit test.
*/
import {
async,
getTestBed,
TestBed
} from '@angular/core/testing';
/**
* Import some specific test classes used to mock the
* http backend and connection classes
*/
import {
MockBackend,
MockConnection
} from '@angular/http/testing';
/**
* Import the HTTP classes we use in our service
*/
import {
BaseRequestOptions,
Http,
Response,
ResponseOptions,
XHRBackend
} from '@angular/http';
/**
* Finally, we import the required custom classes we use in our service,
* and the service we are testing as well.
*/
import { User } from '../shared/models/user.model';
import { AuthService } from './auth.service';
import { UsersService } from './users.service';
To test a service, we have to understand the underlying concept they represent: isolating logic and code into a re-usable class. This means we have a class we can pass around in our application where we get and manipulate data in expected ways.
Keep in mind, we can really only test publically scoped variables and functions. The unit test itself is really only able to interact with your service in the same way that an application is able to.
So, we have these things to test:
In our UserAdmin project, we have a service called UserService used to interact with and manipulate user data we get from the AJAX backend.
The service itself only has quite a few public methods (it’s a very important class!) so we will focus on just two of the suite’s tests of the same method `getUsers`:
This is a test to make sure we return an empty list of users if the AJAX response does not contain an encoded list of users. This is a negative test, we are making sure that we return an expected response even when the backend returns an error.
it('#getUsers should return an empty observable list when the ajax request is unsuccessful', () => {
// set up the mocked service to return an
// unsuccessful response (no users, 500 status)
// with a helper method
usersBodyData.success = false;
setupConnections(backend, {
body: {
body: usersBodyData // use the previously init'd var for consistent responses
},
status: 500
});
// set up our subscriptions to test results
// when we actually get a result returned
service.users$.subscribe((res) => {
if(res) {
expect(res).toBeTruthy();
expect(res.length).toEqual(0);
}
});
service.total$.subscribe((res) => {
if(res) {
expect(res).toBeTruthy();
expect(res).toEqual(0);
}
});
// make the actual request!
let res = service.getUsers();
});
This test makes sure that we get an observable list of users if the AJAX response is successful. To make sure this happens, we mock the response as though we have a JSON encoded list of users.
it('#getUsers should return an observable list of users and result total when the ajax request is successful', () => {
// set up the mocked service to return an
// unsuccessful response (users, 200 status)
// with a helper method
setupConnections(backend, {
body: {
body: usersBodyData
},
status: 200
});
// set up our subscriptions to test results
// when we actually get a result returned
service.users$.subscribe((res) => {
if(res) {
expect(res).toBeTruthy();
expect(res.length).toEqual(2);
}
});
service.total$.subscribe((res) => {
if(res) {
expect(res).toBeTruthy();
expect(res).toEqual(2);
}
});
// make the actual request, with some params to pass via query string
// we will explore this in a future post
let res = service.getUsers(42, 42, 'id', 'desc');
});
A component, like a service, is also a re-usable piece of code but it also has a defined lifecycle from its parent class and a frontend element via it’s template and optional CSS.
So, here is what we have to test:
Like we have noted above when discussing testing a service, you can really only test things that have been scoped as public by the component itself. This does include rendered DOM elements and you should test that these elements appear and render as expected since these are made public by the component itself.
it('should create', () => {
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
component.ngOnInit();
component.users = [];
component.total = 0;
// do we have a component at all?
expect(component).toBeTruthy();
// create new user button
expect(compiled.querySelector('.btn-info')).toBeTruthy();
// do we have a table
expect(compiled.querySelector('table')).toBeTruthy();
expect(compiled.querySelectorAll('tr').length).toBe(2);
});
it('should display list of users', () => {
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
setupConnections(backend, {
body: {
body: bodyData
},
status: 200
});
component.ngOnInit();
// create new user button
expect(compiled.querySelector('.btn-info')).toBeTruthy();
fixture.detectChanges();
// do we have a table with users?
fixture.detectChanges();
expect(compiled.querySelectorAll('tr').length).toBe(21);
// do we see the expected page count
expect(component.getPageCount()).toBe(2);
});
Unit testing is a not a oft-celebrated portion of the software development cycle, but it is an important part of the process. Once you have good tests in place, you can pivot direction and update your code with an increased sense of security.
Tags: angular2, jasmine, js, karmajs, types, unittesting
Categories: Miscellaneous, TypeScript
Lets talk!
Join our mailing list, we promise not to spam.