Angular 2.0 is a different beast than Angular 1.x, due to factors such as the new module system, ES2015 and TypeScript language enhancements, and an entirely new framework and API set. If you’ve tested services in Angular 1.x, you’re probably used to doing something like this:
describe('service under test', function() { var myService = null; beforeEach(module('myModule')); beforeEach(inject(_myService_) { myService = _myService_; }); it('should do something cool', function() { expect(myService.doSomething('cool')).toBe(true); }); })
All smart aleckyness aside, the Angular testing module provided a module
function, and an inject
function, and you could simply inject your module into the test quite easily.
Angular 2 is Different
Concept-wise, we still have services in Angular 2.0. But we wire them a bit differently, at least syntactically. We can still purely unit-test a block of code in either framework by creating a new instance of the service, and mocking all collaborators, but if we want to stand up a service in Angular 2.0, we have to be mindful of the changes in:
- How to bootstrap a test bed
- How to inject a service into the test bed
I’ll be using Karma (with the configuration generated by angular-cli
) to stand up our test specification, and I’ll show you how to both bootstrap and inject your service for testing.
An Angular 2.0 service
Angular 2.0 uses TypeScript, a language that provides data type support and annotations (among other things) on top of ES2015. Developers will typically write their code in TypeScript, and it will be transpiled down to ES5 in most cases, so it can run in a typical browser.
Let’s look at a typical Angular 2.0 service, and how we inject it into an Angular Component. This service, which I’ve provided in our sample repository, will translate Markdown to HTML using the marked library.
import * as marked from 'marked'; import {Injectable} from '@angular/core'; @Injectable() export class MarkdownService { // markdown object is not typescript toHtml(text: string) { return marked.parse(text); } }
This service just wraps the calls to the Marked
library, so we can replace it in future with something else if required. It’s ALWAYS a good idea to wrap libraries like this, as the JavaScript community moves faster than the Road Runner being chased by the Coyote.
You’ll note a few things here. First, external files are loaded dynamically using the ES2015 module loader syntax with import
. You’ll also notice there are several syntaxes.
This one asks for all individual items in the marked
module, and collects them as an object called marked
:
import * as marked from 'marked';
The next example pulls a single dependency out of @angular/core
, one of the main Angular 2.0 libraries, named Injectable
:
import {Injectable} from '@angular/core';
Now, we use the second object as an annotation – something that decorates the service and makes it available as a service for code that wants to consume it. This isn’t always needed, but I have it here for clarity. And the rest of the class definition is basically the new way to create a JavaScript class, TypeScript style:
@Injectable() export class MarkdownService { // markdown object is not typescript toHtml(text: string) { return marked.parse(text); } }
The export
keyword exposes this MarkdownService
class to anything that wants to load it (we’ll see how in a minute, but assume it’s via another import
statement…) The toHtml
function is written in the ES2015 method style, which eschews using the function
keyword for functions that are members of a class definition.
Beyond that, it’s just a class. There is nothing wrong with creating an instance and calling the method in it, if we really wanted to, just like MarkdownService
is a constructor function (which, basically, it will turn into):
let service = new MarkdownService(); // will returnheading 2
let html = service.toHtml('## heading 2');
Did I mention the let
keyword? No, but if you are not comfortable with any of this, take a bit of time and read through a TypeScript or ES2015/ES6 tutorial – I’m going to use a fair amount of that syntax. Suffice it to say that JavaScript is growing more functional and more practical, but the syntax is going to hurt at first. The let
keyword is a block-scoped variable definition keyword, similar to var
except that var
only declares variables at a function scope or higher.
Where were we? Oh, right – testing!
So now we have our service, and it’s time to test it. In order to do the testing properly, we need a specification that boots a test bed, which is a partial application, and we inform the test bed of our service. Then we’ll inject it and test it. Here is the entire Jasmine spec:
import {MarkdownService} from './markdown-service'; import { TestBed, inject } from '@angular/core/testing'; describe('Markdown transformer service', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ MarkdownService ] }); }); it('Should translate markdown to HTML!', inject([MarkdownService], (markdownService) => { expect(markdownService).toBeDefined(); expect(markdownService.toHtml('hi')) .toContain('hi
'); })); });
We’ve already addressed the import
keyword. In this case, we are importing two features from the @angular/core/testing
library, TestBed
and inject
, which perform similar functions to their counterparts in Angular 1, angular.mock.module
and angular.mock.inject
The main differences here are around the arrow functions and new injection syntaxes. An arrow function is shorthand for an anonymous inner function, and it also helps preserve the this
context variable. The injector function, inject
, takes an array of injectable services by their class names, and then a function that injects each one as a parameter. To unpack it further:
inject(/* array of injectable definitions: */ [Foo, Bar], /* arrow fn with injectable instances */ (foo, bar) => { });
To be sure, you can actually provide types to the arrow function too, like this:
... (foo: Foo, bar: Bar) => { ...
We’ll see why that is helpful a bit later on…
Running our test
This next step assumes you’re using Angular CLI… Wiring the test system on your own is highly discouraged while you’re learning, as is bootstrapping the rest of Angular 2. It’s several orders of magnitude more complex than Angular 1.x, so for now, I’d suggest grabbing a starter or using the CLI to begin your journey.
That said, if we put the test in the same directory as our source file, in our case /src/app/services
, and we named it with the suffix .spec.ts
, Karma will find it and execute it. Here’s how we execute our tests in our terminal, using the CLI:
ng test
This assumes you’ve:
- Installed Angular CLI using
npm install -g angular-cli
- Downloaded my samples repository for testing
- Installed all dependencies by switching to the root of the samples directory and issuing
npm install
If you’ve done that, you should be rewarded with running tests, including this one…
A more sophisticated case – using Http
Next, we’ll look at a simple network request using Angular’s Http
library. We’ve written a service that uses the Http
library to fetch data from a web service. I’m not going to dive too deeply here, but you should be able to glean the pattern.
We’re using the Observable API from Microsoft’s RxJS library, which Angular 2 integrates for streams and results that used to be processed mostly by promises. Let’s take a look:
import {Http, Headers, RequestOptions, Response} from '@angular/http'; import {Injectable} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {BlogEntry} from '../domain/blog-entry'; import 'rxjs/add/operator/map'; @Injectable() export class BlogService { // inject the http variable as a member of // blog service (the private keyword does this) constructor(private http: Http) { } private getOptions(): RequestOptions { let headers: Headers = new Headers(); headers.append('content-type', 'application/json; charset=utf-8'); let opts = new RequestOptions({headers: headers}); opts.headers = headers; return opts; } getBlogs(): Observable<any> { return this.http.get('/server/api/blogs') .map((res: Response) => { return BlogEntry.asBlogEntries(res.json()); }, this.getOptions()); } }
This service uses dependency injection, which is performed by importing the service and then providing it as a constructor argument:
constructor(private http: Http) { }
But what is this code doing? It’s using the Http
service to execute a get
command, which does an HTTP GET
on the provided URL. The data is then returned using an Observable – which delivers each payload of data returned as the parameter of its first callback. For more on Observables, see my earlier article, Angular 2 Observables, Separating Services and Components, which needs to be updated to the Angular 2.0 release.
So our test has to somehow provide an Http implementation, without going to the actual server, that returns expected data. Then our subscription to the observable (the area where we call map()
) needs to receive a JSON-compatible payload, which contains the blog articles we want to return to our calling component.
Setting up the Test with a Mock Backend
As in Angular 1, Angular 2 delegates http requests to a backend service, which can be swapped for testing purposes. The normal backend API is the XHRBackend
class, in @angular/http
. That class actually communicates with the outside world. But as in Angular 1, we can swap that with a fake backend, the class MockBackend
in the @angular/http/testing
package.
Let’s set up our test with the appropriate TestBed. This time, we’ll wire the test to import the Angular HttpModule
, which wires up all services required to use HTTP. But then we’ll tell the injector to use the mock backend instead of the normal one by replacing its class.
describe('Blog Service', () => { beforeEach(async(() => { TestBed.configureTestingModule({ providers: [ BlogService, MockBackend, BaseRequestOptions, { provide: Http, deps: [MockBackend, BaseRequestOptions], useFactory: (backend: XHRBackend, defaultOptions: BaseRequestOptions) => { return new Http(backend, defaultOptions); } } ], imports: [ FormsModule, HttpModule ] }); TestBed.compileComponents(); })); // tests here });
For this example, I’ve skipped showing you the imports – please review the class itself online at blog.service.spec.ts.
But the main thing we’re doing is mounting the HttpModule
, providing the BlogService class to the injector, and providing what looks like the XHRBackend
service, but, in fact, is the MockBackend
, which we can use to assert expectations and feed fake results to the caller.
Work At Chariot
If you value continual learning, a culture of flexibility and trust, and being surrounded by colleagues who are curious and love to share what they’re learning (with articles like this one, for example!) we encourage you to join our team. Many positions are remote — browse open positions, benefits, and learn more about our interview process below.
Writing in-container tests
Our first method to test is getBlogs()
, which uses the Http
service to fetch a payload of blog entries. It requires no arguments, and returns a subsription to a payload which is delivered as an array of BlogEntry
objects.
The challenge with async tests
Before we look at our service and test methods, keep in mind that this is a test against an asynchronous method that returns results in an Observable response. If you ignore the asynchrony of the process, you might end up with what appears to be a passing test because the test call completes before Angular returns the response. The symptom is an error message in the test output, but the test reports as passed.
We need a way to allow Jasmine to wait for an asynchronous task to complete. There are several approaches for this. One is that you can inject a callback method name into the Jasmine test function body, like this:
it('should run a test that finishes eventually', done => { // kick off an asynchronous call in the background setTimeout(() => { console.log('now we are done'); done(); }, 500); })
In the above test we’d normally complete the test early. But with the done
callback, we are able to delay the test completion until Jasmine recieves the notification that the test is complete. If the done
method is not called, the test fails after the pre-set timeout time.
Another approach is preferred by Angular developers. It’s using the async
function wrapper around your test implementation, which only completes a test after the asynchronous tasks spawned by the method complete. In essence, it waits for the queue of microtasks to drain completely and then it marks the test as complete:
it('should run a test that finishes eventually in Angular', async(() => { myService.doSomething().then( (result) => { expect(result).toBeDefined(); ... } ); }));
The benefit of this approach is that you don’t have to remember to call a done
method; the test is auto-sensing for when all code running behind the scenes is completed.
Note- if you forget to add the arrow function INSIDE of the async call, you will be greeted with a very cryptic error message – apparently ES2015 or 2016 has an async keyword, and it conflicts with our strategy. The error message explains that we should be transpliling to ES2015 rather than ES5. Ignore that message and go from async() => {
to async(() =>{
. That will save you a few hours of frustration.
Using the async
strategy with getBlogs
Now, let’s look at the latter approach, actually applied to our test method for getBlogs()
. We’ll start with the test declaration and pulling the blog service from the TestBed:
it('should get blogs', async(() => { let blogService: BlogService = getTestBed().get(BlogService);
Next, we’ll ask our fake backend (mockBackend
) to respond to an incoming HTTP GET
request by returning a payload of an array of blog entries. For the sake of brevity we only return a single element in the array:
mockBackend.connections.subscribe( (connection: MockConnection) => { connection.mockRespond(new Response( new ResponseOptions({ body: [ { id: 26, contentRendered: 'Hi there
', contentMarkdown: '*Hi there*' }] } ))); });
Finally, we exercise our API and we check the results. To prove it fails, change one of the parameters to a failing value and make sure the expectations fail:
blogService.getBlogs().subscribe( (data) => { expect(data.length).toBe(1); expect(data[0].id).toBe(26); expect(data[0].contentMarkdown).toBe('*Hi there*'); }); }));
The Http service – how our service processes the data
The service method uses the Http.get
method to fetch from a web service API. This call returns an RxJS Observable
response, which contains data about the response headers, the payload, status codes and the like. RxJS is a functional programming API, which means we can chain methods onto the observed payload and pull out only what we want to return. In this case, we’re going to parse them from JSON into an array of typed TypeScript BlogEntry
objects. Here’s the method:
return this.http.get('/server/api/blogs') .map((res: Response) => { return BlogEntry.asBlogEntries(res.json()); }, this.getOptions());
The map
method, which is mounted with a special import, executes each time an entire GET payload returns (in our case, since we did a single GET, it’s one time, and it contains all of the results). The Response object in Http contains a json()
method, which parses the data into a JavaScript literal (an untyped object with keys and values).
We then take that literal object and pass it into a static method, BlogEntry.asBlogEntries
, which looks like this:
static asBlogEntries(jsonArray: Array<Object>>) { return jsonArray.map((datum) => BlogEntry.asBlogEntry(datum)); }
Again, being a functional API, we just walk the array of data, and map each entry into a single BlogEntry object. We’ve done that using another static method, BlogEntry.asBlogEntry
:
static asBlogEntry(json: any) { let id: number = json['id'], title: string = json['title'], contentRendered: string = json['contentRendered'], contentMarkdown: string = json['contentMarkdown']; return new BlogEntry(title, contentRendered, contentMarkdown, id); }
In our model, the BlogEntry domain object can hydrate itself from a JSON object. You might want to go with an adapter or serializer/deserializer approach where the JSON to typed-object parsing is done in a seperate API. The point is, with a typed programming language you can start thinking about proper parsing and assembly of objects.
You can see the whole BlogEntry
class in our repository.
A few more cases – single fetch, create, update, delete
Here, then, are the methods and tests for getting a single blog entry, creating a new entry, updating an existing entry, and deleting an entry.
Getting a single blog entry
This case is very similar to the bulk GET call, except that we are interested in vetting the parameter is passed within the URL in a RESTful way. The method in the service:
getBlog(id: number): any { return this.http.get('/server/api/blogs/' + id) .map((res: Response) => { return BlogEntry.asBlogEntry(res.json()); }); }
And now the test. Note the use of the expectation in the mock request processing – you want to verify that the getBlog(id)
call passed the correct ID, and that the data is also returned properly. We’re making a simple jump here that the returned payload has the same ID. Technically the test only cares that what you mock is what gets returned, not that the payload ID is the same as the URI ID… But it’s nice to write sane code too!
it('should fetch a single blog entry by a key', async(() => { let blogService: BlogService = getTestBed().get(BlogService); mockBackend.connections.subscribe( (connection: MockConnection) => { // make sure the URL is correct expect(connection.request.url).toMatch(/\/server\/api\/blogs\/3/); connection.mockRespond( new Response( new ResponseOptions({ body: { id: 3, contentRendered: 'Demo
', contentMarkdown: '*Demo*' } })) ); } ); blogService.getBlog(3).subscribe( (blogEntry) => { expect(blogEntry.id).toBe(3); expect(blogEntry.contentMarkdown).toBe('*Demo*'); expect(blogEntry.contentRendered).toBe('Demo
'); } ); }));
Again, nothing majorly different here, except that we check that the path is correct, and that the payload is returned as a single blog entry.
Handling inserts and updates
We’ve made it so that any blog entry passed into the saveBlog(blogEntry)
method will call POST or PUT depending on whether it has an ID. Our RESTful server wants a PUT for an update, and a POST for a brand-new blog entry. This is typical REST. Here is the method in its entirety:
saveBlog(blog: BlogEntry): Observable{ console.log('saving', blog.json()); if (blog.id) { return this.http.put('/server/api/blogs/' + blog.id, blog.json(), this.getOptions()); } else { return this.http.post('/server/api/blogs', blog.json(), this.getOptions()); } }
To test this, we have to simulate two conditions, hence we have two tests. In the first case, we pass it a BlogEntry
with null as the ID to simulate a brand new Blog Entry:
it('should insert new blog entries', async(() => { let blogService: BlogService = getTestBed().get(BlogService); mockBackend.connections.subscribe((connection: MockConnection) => { // is it the correct REST type for an insert? (POST) expect(connection.request.method).toBe(RequestMethod.Post); connection.mockRespond(new Response(new ResponseOptions({status: 201}))); }); let data: BlogEntry = new BlogEntry('Blog Entry', 'Hi
', '*Hi*', null); blogService.saveBlog(data).subscribe( (successResult) => { expect(successResult).toBeDefined(); expect(successResult.status).toBe(201); }); }));
Note the expectations of the 201/CREATED
response for an insert, and the fact that we recieved a POST request.
The next test adds the key, and should get a 204 response, the standard for a PUT request, 204/NO_CONTENT
. This is a bit arbitrary as my original test simulated a 200 response rather than a 204, and as long as the mock response and the expectation are in agreement, they might BOTH be incorrect.
it('should save updates to an existing blog entry', async(() => { let blogService: BlogService = getTestBed().get(BlogService); mockBackend.connections.subscribe(connection => { // is it the correct REST type for an update? (PUT) expect(connection.request.method).toBe(RequestMethod.Put); connection.mockRespond(new Response(new ResponseOptions({status: 204}))); }); let data: BlogEntry = new BlogEntry('Blog Entry', 'Hi
', '*Hi*', 10); blogService.saveBlog(data).subscribe( (successResult) => { expect(successResult).toBeDefined(); expect(successResult.status).toBe(204); }); }));
We have one final sample to test – the delete…
Testing a RESTful delete
A delete is nothing more than a request to a RESTful endpoint with the DELETE
HTTP method. Here is how we handle it in our service:
deleteBlogEntry(id: number): Observable{ return this.http.delete('/server/api/blogs/' + id); }
We just pass the buck to the RESTful server. To test it, we have to issue the request and check whether it was a DELETE, and what the response status code was:
it('should delete an existing blog entry', async(() => { let blogService: BlogService = getTestBed().get(BlogService); mockBackend.connections.subscribe(connection => { expect(connection.request.method).toBe(RequestMethod.Delete); // see options for proper delete returns in real life // here: http://stackoverflow.com/questions/6581285/is-a-response-body-allowed-for-a-http-delete-request connection.mockRespond(new Response(new ResponseOptions({status: 204}))); }); let data: BlogEntry = new BlogEntry('Blog Entry', 'Hi
', '*Hi*', 10); blogService.saveBlog(data).subscribe( (successResult) => { expect(successResult).toBeDefined(); expect(successResult.status).toBe(204); }); }));
Wrap-up
As you can see, the MockBackend
and its related classes, MockRequest
, MockResponse
, MockConnection
and others, are very useful for simulating backend systems. In some ways they appear to be more feature-full than their Angular 1.x predecessors. You’re now armed with techniques for simulating various backend APIs when testing your services.
If you find this useful, and your company is considering moving to Angular 2, please consider using us for your onsite training needs. Our course, Angular 2 Workshop Fundamentals and Architecture, is a three-day, onsite workshop. It covers everything from TypeScript and ES2015 fundamentals, to components, services, pipes, routing, RxJS Observables and testing. We hope to see you there!
The sample code is located on this GitHub repository, github.com/krimple/angular2-unittest-samples-release