天天看點

Angular 依賴的測試和 Fake

依賴注入是 Angular 的一個關鍵特性。這種靈活的方法使我們的可聲明和基于類的服務更容易隔離測試。

可搖樹依賴項移除了間接層 即Angular 子產品,但我們如何測試它們的可搖樹 provider?我們将測試依賴于特定平台 API 的注入令牌的值工廠。

某些元件具有特定于浏覽器的功能。我們将一起測試通知使用者我們将終止 Internet Explorer 11 支援的橫幅。一個合适的測試套件可以給我們足夠的信心,我們甚至不必在 Internet Explorer 11 中測試橫幅。

我們必須小心不要對複雜的內建場景過于自信。我們應該始終確定在盡可能接近生産的環境中執行 QA(品質保證)測試。這意味着在真實 Internet Explorer 11 浏覽器中運作應用程式。

Angular 測試實用程式使我們能夠僞造依賴項以進行測試。我們将使用 Angular CLI 的測試架構 Jasmine 探索在 Angular 測試環境中配置和解決依賴關系的不同選項。

通過示例,我們将探索元件 fixtures、元件初始化、自定義 expectations、模拟事件。我們甚至會為非常精簡但明确的測試用例建立自定義測試工具。

Faking dependency injection tokens used in token providers

看個例子。

我們建立了一個依賴注入令牌,該令牌評估為訓示目前浏覽器是否為 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',
  });
      
// 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',
  });
      

為了單獨測試 Internet Explorer 11 标志提供程式,我們可以用一個假值替換 userAgentToken。

我們注意到使用者代理字元串提供程式從特定于平台的 Navigator API 中提取相關資訊。 為了學習,假設我們将需要來自同一個全局導航器對象的其他資訊。 根據我們使用的測試運作器,Navigator API 甚至可能在測試環境中不可用。

為了能夠建立虛假的導航器配置,我們為導航器 API 建立了一個依賴注入令牌。 我們可以在開發和測試期間使用這些虛假配置來模拟使用者上下文。

// 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',
  });
      
// navigator.token.ts
import { InjectionToken } from '@angular/core';

export const navigatorToken: InjectionToken<Navigator> =
  new InjectionToken('Navigator API', {
    factory: (): Navigator => navigator,
    providedIn: 'root',
  });
      

對于我們的第一個測試,我們将為 Navigator API 令牌提供一個假值,該令牌在工廠提供程式中用作使用者代理字元串令牌的依賴項。

為了出于測試目的替換令牌提供程式,我們在 Angular 測試子產品中添加了一個覆寫提供程式,類似于 Angular 子產品自己的提供程式如何覆寫導入的 Angular 子產品的提供程式。

// navigator-api.spec.ts
import { inject, TestBed } from '@angular/core/testing';

import { navigatorToken } from './navigator.token';
import { userAgentToken } from './user-agent.token';

describe('Navigator API', () => {
  describe('User agent string', () => {
    describe('Provider', () => {
      beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [
            {
              provide: navigatorToken,
              useValue: {
                userAgent: 'Fake browser',
              },
            },
          ],
        });
      });

      it(
        'extracts the user agent string from the Navigator API token',
        inject([userAgentToken], (userAgent: string) => {
          expect(userAgent).toBe('Fake browser');
        }));
    });
  });
});
      

請注意,雖然我們正在測試的是 user agent 令牌及其提供者,但我們正在用假值替換 navigator 令牌依賴項。

Resolving dependencies using the inject function

Angular 測試實用程式為我們提供了不止一種解決依賴關系的方法。 在這個測試中,我們使用@angular/core/testing 包中的 ​​inject​​ 函數(*不是@angular/core 中的那個)。

注入函數允許我們通過在我們作為參數傳遞的數組中列出它們的标記來解決多個依賴項。 每個依賴注入令牌都被解析并作為參數提供給測試用例函數。

例子:​​https://stackblitz.com/edit/testing-and-faking-angular-dependencies?file=src%2Fapp%2Finternet-explorer%2Finternet-explorer-11-banner.component.spec.ts​​

Gotchas when using the Angular testing function inject

當我們使用沒有聲明的 Angular 測試子產品時,即使在同一個測試用例中,我們通常也可以多次覆寫 provider. 我們将在本文後面研究一個例子。

值得注意的是,在使用 Angular 測試功能 ​​inject​​ 時,情況并非如此。 它在執行測試用例函數體之前解決依賴關系。

我們可以使用靜态方法 TestBed.configureTestingModule 和 TestBed.overrideProvider 替換 beforeAll 和 beforeEach 鈎子中的令牌提供者。 但是當我們使用注入測試功能來解決依賴關系時,我們不能在測試用例之間改變提供者或在測試用例期間替換它。

在沒有 declarables 的測試中解決 Angular 依賴關系的一種更靈活的方法是使用靜态方法 TestBed.get。 我們隻需從測試用例函數或測試生命周期鈎子的任何地方傳遞我們想要解析的依賴注入令牌。

讓我們看另一個原生浏覽器 API 示例,我們使用依賴注入令牌對其進行抽象,以進行開發和測試。

Location 依賴于 Document:

// 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',
  });
      
Angular 依賴的測試和 Fake
// location-api.spec.ts
import { DOCUMENT } from '@angular/common';
import { TestBed } from '@angular/core/testing';

import { locationToken } from './location.token';

describe('Location API', () => {
  describe('Provider', () => {
    it('extracts the location from the DOCUMENT token', () => {
      TestBed.configureTestingModule({
        providers: [
          {
            provide: DOCUMENT,
            useValue: {
              location: {
                href: 'Fake URL',
              },
            },
          },
        ],
      });

      const location: Location = TestBed.get(locationToken);

      expect(location.href).toBe('Fake URL');
    });
  });
});
      

我們通過使用靜态 TestBed.get 方法使 Angular 依賴注入系統解析 Location API。 正如 StackBlitz 測試項目中所證明的那樣,文檔令牌被成功僞造并用于使用其真實的工廠提供程式來解析被測令牌。

Gotchas when resolving dependencies using TestBed

在之前的測試中,我們通過在 Angular 測試子產品中為 DOCUMENT 令牌提供文檔來将文檔替換為假對象。 如果我們沒有這樣做,Angular 就會提供全局文檔對象。

此外,如果我們想測試不同的文檔配置,如果我們沒有為文檔令牌建立 test provider,我們将無法這樣做。

在我們使用 TestBed.configureTestingModule 添加測試提供程式的情況下,我們可以使用靜态方法 TestBed.overrideProvider 在各種測試用例中将其替換為不同的假值。 在測試 Internet Explorer 11 檢測和 Internet Explorer 11 橫幅元件時,我們将使用此技術建立測試工具。

請注意,這是唯一可能的,因為我們不使用 declarable。 一旦我們調用 TestBed.createComponent,Angular 測試平台的依賴就被鎖定了。

Testing value factories with dependencies

在本文的第一部分中,我們介紹了一個在其提供程式中帶有值工廠的令牌。 值工廠評估使用者代理字元串是否代表 Internet Explorer 11 浏覽器。

為了測試值工廠中的浏覽器檢測,我們從真實浏覽器中收集了一些使用者代理字元串并将它們放在一個枚舉中。

// fake-user-agent.ts
export enum FakeUserAgent {
  Chrome = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
  InternetExplorer10 = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)',
  InternetExplorer11 = 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; rv:11.0) like Gecko',
  Firefox = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0',
}
      

在 Internet Explorer 11 檢測測試套件中,我們将幾乎孤立地測試 isInternetExplorer11Token。 但真正的業務邏輯價值在于它的工廠提供者,它依賴于使用者代理令牌。

使用者代理令牌從 Navigator API 令牌中提取其值,但 Navigator API 測試套件已涵蓋該依賴項。 我們将選擇使用者代理令牌作為依賴鍊中合适的位置來開始僞造依賴。

// internet-explorer-11-detection.spec.ts
import { TestBed } from '@angular/core/testing';

import { isInternetExplorer11Token } from './is-internet-explorer-11.token';
import { FakeUserAgent } from './fake-user-agent';
import { userAgentToken } from './user-agent.token';

describe('Internet Explorer 11 detection', () => {
  function setup({ userAgent }: { userAgent: string }) {
    TestBed.overrideProvider(userAgentToken, { useValue: userAgent });

    return {
      isInternetExplorer11: TestBed.get(isInternetExplorer11Token),
    };
  }

  const nonInternetExplorerUserAgents: ReadonlyArray<string> =
    Object.entries(FakeUserAgent)
      .filter(([browser]) =>
        !browser.toLowerCase().includes('internetexplorer'))
      .map(([_browser, userAgent]) => userAgent);

  it('accepts an Internet Explorer 11 user agent', () => {
    const { isInternetExplorer11 } = setup({
      userAgent: FakeUserAgent.InternetExplorer11,
    });

    expect(isInternetExplorer11).toBe(true);
  });

  it('rejects an Internet Explorer 10 user agent', () => {
    const { isInternetExplorer11 } = setup({
      userAgent: FakeUserAgent.InternetExplorer10,
    });

    expect(isInternetExplorer11).toBe(false);
  });

  it('rejects other user agents', () => {
    nonInternetExplorerUserAgents.forEach(userAgent => {
      const { isInternetExplorer11 } = setup({ userAgent });

      expect(isInternetExplorer11).toBe(
        false,
        `Expected to reject user agent: "${userAgent}"`);
    });
  });
});
      

在指定測試用例之前,我們建立了一個測試設定函數,并從我們的假使用者代理字元串中減少了一組非 Internet Explorer 使用者代理字元串。

測試設定函數采用使用者代理并使用它來僞造使用者代理令牌提供者。然後我們傳回一個具有屬性 isInternetExplorer11 的對象,該對象具有通過 TestBed.get 方法從 isInternetExplorer11Token 評估的值。

讓我們先測試一下快樂路徑。我們傳遞 Internet Explorer 11 使用者代理字元串,并期望被測令牌通過 Angular 的依賴注入系統評估為 true。正如 StackBlitz 測試項目中所見,浏覽器檢測按預期工作。

當使用者使用 Internet Explorer 10 浏覽時會發生什麼?我們的測試套件表明 Internet Explorer 11 在這種情況下不會導緻誤報。

換句話說,當依賴令牌中提供 Internet Explorer 10 使用者代理字元串時,被測令牌評估為 false。如果這不是預期用途,我們需要更改檢測邏輯。現在我們已經進行了測試,很容易證明該更改何時會成功。

最後的測試在 FakeUserAgent 枚舉定義的非 Internet Explorer 浏覽器上執行浏覽器檢測。測試用例周遊使用者代理字元串,僞造使用者代理提供程式,評估 isInternetExplorer11Token 并期望其值為 false。如果不是這種情況,測試運作程式會顯示有用的錯誤消息。

Faking dependencies in component tests

現在我們對 Internet Explorer 11 浏覽器檢測感到滿意,建立和顯示棄用橫幅很簡單。

<!-- 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>
      
// internet-explorer-11-banner.component.ts
import { Component, Inject } from '@angular/core';

import { isInternetExplorer11Token } from './is-internet-explorer-11.token';

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

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

  constructor(
    @Inject(isInternetExplorer11Token) private isInternetExplorer11: boolean,
  ) {}

  onDismiss() {
    this.isDismissed = true;
  }
}
      

解除狀态隻是作為本地 UI 狀态存儲在私有元件屬性中,該屬性由計算屬性 isBannerVisible 使用。

橫幅元件有一個依賴項——isInternetExplorer11Token,它被評估為一個布爾值。 由于 Inject 裝飾器,這個布爾值是通過橫幅元件構造函數注入的。

Summary

在本文中,我們示範了如何在 Angular 項目中測試和僞造 tree-shakable 依賴項。 我們還測試了依賴于平台特定 API 的價值工廠。

在此過程中,我們調查了使用注入測試功能解決依賴項時的問題。 使用 TestBed,我們解決了依賴注入令牌并探索了這種方法的陷阱。

我們以多種方式測試了 Internet Explorer 11 棄用橫幅,以至于幾乎不需要在實際浏覽器中對其進行測試。 我們在它的元件測試套件中僞造了它的依賴項,但正如我們所讨論的,我們應該始終在複雜的內建場景的真實浏覽器目标中測試它。