Testing and faking Angular dependencies
Lars Gyrup Brink Nielsen
Posted on March 24, 2021
Let’s prepare our experimental gear. Cover photo by deepakrit on Pixabay.
Original publication date: 2019-04-29.
Dependency injection is a key feature of Angular. This flexible approach makes our declarables and class-based services easier to test in isolation.
Tree-shakable dependencies remove the layer of indirection that is Angular modules, but how do we test their tree-shakable providers? We’ll test value factories that depend on injection tokens for platform-specific APIs.
Some components have browser-specific features. Together, we’ll test a banner that notifies our user that we’re ending Internet Explorer 11 support. A proper test suite can give us enough confidence that we won’t even have to test the banner in Internet Explorer 11.
Just kidding! We have to be careful not to get overly confident about complex integration scenarios. We should always make sure to perform QA (Quality Assurance) tests in environments as close to production as possible. This means running the application in a *real* Internet Explorer 11 browser.
The Angular testing utilities enable us to fake dependencies for the purpose of testing. We’ll explore different options of configuring and resolving dependencies in an Angular testing environment using the Angular CLI’s testing framework of choice, Jasmine.
Through examples, we’ll explore component fixtures, component initialisation, custom expectations, emulated events. We’ll even create custom test harnesses for very thin but explicit test cases.
Faking dependency injection tokens used in token providers
In “Tree-shakable dependencies in Angular projects”, we created a dependency injection token that evaluates to a flag indicating whether the current browser is Internet Explorer 11.
// user-agent.token.ts
import { InjectionToken } from '@angular/core';
export const userAgentToken: InjectionToken<string> =
new InjectionToken('User agent string', {
factory: (): string => navigator.userAgent,
providedIn: 'root',
});
To test the Internet Explorer 11 flag provider in isolation, we can replace the userAgentToken
with a fake value. We’ll practice that technique later in this article.
We notice that the user agent string provider extracts the relevant information from the platform-specific Navigator API. For the sake of learning, let’s say that we’re going to need other information from the same global navigator
object. Depending on the test runner we use, the Navigator API might not even be available in the testing environment.
To be able to create fake navigator configurations, we create a dependency injection token for the Navigator API. We can use these fake configurations to simulate user contexts during development and testing.
// user-agent.token.ts
import { inject, InjectionToken } from '@angular/core';
import { navigatorToken } from './navigator.token';
export const userAgentToken: InjectionToken<string> =
new InjectionToken('User agent string', {
factory: (): string => inject(navigatorToken).userAgent,
providedIn: 'root',
});
What we test and how we test it should be part of our testing strategy. In more integrated component tests, we should be able to rely on most of the providers created as part of our dependency injection tokens. We’ll explore this later when testing the Internet Explorer 11 banner component.
WHAT we test and HOW we test it should be part of our testing strategy.
For our first test, we’re going to provide a fake value for the Navigator API token which is used as a dependency in the factory provider for the user agent string token.
To replace a token provider for testing purposes, we add an overriding provider in the Angular testing module similar to how an Angular module’s own providers override those of an imported Angular module.
Note that while it’s the user agent token and its provider we’re testing, it’s the navigator token dependency we’re replacing with a fake value.
Resolving dependencies using the inject
function
The Angular testing utilities give us more than one way to resolve a dependency. In this test, we use the [inject](https://angular.io/api/core/testing/inject)
function from the @angular/core/testing
package (*not* the one from @angular/core
).
The inject
function allows us to resolve multiple dependencies by listing their tokens in an array that we pass as an argument. Every dependency injection token is resolved and available to the test case function as a parameter.
I have created a StackBlitz project with all the tests from this article running in Jasmine. As seen in the test report, the test works. We have successfully faked the native Navigator API for the purpose of testing.
Gotchas when using the Angular testing function inject
When we are using the Angular testing module without declarables, we can usually override a provider several times even within the same test case. We’ll examine an example of that later in this article.
It’s worth noting that this is not the case when using the Angular testing function [inject](https://angular.io/api/core/testing/inject)
. It resolves dependencies just before the test case function body is executed.
We can replace the token provider in beforeAll
and beforeEach
hooks using the static methods TestBed.configureTestingModule
and TestBed.overrideProvider
. But we can’t vary the provider between test cases or replace it during a test case when we use the inject
testing function to resolve dependencies.
Resolving dependency injection tokens using TestBed
A more flexibly way of resolving Angular dependencies in tests without declarables is to use the static method TestBed.get
. We simply pass the dependency injection token we want to resolve, from anywhere in a test case function or a test lifecycle hook.
Let’s look at another example of a native browser API that we abstract using a dependency injection token for the purpose of development and testing.
// location.token.ts
import { DOCUMENT } from '@angular/common';
import { inject, InjectionToken } from '@angular/core';
export const locationToken: InjectionToken<Location> =
new InjectionToken('Location API', {
factory: (): Location => inject(DOCUMENT).location,
providedIn: 'root',
});
The factory in the token’s provider is extracted from the DOCUMENT
token which is available from the @angular/common
package and abstracts the global document
object.
In this test suite, we configure the Angular testing module inside the test case. I think it better illustrates the token dependency that we want to exercise in this test.
We make the Angular dependency injection system resolve the Location API by using the static TestBed.get
method. As demonstrated in the StackBlitz testing project, the document token is successfully faked and used to resolve the token-under-test using its real factory provider.
Gotchas when resolving dependencies using TestBed
In the previous test, we replaced the document with a fake object by providing it for the DOCUMENT
token in the Angular testing module. If we hadn’t done that, Angular would’ve provided the global document
object.
Additionally, if we wanted to test different document configurations, we wouldn’t be able to do so, had we not created a testing provider for the document token.
In the case that we add a testing provider using TestBed.configureTestingModule
, we can use the static method TestBed.overrideProvider
to replace it with different fake values in various test cases. We’ll use this technique to create test harnesses when testing Internet Explorer 11 detection and the Internet Explorer 11 banner component.
Note that this is only possible because we don’t use declarables. As soon as we call TestBed.createComponent
, the Angular testing platform dependencies are locked.
Testing value factories with dependencies
In the first section of this article, we introduced a token with a value factory in its provider. The value factory evaluates whether the user agent string represents an Internet Explorer 11 browser.
To test the browser detection in the value factory, we gather a few user agent strings from real browsers and put them in an enum.
In the Internet Explorer 11 detection test suite, we’ll test the isInternetExplorer11Token
almost in isolation. But the real business logic value lies in its factory provider which depends on the user agent token.
The user agent token extracts its value from the Navigator API token, but that dependency has already been covered by the Navigator API test suite. We’ll pick the user agent token as the adequate place in the dependency chain to start faking dependencies.
Before specifying the test cases, we create a test setup function and reduce an array of the non-Internet Explorer user agent strings from our fake user agent strings.
The test setup function takes a user agent and uses it to fake the user agent token provider. We then return an object with a property isInternetExplorer11
having a value that is evaluated from the isInternetExplorer11Token
through the TestBed.get
method.
Let’s test the happy path first. We pass an Internet Explorer 11 user agent string and expect the token-under-test to evaluate to true
through Angular’s dependency injection system. As seen in the StackBlitz testing project, the browser detection works as expected.
What happens when the user browses with Internet Explorer 10? Our test suite demonstrates that Internet Explorer 11 does not result in a false positive in this case.
In other words, the token-under-test evaluates to false
when an Internet Explorer 10 user agent string is provided in the dependee token. If this is not the intended usage, we’d need to change the detection logic. Now that we’ve got a test, it’d be easy to demonstrate when that change would become successful.
The final test exercises the browser detection on non-Internet Explorer browsers defined by the FakeUserAgent
enum. The test case loops through the user agent strings, fakes the user agent provider, evaluates the isInternetExplorer11Token
and expect its value to be false
. If that is not the case, a useful error message is displayed by the test runner.
Faking dependencies in component tests
Now that we are satisfied with our Internet Explorer 11 browser detection, creating and displaying a deprecation banner is straightforward.
<!-- internet-explorer-11-banner.component.html -->
<aside *ngIf="isBannerVisible">
Sorry, we will not continue to support Internet Explorer 11.<br />
Please upgrade to Microsoft Edge.<br />
<button (click)="onDismiss()">
Dismiss
</button>
</aside>
We enable the user to dismiss the banner. It’s displayed if the user agent (the browser) is Internet Explorer 11 and the user hasn’t yet dismissed the banner by clicking the banner button.
Dismissable Internet Explorer 11 deprecation banner.
The dismissed state is simply stored as local UI state in a private component property which is used by the computed property isBannerVisible
.
The banner component has a single dependency—the isInternetExplorer11Token
which is evaluated to a Boolean value. This Boolean value is injected through the banner component constructor because of the Inject
decorator.
Testing the banner component
To test the banner component, we could simply fake the isInternetExplorer11Token
since it’s a direct dependency. However, integration tests that exercise multiple modules give us even more confidence in our components.
Instead, we will fake the userAgentToken
by providing a value from the FakeUserAgent
enumeration. From previous tests, we know that this chain of dependencies works.
There are 3 features we’d like to exercise in our tests:
- When the user agent is Internet Explorer 11, the banner is displayed
- When the user clicks the banner button, the banner is dismissed
- When any other browser than Internet Explorer 11 is used, the banner is hidden
To have concise tests, we’ll create a test harness that enables us to:
- Fake the user agent
- Check the banner visibility
- Click the dismiss button
This is how we want the test cases to look:
The test harness is returned by our custom setup
function. We’ll look at the implementation in a few seconds.
First, I want you to notice, that we only test Internet Explorer 11 and one other browser. We already covered browser detection of all our supported browsers in the test suite demonstrated by the section “Testing value factories with dependencies”.
Okay, let’s explore how the test harness is created.
If you are familiar with the Angular testing utilities, this should be pretty straightforward.
We fake the user agent token with the passed parameter. Then we create a component fixture for the banner component and initialise it by triggering change detection.
Finally, we create a couple of expectations to verify the banner visibility and a function to emulate a click of the dismiss button. These utilities are returned as methods on the test harness object.
You might wonder how we can create a component fixture without configuring the testing module. Don’t worry, we just need to make sure that the testing module is configured prior to calling the setup
function. We’ll do this using the test case setup hook called beforeEach
.
// user-agent.token.ts
import { InjectionToken } from '@angular/core';
export const userAgentToken: InjectionToken<string> =
new InjectionToken('User agent string', {
factory: (): string => navigator.userAgent,
providedIn: 'root',
});
// is-internet-explorer-11.token.ts
import { inject, InjectionToken } from '@angular/core';
import { userAgentToken } from './user-agent.token';
export const isInternetExplorer11Token: InjectionToken<boolean> =
new InjectionToken('Internet Explorer 11 flag', {
factory: (): boolean =>
/Trident\/7\.0.+rv:11\.0/.test(inject(userAgentToken)),
providedIn: 'root',
});
Putting it all together, we end up with simple test cases with very explicitly defined setup, exercise, and verification phases.
At this point, we should ask ourselves whether we feel confident enough that the deprecation banner is displayed, without testing it in an actual Internet Explorer 11 browser.
Summary
In this article, we demonstrated how to test and fake tree-shakable dependencies in an Angular project. We also tested value factories with dependencies on platform-specific APIs.
During this process, we investigated gotchas when using the inject
testing function to resolve dependencies. Using TestBed
, we resolved dependency injection tokens and explored gotchas for this approach.
We tested the Internet Explorer 11 deprecation banner in many ways, to the degree that there should barely be a need to test it in the actual browser. We faked its dependencies in its component test suite, but as we discussed, we should always test it in a real browser target for complex integration scenarios.
Explore the options that Angular’s dependency injection enable us to do during development in “Faking dependencies in Angular applications”.
Resources
The application that we used to demonstrate how to fake dependencies in Angular applications is in a StackBlitz project.
The test suite for the application which tests and also fakes Angular dependencies is in a separate StackBlitz project.
Microsoft’s Modern.IE domain has free resources for generating browser snapshots with Internet Explorer. It also offers free virtual machine images with Internet Explorer running on Windows 7 or 8.1.
Related articles
We’ll create a browser faker to test the banner component during development in “Faking dependencies in Angular applications”.
Learn how to provide tree-shakable dependencies and other complicated configurations of Angular dependency injection in “Tree-shakable dependencies in Angular projects”. This is the article that our application is based on.
Reviewers
These wonderful people from the Angular community helped review this article:
Posted on March 24, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.