Faking dependencies in Angular Applications

With the power of the Angular dependency injection system, you can fake specific use cases. This is useful for automated testing, but in this article, we’ll look at a way to use it for manual testing.

To make our lives easier, we will create a browser counterfeit component that is only enabled in development mode due to custom structure directives. Just for fun, we’ll add text pipes to use common string operations in our component template.

Simulating a browser environment

Dynamically replacing a dependency using a class-based service

The user agent token factory is evaluated only once for each module injector, and if it is not replaced by an element injector provided by an ancestor component or directive, we must use another technique to forge dependencies. We will replace dependency injection token dependencies with class-based service dependencies.

// internet-explorer-11-banner.component.ts
import { Component } from '@angular/core';

import { InternetExplorerService } from './internet-explorer.service';

@Component({
  selector: 'internet-explorer-11-banner',
  templateUrl: './internet-explorer-11-banner.component.html',
})
export class InternetExplorer11BannerComponent {
  private isDismissed = false;

  get isBannerVisible() {
    return this.internetExplorer.isInternetExplorer11State && !this.isDismissed;
  }

  constructor(
    private internetExplorer: InternetExplorerService,
  ) {}

  onDismiss() {
    this.isDismissed = true;
  }
}
// internet-explorer-service.ts import { Inject, Injectable } from '@angular/core'; import { userAgentToken } from './user-agent.token'; @Injectable({ providedIn: 'root', }) export class InternetExplorerService { get isInternetExplorer11State(): boolean { return this.isInternetExplorer11(this.userAgent); } constructor( @Inject(userAgentToken) private userAgent: string, ) {} isInternetExplorer11(userAgent: string): boolean { return /Trident\/7\.0.+rv:11\.0/.test(userAgent); }}

First, we extract InternetExplorer 11 from the dependency injection token to detect our newly created InternetExplorerService class. The Internet Explorer 11 detection token is now delegated to the service when its value is evaluated against the user agent.

As mentioned earlier, we will not use the element injector to dynamically replace the user agent token declaratively in the template. Instead, we will force a change of state.

Creating an observable state

Instead of using the injection token of the userAgent token, the method shown below uses an Observable. This Observable is retrieved from another Browser service.

// internet-explorer.service.ts import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { BrowserService } from './browser.service'; @Injectable({ providedIn: 'root', }) export class InternetExplorerService { isInternetExplorer11$: Observable<boolean> = this.browser.userAgent$.pipe( map(userAgent => this.isInternetExplorer11(userAgent)), ); constructor( private browser: BrowserService, ) {} isInternetExplorer11(userAgent: string): boolean { return /Trident\/7\.0.+rv:11\.0/.test(userAgent); }}

In browser service implementation, user Agent injection token is used:

// browser.service.ts import { Inject, Injectable, OnDestroy } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { distinctUntilChanged } from 'rxjs/operators'; import { FakeUserAgent } from './fake-user-agent'; import { userAgentToken } from './user-agent.token'; @Injectable({ providedIn: 'root',}) export class BrowserService Implements OnDestroy {// This is the difference between An Observable and a BehaviorSubject: The latter instantiates a BehaviorSubject that requires an initial value:  private userAgent = new BehaviorSubject(this.realUserAgent); userAgent$ = this.userAgent.pipe( distinctUntilChanged(), ); constructor( @Inject(userAgentToken) private realUserAgent: string, ) {} ngOnDestroy() { this.userAgent.complete(); } fakeUserAgent(value: FakeUserAgent) { this.userAgent.next(FakeUserAgent[value]); } stopFakingUserAgent() { this.userAgent.next(this.realUserAgent); }}

We store the current userAgent state in BehaviorSubject<string>, which is exposed in the observable userAgent$property of BrowserService. When the entire application needs a user agent, it should rely on this Observable.

Initially, the initial value of the Behavior Subject comes from the real user agent string of the user agent token. This value is also stored for later use, because we allow two commands to change the browser state.

We exposed the fakeUserAgent method, which sets the user agent state to the fakeUserAgent string. In addition, we allow the dependency to call the stopFakingUserAgent method, which resets the user agent state to the real user agent string.

InternetExplorer Service now exposes an observable property called isInternetExplorer11$that is evaluated whenever the browser Service’s observable user agent property emits a value.

The Internet Explorer service now exposes an observable property called isInternetExplorer11$ which is evaluated whenever the observable user agent property of the browser service emits a value.

All we need now is for the deprecated banner component to rely on the observable Internet Explorer 11 detection properties, rather than the regular properties we replaced.

<! -- internet-explorer-11-banner.component.html --> <aside *ngIf="isBannerVisible$ | async"> Sorry, we will not continue to support Internet Explorer 11.<br /> Please upgrade to Microsoft Edge.<br /> <button (click)="onDismiss()"> Dismiss </button> </aside>

Now whether the banner is visible is controlled by two Boolean values, so use the combineLatest.

// internet-explorer-11-banner.component.ts import { Component } from '@angular/core'; import { BehaviorSubject, combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; import { InternetExplorerService } from './internet-explorer.service'; @Component({ host: { style: 'display: block; ' }, selector: 'internet-explorer-11-banner', templateUrl: './internet-explorer-11-banner.component.html', }) export class InternetExplorer11BannerComponent { private isDismissed = new BehaviorSubject(false); isBannerVisible$ = combineLatest( this.internetExplorer.isInternetExplorer11$, this.isDismissed, ).pipe( map(([isInternetExplorer11, isDismissed]) => isInternetExplorer11 && ! isDismissed), ); constructor( private internetExplorer: InternetExplorerService, ) {} onDismiss(): void { this.isDismissed.next(true); }}

In the deprecated banner component, we replace the Boolean isDismissed property BehaviorSubject< Boolean >, which is initially cleared (set to false). We now have an observable isBannerVisible$property, which is a combination of the observable states from isDismissed and InternetExplorerService#isInternetExplorer11$. The UI behavior logic is similar to before, except that it is now represented as part of the Observable pipeline.

Instead of assigning a Boolean value to the attribute, the onDismiss event handler now emits a Boolean through the isDismissed agent.

At this point, the application behaves exactly as it did before we introduced Internet Explorer services and browser services. We have browser state change commands, but we need some mechanism to trigger them.

To do this, we’ll develop a browser forger component that allows us to fake the browser environment for the rest of the application.

<! -- browser-faker.component.html --> <label> Fake a browser <select [formControl]="selectedBrowser"> <option value=""> My  browser </option> <option *ngFor="let browser of browsers" [value]="browser"> {{browser | replace:wordStartPattern:' $&' | trim}} </option> </select> </label>
// browser-faker.component.ts import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { Observable, Subject } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; import { BrowserService } from './browser.service'; import { FakeUserAgent } from './fake-user-agent'; @Component({ host: { style: 'display: block; ' }, selector: 'browser-faker', templateUrl: './browser-faker.component.html', }) export class BrowserFakerComponent implements OnDestroy, OnInit { private defaultOptionValue = ''; private destroy = new Subject<void>(); private fakeBrowserSelection$: Observable<FakeUserAgent>; private realBrowserSelection$: Observable<void>; browsers = Object.keys(FakeUserAgent); selectedBrowser = new FormControl(this.defaultOptionValue); wordStartPattern = /[A-Z]|\d+/g; constructor( private browser: BrowserService, ) { this.realBrowserSelection$ = this.selectedBrowser.valueChanges.pipe( filter(value => value === this.defaultOptionValue), takeUntil(this.destroy), ); this.fakeBrowserSelection$ = this.selectedBrowser.valueChanges.pipe( filter(value => value ! == this.defaultOptionValue), takeUntil(this.destroy), ); } ngOnInit(): void { this.bindEvents(); } ngOnDestroy() { this.unbindEvents(); } private bindEvents(): Void {// Once an Observable event occurs, Illustrate the user selects a fake browser enclosing fakeBrowserSelection $. The subscribe (userAgent = > enclosing the fakeUserAgent (userAgent)); this.realBrowserSelection$.subscribe(() => this.browser.stopFakingUserAgent()); } private unbindEvents(): void { this.destroy.next(); this.destroy.complete(); }}

The Browser Faker component is injected into the browser service. It has a form control that is bound to the native SELECT control. After selecting a browser, we begin to forge its user agent through the browser service. After selecting the default browser option, we will stop forgery of the user agent.

Now we have a browser forgery component, but we only want to enable it during development. Let’s create a structure directive that conditionally renders only in development mode.

Create an injection token:

// is-development-mode.token.ts
import { InjectionToken, isDevMode } from '@angular/core';

export const isDevelopmentModeToken: InjectionToken<boolean> =
  new InjectionToken('Development mode flag', {
    factory: (): boolean => isDevMode(),
    providedIn: 'root',
  });
// development-only.directive.ts import { Directive, Inject, OnDestroy, OnInit, TemplateRef, ViewContainerRef, } from '@angular/core'; import { isDevelopmentModeToken } from './is-development-mode.token'; @Directive({ exportAs: 'developmentOnly', selector: '[developmentOnly]', }) export class DevelopmentOnlyDirective implements OnDestroy, OnInit { private get isEnabled(): boolean { return this.isDevelopmentMode; } constructor( private container: ViewContainerRef, private template: TemplateRef<any>, @Inject(isDevelopmentModeToken) private isDevelopmentMode: boolean, ) {} ngOnInit(): void { if (this.isEnabled) { this.createAndAttachView(); } } ngOnDestroy(): void { this.destroyView(); } private createAndAttachView(): void { this.container.createEmbeddedView(this.template); } private destroyView(): void { this.container.clear(); }}

If the application is running in development mode, this structure directive renders only the components or elements it is attached to, as validated by its test suite.

Now all that remains is to add the deprecated banner and browser masker to our application.

<! -- app.component.html --> <browser-faker *developmentOnly></browser-faker> <internet-explorer-11-banner></internet-explorer-11-banner> URL: <code><browser-url></browser-url></code>

The final effect: When you select IE 11, you will see a deprecation message:

The prompt disappears when you select another browser:

Summary

To be able to simulate the user environment, we created a browser counterfeit component that conditionally renders in development mode. We encapsulate browser state in a class-based service and make the application depend on it. This is the same service used by browser forgers.

The browser forger is a simple example of forging dependencies in an Angular application. We discussed other techniques for dynamically configuring Angular dependency injection.

This article mentioned the test program address: https://stackblitz.com/edit/t…

More of Jerry’s original articles can be found in “Wang Zixi “: