天天看點

Angular 應用級别的依賴 Fake

使用 Angular 依賴注入系統的強大功能,我們可以僞造特定的用例。 這對于自動化測試很有用,但在本文中,我們将研究一種将其用于手動測試的方法。

為了讓我們的生活更輕松,我們将建立一個浏覽器僞造元件,由于自定義結構指令,該元件僅在開發模式下啟用。 為了好玩,我們将添加文本管道以在我們的元件模闆中使用常見的字元串操作。

Simulating a browser environmentDynamically replacing a dependency using a class-based service

使用者代理令牌工廠隻對每個子產品注入器評估一次,如果它沒有被祖先元件或指令提供的元素注入器替換,我們必須使用另一種技術來僞造依賴項。 我們将使用基于類的服務依賴替換依賴注入令牌依賴。

// 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);
  }
}
      

首先,我們從依賴注入令牌中提取 Internet Explorer 11 檢測到我們新建立的 InternetExplorerService 類。 Internet Explorer 11 檢測令牌現在在根據使用者代理評估其值時委托給服務。

如前所述,我們不會使用元素注入器在模闆中以聲明方式動态替換使用者代理令牌。 相反,我們将強制更改狀态。

Creating an observable state

下面展示的辦法不使用 userAgent token 的 injection token,而是使用 Observable. 這個 Observable 對象從另一個 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);
  }
}
      

browser service 實作裡,還是會使用 user agent injection token:

// 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 {

// 這展現了 Observable 和 BehaviorSubject 的差別:後者執行個體化時,需要一個初始值:

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);
  }
}
      

我們将目前使用者代理狀态存儲在 BehaviorSubject 中,它暴露在 BrowserService 的可觀察 userAgent$ 屬性中。 當整個應用程式需要使用者代理時,它應該依賴于這個 observable。

最初,behavior subject 的初始值來自使用者代理令牌的真實使用者代理字元串。 該值也被存儲以備後用,因為我們允許通過兩個指令更改浏覽器狀态。

我們公開了 fakeUserAgent 方法,該方法将使用者代理狀态設定為假使用者代理字元串。 此外,我們允許依賴者調用 stopFakingUserAgent 方法,該方法将使用者代理狀态重置為真實的使用者代理字元串。

Internet Explorer Service 現在公開一個名為 isInternetExplorer11$ 的可觀察屬性,隻要浏覽器服務的可觀察使用者代理屬性發出值,就會評估該屬性。

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.

我們現在需要的隻是讓棄用橫幅元件依賴于可觀察的 Internet Explorer 11 檢測屬性,而不是我們替換的正常屬性。

<!-- 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>
      

現在 banner 是否 visible,是由兩個 boolean 值控制了,是以使用 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);
  }
}
      

在棄用橫幅元件中,我們将 Boolean isDismissed 屬性替換為 BehaviorSubject ,該屬性最初被清除(設定為 false)。 我們現在有一個可觀察的 isBannerVisible$ 屬性,它是來自 isDismissed 和 InternetExplorerService#isInternetExplorer11$ 的可觀察狀态的組合。 UI 行為邏輯與之前類似,不同之處在于它現在表示為 observable 管道的一部分。

現在,onDismiss 事件處理程式不再為屬性配置設定布爾值,而是通過 isDismissed 行為主體發出布爾值。

此時,應用程式的行為與我們引入 Internet Explorer 服務和浏覽器服務之前的行為完全相同。 我們有浏覽器狀态更改指令,但我們需要某種機制來觸發它們。

為此,我們将開發一個浏覽器僞造器元件,使我們能夠為應用程式的其餘部分僞造浏覽器環境。

<!-- 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 {
// 一旦這個 Observable 有事件發生,說明使用者選擇了 fake browser
    this.fakeBrowserSelection$.subscribe(userAgent =>
      this.browser.fakeUserAgent(userAgent));
    this.realBrowserSelection$.subscribe(() =>
      this.browser.stopFakingUserAgent());
  }

  private unbindEvents(): void {
    this.destroy.next();
    this.destroy.complete();
  }
}
      

browser faker 元件注入浏覽器服務。 它有一個綁定到本機 select 控件的表單控件。 選擇浏覽器後,我們開始通過浏覽器服務僞造其使用者代理。 選擇預設浏覽器選項後,我們會停止僞造使用者代理。

現在我們有一個浏覽器僞造元件,但我們隻希望在開發過程中啟用它。 讓我們建立一個僅在開發模式下有條件地呈現的結構指令。

建立一個 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();
  }
}
      
Angular 應用級别的依賴 Fake

如果應用程式在開發模式下運作,則此結構指令僅呈現它所附加的元件或元素,正如其測試套件所驗證的那樣。

現在,剩下的就是将棄用橫幅和浏覽器僞裝器添加到我們的應用程式中。

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

URL: <code><browser-url></browser-url></code>
      

最後的效果:選擇 IE 11 時,出現 deprecation 提示:

Angular 應用級别的依賴 Fake

選擇其他浏覽器時,該提示消失:

Angular 應用級别的依賴 Fake

Summary

為了能夠模拟使用者環境,我們建立了一個在開發模式下有條件地呈現的浏覽器僞造元件。 我們将浏覽器狀态封裝在一個基于類的服務中,并讓應用程式依賴它。 這與浏覽器僞造者使用的服務相同。

浏覽器僞造器是在 Angular 應用程式中僞造依賴項的一個簡單示例。 我們讨論了動态配置 Angular 依賴注入機制的其他技術。

本文提到的測試程式位址:​​https://stackblitz.com/edit/testing-and-faking-angular-dependencies-app?file=src%2Fapp%2Fbrowser%2Fbrowser-faker.component.html​​

更多Jerry的原創文章,盡在:"汪子熙":

Angular 應用級别的依賴 Fake