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:
DebugDomRootRenderer
: This overrides the default DomRenderer
in the browser and enables debugging of elements using DebugElement
and probingMockDirectiveResolver
: This overrides the default DirectiveResolver
and allows overriding of directive metadata for testing purposesMockViewResolver
: This overrides the default ViewResolver
and allows overriding of component view specific metadataUsing 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 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.
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:
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 |
---|---|
|
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 |
|
This method destroys the underlying component and performs any cleanup that is required. This can be used to test the |
|
This property points to the component class instance, and this is our main interaction point if we want to interact with the component. |
|
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. |
|
This is the |
|
This property points to an instance of |
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.
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.
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!