Angular2: Final Release Unit Test Migration Guide

About to use Angular2? Need to refactor your codebase? Take a read of this handy guide.

photo of Vadym Kukhtin
Vadym Kukhtin

Frontend Developer

Posted on Nov 09, 2016

I am not a fan of the Angular-way in Angular1, as I think it looks rather strange. Nevertheless, Angular2 gives a much better impression in terms of code structure, code purity, and scalability. While they didn’t reinvent the wheel, its creators have built something interesting and spectacular that stacks up well against modern frameworks (React and Redux, Aurelia, etc.).

The final release of Angular2 came out and β€œsurprised” developers with tons of changes (if you were still using RC Version < 5) that may have helped with regards to development, but forced you to rewrite a bunch of code. I was amazed when I discovered the β€œTesting” section in Angular2 - Quick Start, so I decided to further explore here.

Angular2 has pros and cons already described in several articles and books, so you won’t find contrasting arguments in this post.

However, I hope this article will help developers who suffer when trying to refactor their codebase!

Let’s define some basic App structure:

β€” app β€” app.component.ts β€” app.module.ts β€” main.ts β€” components β€” table.component.ts β€” services β€” post.service.ts β€” models β€” post.model.ts β€” test β€” post.service.mock.ts β€” table.component.spec.ts β€” post.model.spec.ts β€” post.service.spec.ts

From here on I will use TypeScript examples, as I find TypeScript more elegant in this case.

This application will render a table:

app.component – the first, initial component that will be rendered at app initialization:

// Angular
import { Component } from '@angular/core';
// Services
import {PostService} from './app/services/post.service';
import {Post} from './app/models/post.model';
@Component({
    selector: 'app',
    template: `



        `
})
export class AppComponent {
    public isDataLoaded: boolean = false;
    public post: Post;
    constructor(public postService: PostService) {}
    ngOnInit(): void {
         this.postService.getPost().subscribe((post: any) => {
              this.post = new Post(post);
              this.isDataLoaded = true;
         });
    }
}

app.module – Will store all app dependencies. In our case, provide PostService and TableComponent:

import { NgModule }       from '@angular/core';
import { BrowserModule  } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
// Components
import { AppComponent }   from './app/app.component';
import {TableComponent} from './app/components/table/table.component';
// Services
import {PostService} from './app/services/post.service';
@NgModule({
    declarations: [
        AppComponent
        TableComponent
    ],
    imports: [
        BrowserModule,
        HttpModule
    ],
    providers: [
        PostService
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}

main – The app start point where you will bootstrap the app:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule }              from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

table.component – TableComponent, which must be rendered:

// Angular
import {Component, Input} from '@angular/core';
@Component({
    selector: 'table-component',
    template: `


                                  Post Title

Post Author

{{ post.title}}

{{ post.author}}

` }) export class TableComponent { @Input() public post: any; }

post.service – An injectable service that performs API calls:

    import {Injectable} from '@angular/core';
    import {Observable} from 'rxjs/Rx';
    import {Post} from './app/models/post.model';
    import { Http } from '@angular/http';
    @Injectable()
    export class PostService {
        constructor(http: Http) {}
        public getPost(): any {
            // Abstract API, Google, Facebook etc
            return this.http.get(AbstractAPI.url)
                    .map((res: any) => res.json())
        }
    }

post.model – Post Class with JSON structure:

   export class Post {
        public title: number;
        public author: string;

        constructor(post: any) {
            this.title = post.title;
            this.author = post.author;
        }
    }

Our App is now ready and working, but how will we test it?

I am a fan of TDD, so tests are really important for me and should be for you too. I will use Karma and Jasmine for testing and all examples will be based on these.

Main is changed for @angular/testing – {it, describe} are removed from @angular/core/testing. They are now deprecated and will be grabbed from the framework itself (Karma in my case).

Before:

import {setBaseTestProviders} from '@angular/core/testing';
import {
    TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
    TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS
} from '@angular/platform-browser-dynamic/testing';
setBaseTestProviders(
    TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
    TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS
);

After:

import {TestBed} from '@angular/core/testing';
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(
    BrowserDynamicTestingModule,
    platformBrowserDynamicTesting()
);

Now in all cases, you need to create @NgModule. See below an example with FormsModule.

Before:

import {disableDeprecatedForms, provideForms} from @angular/forms;
bootstrap(App, [
  disableDeprecatedForms(),
  provideForms()
]);

After:

import {DeprecatedFormsModule, FormsModule, ReactiveFormsModule} from @angular/common;
@NgModule({
  declarations: [MyComponent],
  imports: [BrowserModule, DeprecatedFormsModule],
  boostrap:  [MyComponent],
})
export class MyAppModule{}

There are a lot of other changes to take note of. You can read them in the changelog here.

Let’s start with something easy, like post.model.spec. We can begin by testing all the properties of the model:

import {Post} from './../app/models/post.model';
let testPost = {title: 'TestPost', author: 'Admin'}
describe('Post', () => {
    it('checks Post properties', () => {
        var post = new Post(testPost);
        expect(post instanceof Post).toBe(true);
        expect(post.title).toBe("testPost");
        expect(post.author).toBe("Admin");
    });
});

If you’d like to continue with Services, then it gets a bit more complicated, but the core concept is the same.

post.service.spec – this tests the service that makes API calls:

import {
    inject,
    fakeAsync,
   TestBed,
    tick
} from '@angular/core/testing';
import {MockBackend} from '@angular/http/testing';
import {
    Http,
    ConnectionBackend,
    BaseRequestOptions,
    Response,
    ResponseOptions
} from '@angular/http';
import {PostService} from './../app/services/post.service';
describe('PostService', () => {
    beforeEach(() => {
        // Inject all needed services
        TestBed.configureTestingModule({
            providers: [
                PostService,
                BaseRequestOptions,
                MockBackend,
                { provide: Http, useFactory: (backend: ConnectionBackend,
                                              defaultOptions: BaseRequestOptions) => {
                    return new Http(backend, defaultOptions);
                }, deps: [MockBackend, BaseRequestOptions]}
            ],
            imports: [
                HttpModule
            ]
        });
    });
    describe('getPost methods', () => {
        it('is existing and returning post',
            // Instantiate all needed services
            inject([PostService, MockBackend], fakeAsync((ps: postService, be: MockBackend) => {
                var res;
                // Emulate server connection
                backend.connections.subscribe(c => {
                    expect(c.request.url).toBe(AbstractAPI.url);
                    let response = new ResponseOptions({body: '{"title": "TestPost", "author": "Admin"}'});
                    c.mockRespond(new Response(response));
                });
                ps.getPost().subscribe((_post: any) => {
                    res = _post;
                });
                // tick() function is waiting until the call will be done
                tick();
                expect(res.title).toBe('TestPost');
                expect(res.author).toBe('Admin');
            }))
        );
    });
});

Now, onto the hardest part: Components.

Before giving any detailed explanations, I want to create MockPostService, which will β€œmock” PostService.

post.service.mock – here we will rewrite real calls on a mocked one to return test data:

import {PostService} from './../app/services/post.service';
import {Observable} from 'rxjs';
export class MockPostService extends PostService {
    constructor() {
        // Inherits from real service
        super();
    }
    // Rewrite real method on mocked one to return test data
    getPost() {
        // Http ΠΈis using Observable, so we need to define mocked Observable
        return Observable.of({title: 'TestPost', author: 'Admin'});
    }
}

Then we test for the component.

Before:

import {
    inject,
    addProviders
} from '@angular/core/testing';
import {TableComponent} from './../app/components/table/table.component';
// Standard Builder for components
import {TestComponentBuilder} from '@angular/core/testing';
@Component({
    selector  : 'test-cmp',
    template  : ''
})
class TestCmpWrapper {
    public postMock = new Post({'title': 'TestPost', 'author': 'Admin'});
}
describe("TableComponent", () => {
    it('render table', inject([TestComponentBuilder], (tcb) => {
        return tcb.overrideProviders(TableComponent)
            .createAsync(TableComponent)
            // In fixture we store all component metadata, like componentInstance and nativeElemnet to access Component template
fixture.debugElement.children.
            .then((fixture) => {
                let componentInstance = fixture.componentInstance;
                let nativeElement = jQuery(fixture.nativeElement);
                componentInstance.post = new Post({title: 'TestPost', author: 'Admin'});
                fixture.detectChanges();
                let firstTable = nativeElement.find('table');
                expect(firstTable.find('tr td:nth-child(1)').text()).toBe('TestPost');
                expect(firstTable.find('tr td:nth-child(2)').text()).toBe('Admin');
            });
    }));
});

After:

import {Component} from '@angular/core';
// TestComponentBuilder was changed on TestBed
import {TestBed, async} from '@angular/core/testing';
import {Post} from './../app/models/post.model';
import {TableComponent} from './../app/components/table/table.component';
// Services
import {PostService} from './../app/services/post.service';
import {MockPostService} from './post.service.mock'
// Create TestCmpWrapper and grab all test data
@Component({
    selector  : 'test-cmp',
    template  : ''
})
class TestCmpWrapper {
    public postMock = new Post({'title': 'TestPost', 'author': 'Admin'});
}
describe("TableComponent", () => {
    // Innovation – you need to create all dependencies in NgModule
    beforeEach(() => {
        TestBed.configureTestingModule({
            declarations: [
                TestCmpWrapper,
                TableComponent
            ],
            providers: [
                {provide: PostService, useClass: MockPostService
            ]
        });
    });
    describe('check rendering', () => {
        it('if component is rendered', async(() => {
            TestBed.compileComponents().then(() => {
                let fixture = TestBed.createComponent(TestCmpWrapper);
                let componentInstance = fixture.componentInstance;
                let nativeElement = jQuery(fixture.nativeElement);
                componentInstance.post = new Post({title: 'TestPost', author: 'Admin'});
                fixture.detectChanges();
                let firstTable = nativeElement.find('table');
                expect(firstTable.find('tr td:nth-child(1)').text()).toBe('TestPost');
                expect(firstTable.find('tr td:nth-child(2)').text()).toBe('Admin');
            });
        }));
    });
});

Read all the comments in the code carefully, as they are incredibly important. I’d also like to hear your own comments on whether or not this was helpful for you – they are very appreciated.


We're hiring! Do you like working in an ever evolving organization such as Zalando? Consider joining our teams as a Frontend Engineer!



Related posts