Utilities to test components

So far, we tested our components with plain vanilla JavaScript. The fact that components are in just regular classes make this possible. However, this can only be done for very simple use-cases. As soon as we'd like to test components for things that involve template compilation, user interaction on components, change detection, or dependency injection, we'll need to get a little help from Angular to perform our tests.

Angular comes with a whole bunch of testing tools that help us out here. In fact, the platform-agnostic way that Angular is built allows us to exchange the regular view adapter with a debug view adapter. This enables us to render components in such a way that allows us to inspect them in great detail.

To enable the debugging capabilities of Angular while rendering components, we need to modify our main entry point for our tests first.

Let's open up all.spec.js to make the necessary modifications:

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_PLATFORM_PROVIDERS, TEST_BROWSER_APPLICATION_PROVIDERS);

import './lib/ui/auto-complete/auto-complete.spec';
import './lib/plugin/plugin.spec';

Using the setBaseTestProviders function of the @angular/core/testing module, we can actually initialize a test platform injector, which will then be used in the context of our Angular testing. This function takes two arguments where the first one is an array of platform providers, and the second one is an array of application providers.

From the @angular/platform-browser-dynamic/testing module, we can import two constants that contain an already prepared list for both platform and application-level dependencies. Here are some of the providers present in these constants:

  • Platform-level providers: These consist mostly of platform initialization providers to debug
  • Application-level providers: These consist of the following:
    • DebugDomRootRenderer: This overrides the default DomRenderer in the browser and enables debugging of elements using DebugElement and probing
    • MockDirectiveResolver: This overrides the default DirectiveResolver and allows overriding of directive metadata for testing purposes
    • MockViewResolver: This overrides the default ViewResolver and allows overriding of component view specific metadata

Using the setBaseTestProviders function and the imported constants with the debugging providers, we can now initialize our test environment. After calling this function and passing our providers, Angular is set up for testing.

Injecting in tests

Injecting Angular dependencies in tests is made easy by two helper functions that we can use. The inject and async functions are available through the @angular/core/testing package, and they help us inject dependencies in our tests.

Let's look at this simple example where we inject the document element using the inject wrapper function. This test is irrelevant for our application, but it illustrates how we can now make use of injection in our tests:

import {describe, expect, it, inject} from '@angular/core/testing';
import {DOCUMENT} from '@angular/platform-browser';

describe('Application initialized with test providers', () => {
  it('should inject document', inject([DOCUMENT], (document) => {
    expect(document).toBe(window.document);
  }));
});

We can simply use inject to wrap our test function. The inject function accepts an array as the first parameter that should include a list of injectables. The second parameter is our actual test function, which will now receive the injected document.

The async function on the other hand helps us with a different concern too. What if our tests actually involve asynchronous operations? Well, a standard asynchronous Jasmine test would look like the following:

describe('Async test', () => {
  it('should be completed by calling done', (done) => {
    setTimeout(() => {
      expect(true).toBe(true);
      done();
    }, 2000);
  });
});

Jasmine provides us with a nice way to specify asynchronous tests. We can simply use the first parameter of our test functions, which resolves to a callback function. By calling this callback function, in our case we named it done, we tell Jasmine that our asynchronous operations are done, and we would like to finish the test.

Using callbacks to indicate whether our asynchronous test is finished is a valid option. However, this can make our test quite complicated if many asynchronous operations are involved. It's sometimes even impossible to monitor all the asynchronous operations that are happening under the hood, which also makes it impossible for us to determine the end of our test.

This is where the async helper function comes into play. Angular uses a library called Zone.js to monitor any asynchronous operation in the browser. Simply put, Zone.js hooks into any asynchronous operation and monitors where they are initiated as well as when they are finished. With this information, Angular knows exactly how many pending asynchronous operations there are.

If we're using the async helper, we tell Angular to automatically finish our test when all asynchronous operations in our test are done. The helper uses Zone.js to create a new zone and determine whether all the microtasks executed within this zone are finished.

Let's look at how we can combine injection with an asynchronous operation in our test:

import {describe, expect, it, inject, async} from '@angular/core/testing';
import {DOCUMENT} from '@angular/platform-browser';

describe('Application initialized with test providers', () => {
  it('should inject document', async(inject([DOCUMENT], (document) => {
      
      setTimeout(() => {
        expect(document).toBe(window.document);
      }, 2000);
    }))
  );
});

By combining inject with async (wrapping), we now have an asynchronous operation in our test. The async helper will make our test wait until all asynchronous operations are completed. We don't need to rely on a callback, and we have the guarantee that even internal asynchronous operations will complete before our test finishes.

Tip

Zone.js is designed to work with all asynchronous operations in the browser. It patches all core DOM APIs and makes sure that every operation goes through a zone. Angular also relies on Zone.js in order to initiate change detection.

Test component builder

Angular comes with another very important testing utility to test components and directives. So far, we only tested the component class of our components. However, as soon as we need to test components and their behavior in our application, this involves a few more things:

  • Testing the view of components: It's sometimes required that we test the rendered view of components. With all the bindings in our view, dynamic instantiation using template directives and content insertion, it's required that we can have a way to test all this behavior.
  • Testing change detection: As soon as we update our model in our component class, we want to test the updates that are performed via change detection. This involves the whole change detection behavior of our components.
  • User interaction: Our component templates probably contain a set of event bindings, which trigger some behavior on user interaction. We'd also need a way to test the state after some user interaction.
  • Overriding and mocking: In a testing scenario, it's sometimes required to mock certain areas in our components in order to create a proper isolation for our test. In unit testing, we should be concerned only about the specific behavior that we want to test.

The TestComponentBuilder, which is available through the @angular/compiler/testing package, helps us exactly with the previous concerns. It's our main tool to test components.

TestComponentBuilder is provided to the test application injector, which we initialized in our all.spec.js module using the setBaseTestProviders function. The reason for this is that the builder itself also relies on a lot of platform and application dependencies to create components. As all our dependencies now come from the test injector and most of them are overridden to enable inspection, this makes perfect sense.

Let's look at a very simple example of how we can use TestComponentBuilder to test the view rendering of a dummy component:

@Component({
  selector: 'dummy-component',
  template: 'dummy'
})
class DummyComponent {}

describe('Creating a component with TestComponentBuilder', () => {
  it('should render its view correctly', async(inject([TestComponentBuilder], (tbc) => {
      tbc.createAsync(DummyComponent).then((fixture) => {
        // When
        fixture.detectChanges();
        // Then
        expect(fixture.nativeElement.textContent).toBe('dummy');
      });
    }))
  );
});

As TestComponentBuilder is exposed in the test injector, we need to use dependency injection to get hold of the instance. We use the inject helper for this purpose. As creating a component is an asynchronous operation, we also need to make our test wait for completion using the async helper.

In our test function, we call the createAsync method of TestComponentBuilder and pass a reference to DummyComponent, which we want to create. This method returns a Promise, which will resolve once the component is successfully compiled.

In the then callback of the returned promise, we'll receive a special fixture object of the ComponentFixture type. We can then call the detectChanges method on this fixture object, which will execute change detection on the created component. After this initial change detection, the view of our dummy component is updated. We can now use the nativeElement property of the fixture in order to access the root DOM element of the created component.

Let's look at the ComponentFixture type and the available fields in more detail:

Member

Description

detectChanges()

This executes change detection on the root component that was created in the context of the fixture. The template bindings will not be evaluated automatically after creating a component using TestComponentBuilder. It's our own responsibility to trigger change detection. Even after we change the state of our components, we'd need to trigger change detection again.

destroy()

This method destroys the underlying component and performs any cleanup that is required. This can be used to test the OnDestroy component's lifecycle.

componentInstance

This property points to the component class instance, and this is our main interaction point if we want to interact with the component.

nativeElement

This is a reference to the native DOM element at the root of the created component. This property can be used to inspect the rendered DOM of our component directly.

elementRef

This is the ElementRef wrapper around the root element of the created component.

debugElement

This property points to an instance of DebugElement that was created by DebugDomRootRenderer in the component view rendering pipeline. The debug element provides us with some nice utilities to inspect the rendered element tree and testing user interaction. We'll take a closer look at this later in another section.

We've now looked at a very simple dummy component and how to test it using TestComponentBuilder in conjunction with the inject and async helper functions.

This is great, but it doesn't really reflect the complexity that we face when we need to test real components. Real components have a lot more dependencies than our dummy component. We rely on child directives and probably on injected services to obtain data.

Of course, the TestComponentBuilder also provides us with the tools that we need in order to test more complex components and keep the necessary isolation in a unit test.

Let's first look at an example where we'd like to test a ParentComponent component, which uses a ChildComponent component to render a list of numbers. As we'd only like to test ParentComponent, we're not interested in how ChildComponent renders this list. We want to remove the behavior of the child component from our test by providing a mock component for ChildComponent during our test, which allows us to easily verify that the data is received by the child component:

@Component({
  selector: 'child',
  template:'<ul><li *ngFor="let n of numbers">Item: {{n}}</li></ul>'
})
class ChildComponent {
  @Input() numbers;
}

@Component({
  selector: 'parent',
  template: '<child [numbers]="numbers"></child>',
  directives: [ChildComponent]
})
class ParentComponent {
  numbers = [1, 2, 3];
}

This is our starting point. We have two components, where we'll only be interested in testing the parent component. However, the child component is required by the parent component, and it implies a very specific way to render the numbers that are passed by the parent. We would only like to test whether our numbers were passed successfully to the child component. We don't want to involve the rendering logic of the child component in our test. This is very important because changing only the child component could then break our parent component test, which we want to avoid.

The thing we want to achieve now is to create a mock of our child component in the context of our test:

@Component({
  selector: 'child',
  template: '{{numbers.toString()}}'
})
class MockChildComponent {
  @Input() numbers;
}

In our MockChildComponent class, it's important that we use the same selector property as the real component. Otherwise, the mocking will not work. In the template, we use a very simple output of the numbers input, which enables an easy inspection.

It's also important that we provide the same input properties as the original component. Otherwise, we won't imitate the real component correctly.

Now, we can go ahead and perform our test. Using an additional method of TestComponentBuilder, we are able to override the real ChildComponent with our mock component:

describe('ParentComponent', () => {
  it('should pass data to child correctly', async(inject([TestComponentBuilder], (tbc) => {
      tbc
       .overrideDirective(ParentComponent, ChildComponent, MockChildComponent)
       .createAsync(ParentComponent).then((fixture) => {
         fixture.detectChanges();
         expect(fixture.nativeElement.textContent).toBe('1,2,3');
       });
    }))
  );
});

Using the overrideDirective method on TestBuilderComponent, we can modify the parent component's directives metadata before we create it. In this way, we're able to exchange the real child component with our MockChildComponent class.

As a result, we decouple ParentComponent from ChildComponent in the context of our test. We need this level of separation in order to create a proper isolation of our unit test. As our mock child component simply renders the string representation of the passed array, we can easily test the text content of our fixture.

Tip

The definition of a unit test is to test a single unit and isolate the unit from any dependencies. If we want to stick to this paradigm, we'd need to create a mock for every dependent component. This can easily get us into a situation where we need to maintain more complexity only for the sake of our tests. The key here lies in finding the right balance. You should mock dependencies that have a great impact on our subject and ignore dependencies that have low impact on the functionality we'd like to test.

Let's look at a different use case where we have a component that injects a service in order to obtain data. As we also want to test only our component and not the service it relies on, we somehow need to sneak in a mock service instead of the real service into our component. TestComponentBuilder also provides a method to modify the providers metadata of directives, which comes in very handy for this case.

First, we declare our base component and a service that it relies on. In this example, the NumbersComponent class injects the NumbersService class, and it obtains an array with numbers from it:

@Injectable()
class NumbersService {
  numbers = [1, 2, 3, 4, 5, 6];
}

@Component({
  selector: 'numbers-component',
  template: '{{numbers.toString()}}',
  providers: [NumbersService]
})
class NumbersComponent {
  constructor(@Inject(NumbersService) numbersService) {
    this.numbers = numbersService.numbers;
  }
}

Now, we need to create a mock service that provides the data required in our test and isolates our component from the original service:

@Injectable()
class MockNumbersService extends NumbersService {
  numbers = [1, 2, 3];
}

In this simplified example, we just provide a different set of numbers. However, in a real mocking case, we can exclude a lot of steps that are unnecessary and could potentially create side effects. Using a mock service also ensures that our test, which is focused on the NumbersComponent class, will not break because of a change in the NumbersService class.

By extending the real service, we can leverage some of the behavior of our original service while overriding certain functionality in our mock. You need to be careful with this approach though, as we rely on the original service by doing this. If you'd like to create a fully isolated test, you should probably override all methods and properties. Or you can create a completely independent mock service, which provides the same methods and properties that are used in your test.

Tip

When using TypeScript, you should use interfaces for this purpose where both your real service as well as your mock service implement the same interface.

Let's now look at the test case and how we can use TestComponentBuilder to provide our mock service instead of the real one:

describe('NumbersComponent', () => {
  it('should render numbers correctly', async(inject([TestComponentBuilder], (tbc) => {
      tbc
       .overrideProviders(NumbersComponent, [
         provide(NumbersService, {
           useClass: MockNumbersService
         })
       ])
       .createAsync(NumbersComponent).then((fixture) => {
         fixture.detectChanges();
         expect(fixture.nativeElement.textContent).toBe('1,2,3');
       });
    }))
  );
});

Using the overrideProviders method on TestComponentBuilder, we can provide additional providers to the component under test. This allows us to override existing providers that are already present on the component. Using the provide function of the @angular/core module, we can create a provider which provides on requests for NumberService but also resolves to a MockNumberService.

TestComponentBuilder allows us to perform tests in a very simple, isolated, and flexible fashion. It plays a major role when writing unit tests for components. If you'd like to read more about the available methods on TestComponentBuilder, you can visit the official documentation website at https://angular.io/docs/ts/latest/api/core/testing/TestComponentBuilder-class.html.

Now, it's time to use what we learned about TestComponentBuilder service and start to test our application components in action!

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

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