Testing components

Testing pipes is pretty straightforward but testing components can become a more daunting experience when approached for the first time. There are too many questions: how can we test a component that needs to be bootstrapped somewhere? Good news is that Angular 2, and more specifically its testing bundle, contains a class named TestComponentBuilder that can be used to instantiate fully functional components of any given type, wrapped by a fixture object that gives us access to the component instance object or its compiled HTML view. In summary, any instance of the TestComponentBuilder exposes the following properties and methods:

  • debugElement: This is the DebugElement associated with the root element of this component.
  • ComponentInstance: This returns the instance object of the root component class, with full access to all its properties and methods.
  • NativeElement: This returns the native element at the root of the component.
  • DetectChanges(): This triggers a change detection cycle for the component. We want to run this method in order to check that the changes occurred on the component state should we update any of its properties or execute its methods.
  • destroy(): This triggers component destruction.

With all these points in mind, let's create our first component test. TaskIconsComponent is a perfect candidate to start with:

app/tasks/task-icons.component.spec.ts

import TaskIconsComponent from './task-icons.component';
import {
  describe,
  expect,
  it,
  inject,
  beforeEach,
  beforeEachProviders } from '@angular/core/testing';
import { TestComponentBuilder } from '@angular/compiler/testing';

describe('tasks:TaskIconsComponent', () => {
  let testComponentBuilder: TestComponentBuilder;

  // First we setup the injector with providers for our component
  // and for a fixture component builder
  beforeEachProviders(() => [TestComponentBuilder]);

  // We reinstantiate the fixture component builder 
  // before each test
  beforeEach(
    inject([TestComponentBuilder],
    (_testComponentBuilder: TestComponentBuilder) => {
      testComponentBuilder = _testComponentBuilder;
    }
  ));

  // Specs with assertions
  it('renders 1 image for each pomodoro session required', done => {
    // We create a test component fixture on runtime 
    // out from the TaskIconsComponent symbol
    testComponentBuilder
    .createAsync(TaskIconsComponent)
    .then(componentFixture => {

      // We fetch instances of the component and the rendered DOM
      let taskIconsComponent = componentFixture.componentInstance;
      let nativeElement = componentFixture.nativeElement;

      // We set a test value to the @Input property 
      // and trigger change detection
      taskIconsComponent.task = { pomodorosRequired: 3 };
      componentFixture.detectChanges();

      // these assertions evaluate the component's surface DOM
      expect(nativeElement.querySelectorAll('img').length).toBe(3);

      // We finally destroy the component fixture and 
      // resolve the async test
      componentFixture.destroy();
      done();

    })
    .catch(e => done.fail(e));
  });

});

Take a minute to look at the import statement block at the top of the file. Besides the basic testing functions, we are importing the symbols pertaining to the DI-related functions of the testing bundle, apart from the setup methods beforeEach and beforeEachProviders. First, we will execute beforeEachProviders, passing as an argument a lambda function returning the array of providers we will need. Then, the beforeEach function uses the injector to fetch an object instance of type TestComponentBuilder, which we previously declared as a provider, and binds it to the testComponentBuilder variable. Inside the test spec, we use object variable to execute the asynchronous promisified createAsync() method, which will return a fixture around our component of choice (defined as an argument). As we saw already at the beginning of this section, we can inspect the fixture to grab an actual instance of TaskIconsComponent and its underlying native element.

Then, we begin interacting with the component by configuring its properties. Every time we update any input property or execute a method that might change the component state, we need to execute the fixture's detectChanges() method to trigger change detection and hence reflect that state change in the nativeElement. This allows us to test assertions using a matcher function to compare the amount of DOM nodes generated against the expected amount of nodes configured in the matcher. Finally, we destroy the component and resolve the asynchronous function spec we're in.

Usually, a test has more than one spec, one per each functionality described, for instance. Thus, let's add another it statement right after the one we already have inside the describe() suite:

app/tasks/task-icons.component.spec.ts (continued)

...
it('should render each image with the proper width', done => {
  testComponentBuilder.createAsync(TaskIconsComponent)
  .then(componentFixture => {
    let taskIconsComponent = componentFixture.componentInstance;
    let nativeElement = componentFixture.nativeElement;
    let actualWidth;

    taskIconsComponent.task = { pomodorosRequired: 2 };
    taskIconsComponent.size = 60;
    componentFixture.detectChanges();

    actualWidth = nativeElement
      .querySelector('img')
      .getAttribute('width');

    expect(actualWidth).toBe('60');
    done();
  })
  .catch(e => done.fail(e));
});
...

We can add as many specs as we feel necessary to provide a broad coverage of all scenarios.

Tip

Debugging our own tests

As we create more and more specs, chances are we will introduce some bugs in our own test implementations, turning this into a general failure in our test report. Tracking down these issues can be a bit tricky, so the best way to address this scenario is by isolating test suites or specs execution or, all the way around, disabling the execution of broken tests temporarily. To do so, we can use variations of the describe() and it() functions, by prepending a letter to the function name, as follows:

  • fdescribe(): This instructs the test runner to only run the test cases in this group. It can be used as ddescribe() as well.
  • fit(): The test runner will only execute this test, disregarding all the others. It can be used as iit() as well.
  • xdescribe(): This instructs the runner to exclude this test suite from execution.
  • xit(): This instructs the runner to exclude this test spec from execution.

Testing components with dependencies

In the previous section, we undertook our very first unit test, taking a simple component with no dependencies as an example. However, components and other Angular 2 modules usually have dependencies injected. The unit test needs to reflect this circumstance. Let's look at TimerWidgetComponent as an example. This tiny component requires all these dependencies injected through its constructor:

app/timer/timer-widget.component.ts

...
constructor(
    private settingsService: SettingsService,
    private routeParams: RouteParams,
    private taskService: TaskService,
    private animationBuilder: AnimationBuilder,
    private elementRef: ElementRef) { ...

Thus, in order to instantiate the component within the fixture returned by the TestComponentBuilder factory, we need to declare the providers for these dependencies at the test injector and have them injected somehow. On top of that, the component had some important nuances in its execution: it is sensitive to URL params and one of its dependencies (TaskService) performs underlying XHR operations by means of the Http module. It needs to be intercepted and properly mocked.

This might sound quite daunting, but the truth is that you already know all the code procedures required to put together this test. Let's see it with inline comments in the code:

app/timer/timer-widget.component.test.ts

import TimerWidgetComponent from './timer-widget.component';
import { provide } from '@angular/core';
import { RouteParams } from '@angular/router-deprecated';
import { SettingsService, TaskService } from '../shared/shared';
import {
  describe,
  expect,
  it,
  inject,
  beforeEach,
  beforeEachProviders,
  setBaseTestProviders } from '@angular/core/testing';
import { TestComponentBuilder } from '@angular/compiler/testing';
import { Http, BaseRequestOptions }  from '@angular/http';
import { MockBackend } from '@angular/http/testing';
import 'rxjs/add/operator/map';

describe('timer:TimerWidgetComponent', () => {
  let testComponentBuilder: TestComponentBuilder;
  let componentFixture: any;

  // First we setup the injector with providers for our component
  // dependencies and for a fixture component builder
  // Note: Animation providers are not necessary
  beforeEachProviders(() => [
    TestComponentBuilder,
    SettingsService,
    TaskService,

    // RouteParams is instantiated with custom values upon injecting
    provide(RouteParams, {useValue: new RouteParams({id: null})}),
    
    // We replace the Http provider injected later in TaskService
    MockBackend,
    BaseRequestOptions,
    provide(Http, { useFactory: 
      (backend:MockBackend, options:BaseRequestOptions) => {
        return new Http(backend, options);
      },
      deps: [MockBackend, BaseRequestOptions]
    }),
    TimerWidgetComponent
  ]);

  // We reinstantiate the fixture component builder before each test
  beforeEach(inject([TestComponentBuilder], 
    (_testComponentBuilder: TestComponentBuilder) => {
      testComponentBuilder = _testComponentBuilder;
    }
  ));
});

You have probably noticed there is not a single test assertion in the suite. We will get there in a minute, but now let's overview each piece of code in the script. The test suite implementation contains everything you already know about injecting providers in tests. First we use beforeEachProviders() to declare all the providers we need the injector to be aware of. As you will remember, the TimerWidgetComponent had a dependency on RouteParams, which allowed us to fetch the value of the id query string parameter, if any. Obviously, we not only need to inject that provider, but our injector ought to return an instance of it with the parameter properly populated for our testing purposes:

provide(RouteParams, {useValue: new RouteParams({id: null})}),

A bit more attention is required to understand how we accomplish the HTTP requests performed by TaskService. The default constructor of the Http module requires a backend connection object implementing the ConnectionBackend interface. For our test, we need to provide the injector with a working version of Http and thus we leverage MockBackend, which implements the interface required. Later in this chapter, we will see how we can leverage this class to intercept XHR requests and return canned responses for our tests.

BaseRequestOptions,
provide(Http, { useFactory: 
  (backend:MockBackend, options:BaseRequestOptions) => {
    return new Http(backend, options);
  },
  deps: [MockBackend, BaseRequestOptions]
}),

With all the providers properly available from the injector, we can leverage it to instantiate a new TestComponentBuilder factory object before executing any test.

beforeEach(inject([TestComponentBuilder], 
  (_testComponentBuilder: TestComponentBuilder) => {
    testComponentBuilder = _testComponentBuilder;
  }
));

With all the testing scaffolded and ready, let's introduce our first test spec. Append the following piece of code right after the beforeEach(...) block within the body of the describe(...) suite:

it('should initialise with the pomodoro counter at 24:59', done => {
  // We create a test component fixture on
  // runtime out from the component symbol
  testComponentBuilder
  .createAsync(TimerWidgetComponent)
  .then(componentFixture => {

    // We fetch instances of the component and the rendered DOM
    let timerWidgetComponent = componentFixture.componentInstance;
    let nativeElement = componentFixture.nativeElement;

    // We execute the OnInit hook and trigger change detection
    timerWidgetComponent.ngOnInit();
    componentFixture.detectChanges();

    // These assertions evaluate the component properties
    expect(timerWidgetComponent.isPaused).toBeTruthy();
    expect(timerWidgetComponent.minutes).toEqual(24);
    expect(timerWidgetComponent.seconds).toEqual(59);

    componentFixture.destroy();
    done(); // Resolve async text
  })
    .catch(e => done.fail(e));
});

As you can see, our newly created spec executes seamlessly by instantiating a component fixture wrapping the component instance and native element we require. We execute the component's ngOnInit() hook method to force its initialization as if had been rendered on a view. Then, we trigger the fixture's detectChanges() method that will trigger a change detection cycle on our component instance, applying any state change as a result of the operations taken place within ngOnInit().

The following spec, which you can append to the describe() body right after the previous test spec, reinforces these concepts:

it('should initialise displaying the default labels', done => {
  testComponentBuilder
  .createAsync(TimerWidgetComponent)
  .then(componentFixture => {
    componentFixture.componentInstance.ngOnInit();
    componentFixture.detectChanges();

    expect(componentFixture.componentInstance.buttonLabelKey)
      .toEqual('start');

    expect(componentFixture.nativeElement
      .querySelector('button')
      .innerHTML.trim())
      .toEqual('Start Timer');

    componentFixture.destroy();
    done();
  })
  .catch(e => done.fail(e));
});

Overriding component dependencies for refined testing

In the previous examples, we saw how we could declare and inject the providers that our subjects of testing required. We also saw how we could leverage the provide() function to pass the injector an instance of any given provider already populated with the values we require. In that sense, provide() is not just used to replace dependency types upon injecting providers, but to customize the way we want a particular provider of that specific type to be injected.

However, can we override providers at a test spec level? The answer is yes, and it is quite useful when it comes to mock dependency values for certain tests. In our next test spec, we will continue testing the timer widget component. However, we will override the TaskService provider this time, replacing it by an object literal with mock data. We will also override the default RouteParams injection with another instance object of RouteParams, featuring an actual value for the id parameter.

Add this test spec right after the previous two specs within the body of the describe(...) function:

it('should initialise displaying a specific task', done => {
  // We mock the TaskService provider with some fake data
  let mockTaskService = {
    taskStore: [{
        name: 'Task A'
      }, {
        name: 'Task B'
      }, {
        name: 'Task C'
      }
    ]
  };

  testComponentBuilder
  .overrideProviders(TimerWidgetComponent, [
    provide(RouteParams, { useValue: new RouteParams({ id: '1' }) }),
    provide(TaskService, { useValue: mockTaskService })
  ])
  .createAsync(TimerWidgetComponent)
  .then(componentFixture => {
    componentFixture.componentInstance.ngOnInit();
    componentFixture.detectChanges();

    expect(componentFixture.componentInstance.taskName)
      .toEqual('Task B');

    expect(componentFixture.nativeElement.querySelector('small'))
      .toHaveText('Task B');

    componentFixture.destroy();
    done();
  })
  .catch(e => done.fail(e));
});

The code is pretty self-explanatory, but let's take some minutes to analyze this block:

testComponentBuilder.overrideProviders(TimerWidgetComponent, [
  provide(RouteParams, { useValue: new RouteParams({ id: '1' }) }),
  provide(TaskService, { useValue: mockTaskService })
])

Basically, we leverage the overrideProviders() method of the TestComponentBuilder factory, which will expect in its first argument the type of the component whose providers we want to override and an array of providers as a second argument. We can insert in such an array any kind of type override or replacement by means of the provide() function.

In order to get the overrideProviders() to work, it parses the current providers property of the component decorator whose providers we want to override. If the component does not feature the property in its decorator (mostly because all its dependencies are inherited from the root injector), Angular will throw an exception. So, for our example, please include an empty providers property in the TimerWidgetComponent decorator configuration:

app/timer/timer-widget.component.ts

@Component({
  selector: 'pomodoro-timer-widget',
  styleUrls: ['app/timer/timer-widget.component.css'],
  providers: [],
  template: ` … `
}) 

This issue might be addressed by the Angular 2 team in the future, but in the meantime we need to proceed this way.

Tip

Need to override a test component's directives or template?

You can override other elements of the test component instance with the methods overrideTemplate(), overrideView() (which gives you access to override the literal defining things such as styles), or overrideDirectives(). Their signature follows pretty much the same convention, where we define first the component type and then, as a second argument, the replacement we need for the component original value.

Please refer to the official API documentation for further details if required.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.197.2