Testing Angular 2 Apps (Part 2): Dependency Injection and Components

If you missed Part 1 of our Testing Angular 2 Apps series, I encourage you to take a look at it here.

Note: Article is based on Angular 2 beta.2

Up-to-date Gist with examples for whole series is here: Angular 2 testing gist

Starter project with setup for tests is here: https://github.com/wkwiatek/angular2-webpack2

Testing Services

The easiest thing to start testing real Angular 2 thing is Service. It is pure JS class which is about to be injected.

Let’s start with simple service:

export class TestService {
  public name: string = 'Injected Service';
}

We can go ahead and use something you’ve learned in the previous part:

import {TestService} from './test.service';

describe('TestService', () => {

  beforeEach(function() {
    this.testService = new TestService();
  });

  it('should have name property set', function() {
    expect(this.testService.name).toBe('Injected Service');
  });

});

There it is. Pure service tests can look just like that! But what about injecting them? If you do something like this in your bootstrap function:

bootstrap(App, [TestService]);

then your TestService is ready to be injected through the whole App. It means every time when something needs that service we can simply use it.

Things get a little bit more complicated when we want to test such a behaviour. First of all we won’t use pure Jasmine functions any more. This is what is about to happen:

import {
  beforeEach,
  beforeEachProviders,
  describe,
  expect,
  it,
  inject,
  injectAsync
} from 'angular2/testing';


import {TestService} from './service';

Note: To run tests in the browser one more thing needs to be included. It’s DOM adapter. It looks like this:

import {setBaseTestProviders} from 'angular2/testing';
import {
  TEST_BROWSER_PLATFORM_PROVIDERS,
  TEST_BROWSER_APPLICATION_PROVIDERS
} from 'angular2/platform/testing/browser';
setBaseTestProviders(TEST_BROWSER_PLATFORM_PROVIDERS,
                     TEST_BROWSER_APPLICATION_PROVIDERS);

What’s in there? Note that all of the helpers from Jasmine were replaced by their equivalents from angular2/testing. Why? Because Angular 2 relies heavily on Dependency Injection (great article on that topic) and this is not something that Jasmine is aware of. Angular team made wrappers which add their own logic (see source code). The outcome is we can now use inject() instead of anonymous callback function in it or beforeEach that will inject some class.

Note: Angular 2 will eventually use @Inject decorator instead of inject function but now it’s matter of Traceur limitation.

So to inject something we can say hello to beforeEachProviders and inject:

describe('TestService', () => {

  beforeEachProviders(() => [TestService]);

  it('should have name property set', inject([TestService], (testService: TestService) => {
    expect(testService.name).toBe('Injected Service');
  }));

});

Here it is! We’ve injected our service into test case. beforeEachProviders is a way of injecting providers that were originally specified in bootstrap.

Note: testService: TestService is only type checking here unlike injection in Component.

Mocking providers

We haven’t solved any problem yet but it’s about to come! Imagine you’d like to mock the service now. It’s common pattern to make a testing environment independent from real implementation of service.

class MockTestService {
  public mockName: string = 'Mocked Service';
}

describe('TestService', () => {

  beforeEachProviders(() => [
    provide(TestService, {useClass: MockTestService})
  ]);

  it('should have name property set', inject([TestService], (testService: TestService) => {
    expect(testService.mockName).toBe('Mocked Service');
  }));

});

What’s cool about that? In the test case code doesn’t know what exactly is TestService. It doesn’t care – it’s completely transparent. This way we can test the behaviour of the component itself, not its dependencies. Moreover, we can use something from both worlds – the real one and the mocked (if really needed). How? Using simple JS inheritance:

class MockTestService extends TestService {
  public sayHello(): string {
    return this.name;
  }
}

describe('TestService', () => {

  beforeEachProviders(() => [
    provide(TestService, {useClass: MockTestService})
  ]);

  it('should say hello with name', inject([TestService], (testService: TestService) => {
    expect(testService.sayHello()).toBe('Injected Service');
  }));

});

Component test with DOM changes

Ok. Injecting services themselves to test suites is not what you really need. They can be tested using pure classes (unless you want to inject service into service). The point is that the knowledge gives you ability to fully test a component! But leave it for a while. Go to something different for now.

Let’s say that our component is a list of items (e.g. users). It’s a dumb component – all it does is rendering list for given input. It can’t be simpler than that:

import {Component, Input} from 'angular2/core';
import {NgFor} from 'angular2/common';

@Component({
  selector: 'list',
  template: '<span *ngFor="#user of users">{{user}}</span>',
  directives: [NgFor]
})
export class ListComponent {
  @Input() public users: Array<string> = [];
}

And now we have to meet a little bit of a ‘magic’ to create such component in tests:

describe('ListComponent', () => {

  it('should render list', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.createAsync(ListComponent).then((componentFixture: ComponentFixture) => {
      const element = componentFixture.nativeElement;
      componentFixture.componentInstance.users = ['John'];
      componentFixture.detectChanges();
      expect(element.querySelectorAll('span').length).toBe(1);
    });
  }));

});

Note: Do you remember about BrowserDomAdapter to make your test work?

First of all look at TestComponentBuilder. It will create component from given class with decorator and come back with created component as a fixture. We also use injectAsync there and adds return to handle asynchronous component creation. We are now able to perform operations on its properties, methods etc. and moreover – we are able to check if Angular specific stuff is happening. What am I talking about? We now have instance of component so we can check everything what is connected with it. One of the valuable things is ability to check if renders what’s proper data.

Note: We don’t care whether property is passed via @Input() or not. Change detection has to be run manually so we assume angular will take care of it. It can be a problem if we’re setting different detection strategy.

Now to get fully functional component we miss one thing – DI for component.

Component test with DOM and DI

We have covered these topics alone. Now the point is to combine both to get all you need to test most of the components. So let’s use values of injected service instead of @Input(). There goes the service:

export class UserService {
  public users: Array<string> = ['John'];
}

And the component:

import {Component, Input} from 'angular2/core';
import {NgFor} from 'angular2/common';

@Component({
  selector: 'list',
  template: '<span *ngFor="#user of users">{{user}}</span>',
  directives: [NgFor]
})
export class ListComponent {
  private users: Array<string> = [];

  constructor(userService: UserService) {
    this.users = userService.users;
  }
}

If we run the following test now it’ll throw an error:

describe('ListComponent', () => {

  it('should render list', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.createAsync(ListComponent).then((componentFixture: ComponentFixture) => {
      const element = componentFixture.nativeElement;
      componentFixture.detectChanges();
      expect(element.querySelectorAll('span').length).toBe(1);
    });
  }));

});

There’s no boostrap in tests so we have to mock dependencies to pass it!

Let’s fix it:

class MockUserService {
  public users: Array<string> = ['John', 'Steve'];
}

describe('ListComponent', () => {

  beforeEachProviders(() => [
    provide(UserService, {useClass: MockUserService})
  ]);

  it('should render list', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.createAsync(ListComponent).then((componentFixture: ComponentFixture) => {
      const element = componentFixture.nativeElement;
      componentFixture.detectChanges();
      expect(element.querySelectorAll('span').length).toBe(2);
    });
  }));

});

Now there’s used mocked version of provider given in bootstrap.

But providers can be also defined in component itself. So that the code looks as follows:

import {UserService} from './user.service';

@Component({
  selector: 'list',
  template: '<span *ngFor="#user of users">{{user}}</span>',
  directives: [NgFor],
  providers: [UserService]
})

This time beforeEachProviders won’t work. We have to overwrite providers when creating a test component:

it('should render list', inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
  tcb
    .overrideProviders(ListComponent, [provide(UserService, {useClass: MockUserService})])
    .createAsync(ListComponent).then((componentFixture: ComponentFixture) => {
      const element = componentFixture.nativeElement;
      componentFixture.detectChanges();
      expect(element.querySelectorAll('span').length).toBe(2);
    });
}));

The same way we can e.g. overwrite template of the component to use mock instead. Final version would probably be splitted to cope with more test cases and be DRY:

describe('ListComponent', () => {

  beforeEach(injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb
      .overrideProviders(ListComponent, [provide(UserService, {useClass: MockUserService})])
      .createAsync(ListComponent)
      .then((componentFixture: ComponentFixture) => {
        this.listComponentFixture = componentFixture;
      });
  }));

  it('should render list', () => {
    const element = this.listComponentFixture.nativeElement;
    this.listComponentFixture.detectChanges();
    expect(element.querySelectorAll('span').length).toBe(2);
  });

});

It should give you enough knowledge to test real app. It’s everything for now. Keep on waiting for the next part – you’ll probably want to add a router and some API, right?

Part 3 is here

Tagged: , , , , ,

Categorised in: ,

  • Great article.
    Swietny artykul – czekam na więcej

    • Wojciech Kwiatek

      Thanks! We’re trying to write more and more about technical stuff here. Angular 2 will be one of the main topics I bet.

      • There it is. Pure service tests can look just like that! But what about injecting them? If you do something like this in your bootstrap function:

  • Joshua Godi

    What about if you use the HTTP object NG2 provides?

    • Wojciech Kwiatek

      I’m going to cover these in the next part so please stay tuned! It should be ready next week.

  • Awesome article looking forward to the next one I like how you build up the test cases through the last two blogs.

  • Darius

    Component test with DOM changes does not work for me. I modified a bit and getting error:

    app/todo_service/todoItemRenderer.spec.ts(42,27): error TS2345: Argument of type ‘FunctionWithParamTokens’ is not assignable to parameter of type ‘(done: () => void) => void’.

    Type ‘FunctionWithParamTokens’ provides no match for the signature ‘(done: () => void): void’

    injectAsync is underlined in web storm

    it('should render list', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.createAsync(TodoItemRenderer).then((componentFixture: ComponentFixture) => {
    const element = componentFixture.nativeElement;
    componentFixture.componentInstance.users = ['John'];
    componentFixture.detectChanges();
    expect(element.querySelectorAll('span').length).toBe(1);
    });
    }));

    created SO question:
    http://stackoverflow.com/questions/35991531/injectasync-argument-of-type-functionwithparamtokens-is-not-assignable-to-para

    • Wojciech Kwiatek

      As someone pointed in SO – it’s rather problem with wrong imports than test itself. At the beginning of the post is a detailed explanation.

      • Darius

        thanks, after imported those things, things started working well. Did not start reading article from begining and spent many hours, thinking I know basics so I can skip :(

  • dydek

    Have you tried to test component which uses async pipe to display data ? I can’t figure out how to write it, I always have an error about detectChangesInRecords.

    • Wojciech Kwiatek

      Yes, I’ve tried but without success really (but I don’t see any error – just no date when the result should be). It seems like detectChanges is not enough to force async to handle promise/observable.

      It may be a bug in Angular core so it if it’s crucial for you I advice you to submit an issue.

      Sorry for responding so late.

      • Brecht Billiet

        Do you have any updates about this? Is this actually a bug in the Angular Core?

  • It would be helpful to me, and probably many other readers of this article, if you would specify the names of the files I should use for each of the code blocks you list on the page.

    • Wojciech Kwiatek

      I haven’t added them because there are in fact 3 files here (two services and test file).

      In the near future I’m going to add repo/gist/whatever to keep all best practices from these articles as a real code.

      Thanks for sharing your thoughts!

    • Wojciech Kwiatek

      I added links to the source code at the top. Enjoy!

  • Дмитрий

    Thank you for an article. I’m using Angular 2.0.0-beta.7
    I’ve followed all recommendations in this article but still I’m having an error.
    I got a component that has service specified in “providers” and in “constructor”.
    Trying to mock it as you’ve mentioned in your latest code sample, creates an error without explanation.
    I’ve created SO question for this: http://stackoverflow.com/questions/35888510/error-during-mocking-the-service-for-angular2-application-unit-test
    Is there something that has changed in Angular 2, so your method doesn’t work anymore or it’s my fault?

    • Wojciech Kwiatek

      You’re trying to inject component (ApplicationsSelectorComponent) as a provider in test (beforeEachProviders).

      • Дмитрий

        I’ve tried all possible possibilities. Now I got code like this and still the same issue:

        class ApplicationsServiceMock {

        getApplications() {

        return ['ABC', 'XYZ'];

        }

        }

        describe('ApplicationsSelectorComponent', () => {

        beforeEach(injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {

        return tcb

        .overrideProviders(ApplicationsSelectorComponent, [provide(ApplicationsService, { useClass: ApplicationsServiceMock })])

        .createAsync(ApplicationsSelectorComponent)

        .then((componentFixture: any) => {

        this.component = componentFixture;

        });

        }));

        it('should have empty default values', () => {

        console.log('In Test: ', this.component);

        });

        });

        • Wojciech Kwiatek

          I’ve just added response to your SO question as it’s better place to put the code.

  • Dan

    IMHO I think testing DOM elements in tests with jasmine is a big mistake. I would test functionalities rather than UI. For that I would go for e2e. I really like your articles tho, they have helped me a lot. I need to test a component which injects a service, the problem is I used to do this with Angular 1 and now it seems like the concept has changed. I tried something like:

    let subjectsComponent = new SubjectsComponent(_navController, _navParams, _mockService);

    This kind of approach worked in Angular 1. In Angular 2 maybe I MUST use the TestComponentBuilder?

    • Wojciech Kwiatek

      You’re not really testing DOM elements but only the template logic behavior (e.g. when you have collection in controller and should be printed as a list in the view). I don’t think it’s wrong. It’s just another step between integration tests and unit tests of a controller functions because you’re still testing just one component separately.

      In case of ng1 vs ng2 – you have to use TestComponentBuilder to have all utilities.

  • Manningham

    What if your component has a whole bunch of providers and you only want to mock some of them? Struggling with this now…

    • Wojciech Kwiatek

      Then you just mock some of them and inject real providers to the component.

      • Manningham

        Hmm that doesn’t seem to work, or I am doing it wrong:


        beforeEach(() => {
        addProviders([ProductClient])
        });

        beforeEach(async(inject([TestComponentBuilder],
        (tcb: TestComponentBuilder) => {
        return tcb
        .overrideProviders(ProductsMain, [
        provide(ProductRepository, { useClass: ProductRepositoryMock }),
        provide(ProgressService, { useClass: ProgressServiceMock }),
        provide(NotificationService, { useClass: NotificationServiceMock })
        ])
        .createAsync(ProductsMain)
        .then((f: ComponentFixture) => {
        fixture = f;
        main = f.componentInstance;
        element = f.nativeElement;
        initialize();
        });
        })));

        it('initializes', () => {
        expect(main).toBeDefined();
        });

        RC4 seems to have changed the syntax a bit, but the general implementation in your post holds. The test in the code block above fails (`createAsync` promise is never resolved), but succeeds if I include that last service, `ProductClient`, in `overrideProviders`.

  • uma

    I don’t know anything about testing, after reading this article i got clear picture about it. Great article @wojciech_kwiatek:disqus. Thank you very much.

  • JimTheMan

    This guide is outdated. I’m importing from ‘@angular/core/testing’, and it’s giving me an error that “beforeEachProviders is not a function”.

  • Joao Reis

    You need to remove this or update it. It’s totally outdated

    • a_n_t_m_a_n

      Totally agree with @disqus_lRv8hgJwzj:disqus here. It’s not a very useful tutorial in its current state.

      Just cloning the starter project and running `npm install` and `npm test` throws nonsense errors like: `WebpackOptionsValidationError: Invalid configuration object.`

      After `npm install npm-update-all` and `npm-update-all` you still get `npm test` throwing errors all over the place.