填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

測試元件的基礎知識

Basics of testing components

元件與 Angular 應用的所有其它部分不同,它結合了 HTML 範本和 TypeScript 類別。事實上,元件就是由範本和類別一起工作的。要想對元件進行充分的測試,你應該測試它們是否如預期般協同工作。

A component, unlike all other parts of an Angular application, combines an HTML template and a TypeScript class. The component truly is the template and the class working together. To adequately test a component, you should test that they work together as intended.

這些測試需要在瀏覽器 DOM 中建立該元件的宿主元素,就像 Angular 所做的那樣,然後檢查元件類別與 DOM 的互動是否如範本中描述的那樣工作。

Such tests require creating the component's host element in the browser DOM, as Angular does, and investigating the component class's interaction with the DOM as described by its template.

Angular 的 TestBed 可以幫你做這種測試,正如你將在下面的章節中看到的那樣。但是,在很多情況下,單獨測試元件類別(不需要 DOM 的參與),就能以更簡單,更明顯的方式驗證元件的大部分行為。

The Angular TestBed facilitates this kind of testing as you'll see in the sections below. But in many cases, testing the component class alone, without DOM involvement, can validate much of the component's behavior in an easier, more obvious way.

對於本測試指南中描述的範例應用,參閱範例應用範例應用

For the sample app that the testing guides describe, see thesample appsample app.

要了解本測試指南中涉及的測試特性,請參閱teststests

For the tests features in the testing guides, seeteststests.

元件類別測試

Component class testing

你可以像測試服務類別那樣來測試一個元件類別本身。

Test a component class on its own as you would test a service class.

元件類別的測試應該保持非常乾淨和簡單。它應該只測試一個單元。一眼看上去,你就應該能夠理解正在測試的物件。

Component class testing should be kept very clean and simple. It should test only a single unit. At first glance, you should be able to understand what the test is testing.

考慮這個 LightswitchComponent,當用戶單擊該按鈕時,它會開啟和關閉一個指示燈(用螢幕上的一條訊息表示)。

Consider this LightswitchComponent which toggles a light on and off (represented by an on-screen message) when the user clicks the button.

app/demo/demo.ts (LightswitchComp)
      
      @Component({
  selector: 'lightswitch-comp',
  template: `
    <button (click)="clicked()">Click me!</button>
    <span>{{message}}</span>`
})
export class LightswitchComponent {
  isOn = false;
  clicked() { this.isOn = !this.isOn; }
  get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
    

你可能要測試 clicked() 方法是否切換了燈的開/關狀態並正確設定了這個訊息。

You might decide only to test that the clicked() method toggles the light's on/off state and sets the message appropriately.

這個元件類別沒有依賴。要測試這種型別的元件類別,請遵循與沒有依賴的服務相同的步驟:

This component class has no dependencies. To test these types of classes, follow the same steps as you would for a service that has no dependencies:

  1. 使用 new 關鍵字建立一個元件。

    Create a component using the new keyword.

  2. 呼叫它的 API。

    Poke at its API.

  3. 對其公開狀態的期望值進行斷言。

    Assert expectations on its public state.

app/demo/demo.spec.ts (Lightswitch tests)
      
      describe('LightswitchComp', () => {
  it('#clicked() should toggle #isOn', () => {
    const comp = new LightswitchComponent();
    expect(comp.isOn).toBe(false, 'off at first');
    comp.clicked();
    expect(comp.isOn).toBe(true, 'on after click');
    comp.clicked();
    expect(comp.isOn).toBe(false, 'off after second click');
  });

  it('#clicked() should set #message to "is on"', () => {
    const comp = new LightswitchComponent();
    expect(comp.message).toMatch(/is off/i, 'off at first');
    comp.clicked();
    expect(comp.message).toMatch(/is on/i, 'on after clicked');
  });
});
    

下面是“英雄之旅”課程中的 DashboardHeroComponent

Here is the DashboardHeroComponent from the Tour of Heroes tutorial.

app/dashboard/dashboard-hero.component.ts (component)
      
      export class DashboardHeroComponent {
  @Input() hero!: Hero;
  @Output() selected = new EventEmitter<Hero>();
  click() { this.selected.emit(this.hero); }
}
    

它出現在父元件的範本中,把一個英雄繫結到了 @Input 屬性,並監聽透過所選@Output 屬性引發的一個事件。

It appears within the template of a parent component, which binds a hero to the @Input property and listens for an event raised through the selected @Output property.

你可以測試類別程式碼的工作方式,而無需建立 DashboardHeroComponent 或它的父元件。

You can test that the class code works without creating the DashboardHeroComponent or its parent component.

app/dashboard/dashboard-hero.component.spec.ts (class tests)
      
      it('raises the selected event when clicked', () => {
  const comp = new DashboardHeroComponent();
  const hero: Hero = {id: 42, name: 'Test'};
  comp.hero = hero;

  comp.selected.subscribe((selectedHero: Hero) => expect(selectedHero).toBe(hero));
  comp.click();
});
    

當元件有依賴時,你可能要使用 TestBed 來同時建立該元件及其依賴。

When a component has dependencies, you may wish to use the TestBed to both create the component and its dependencies.

下列的 WelcomeComponent 依賴於 UserService 來了解要問候的使用者的名字。

The following WelcomeComponent depends on the UserService to know the name of the user to greet.

app/welcome/welcome.component.ts
      
      export class WelcomeComponent implements OnInit {
  welcome = '';
  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.welcome = this.userService.isLoggedIn ?
      'Welcome, ' + this.userService.user.name : 'Please log in.';
  }
}
    

你可以先建立一個能滿足本元件最低需求的 UserService

You might start by creating a mock of the UserService that meets the minimum needs of this component.

app/welcome/welcome.component.spec.ts (MockUserService)
      
      class MockUserService {
  isLoggedIn = true;
  user = { name: 'Test User'};
}
    

然後在 TestBed 配置中提供並注入所有這些元件服務

Then provide and inject both the component and the service in the TestBed configuration.

app/welcome/welcome.component.spec.ts (class-only setup)
      
      beforeEach(() => {
  TestBed.configureTestingModule({
    // provide the component-under-test and dependent service
    providers: [
      WelcomeComponent,
      { provide: UserService, useClass: MockUserService }
    ]
  });
  // inject both the component and the dependent service.
  comp = TestBed.inject(WelcomeComponent);
  userService = TestBed.inject(UserService);
});
    

然後,測驗元件類別,別忘了要像 Angular 執行應用時一樣呼叫生命週期鉤子方法

Then exercise the component class, remembering to call the lifecycle hook methods as Angular does when running the app.

app/welcome/welcome.component.spec.ts (class-only tests)
      
      it('should not have welcome message after construction', () => {
  expect(comp.welcome).toBe('');
});

it('should welcome logged in user after Angular calls ngOnInit', () => {
  comp.ngOnInit();
  expect(comp.welcome).toContain(userService.user.name);
});

it('should ask user to log in if not logged in after ngOnInit', () => {
  userService.isLoggedIn = false;
  comp.ngOnInit();
  expect(comp.welcome).not.toContain(userService.user.name);
  expect(comp.welcome).toContain('log in');
});
    

元件 DOM 測試

Component DOM testing

測試元件類別和測試服務一樣簡單。

Testing the component class is as easy as testing a service.

但元件不僅僅是它的類別。元件還會與 DOM 以及其他元件進行互動。只對類別的測試可以告訴你類別的行為。但它們無法告訴你這個元件是否能正確渲染、響應使用者輸入和手勢,或是整合到它的父元件和子元件中。

But a component is more than just its class. A component interacts with the DOM and with other components. The class-only tests can tell you about class behavior. They cannot tell you if the component is going to render properly, respond to user input and gestures, or integrate with its parent and child components.

以上所有只對類別的測試都不能回答有關元件會如何在螢幕上實際執行方面的關鍵問題。

None of the class-only tests above can answer key questions about how the components actually behave on screen.

  • Lightswitch.clicked() 繫結到了什麼?使用者可以呼叫它嗎?

    Is Lightswitch.clicked() bound to anything such that the user can invoke it?

  • Lightswitch.message 是否顯示過?

    Is the Lightswitch.message displayed?

  • 使用者能否真正選中由 DashboardHeroComponent 顯示的英雄?

    Can the user actually select the hero displayed by DashboardHeroComponent?

  • 英雄名字是否按預期顯示的(也就是大寫字母)?

    Is the hero name displayed as expected (i.e, in uppercase)?

  • WelcomeComponent 的範本是否顯示了歡迎資訊?

    Is the welcome message displayed by the template of WelcomeComponent?

對於上面描述的那些簡單元件來說,這些問題可能並不麻煩。但是很多元件都與範本中描述的 DOM 元素進行了複雜的互動,導致一些 HTML 會在元件狀態發生變化時出現和消失。

These may not be troubling questions for the simple components illustrated above. But many components have complex interactions with the DOM elements described in their templates, causing HTML to appear and disappear as the component state changes.

要回答這些問題,你必須建立與元件關聯的 DOM 元素,你必須檢查 DOM 以確認元件狀態是否在適當的時候正確顯示了,並且你必須模擬使用者與螢幕的互動以確定這些互動是否正確。判斷該元件的行為是否符合預期。

To answer these kinds of questions, you have to create the DOM elements associated with the components, you must examine the DOM to confirm that component state displays properly at the appropriate times, and you must simulate user interaction with the screen to determine whether those interactions cause the component to behave as expected.

為了編寫這些型別的測試,你將使用 TestBed 的其它特性以及其他的測試輔助函式。

To write these kinds of test, you'll use additional features of the TestBed as well as other testing helpers.

CLI 產生的測試

CLI-generated tests

當你要求 CLI 產生一個新元件時,它會預設為你建立一個初始的測試檔案。

The CLI creates an initial test file for you by default when you ask it to generate a new component.

比如,下列 CLI 命令會在 app/banner 資料夾中產生帶有內聯範本和內聯樣式的 BannerComponent

For example, the following CLI command generates a BannerComponent in the app/banner folder (with inline template and styles):

      
      ng generate component banner --inline-template --inline-style --module app
    

它還會產生一個初始測試檔案 banner-external.component.spec.ts,如下所示:

It also generates an initial test file for the component, banner-external.component.spec.ts, that looks like this:

app/banner/banner-external.component.spec.ts (initial)
      
      import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';

import { BannerComponent } from './banner.component';

describe('BannerComponent', () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({declarations: [BannerComponent]}).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(BannerComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeDefined();
  });
});
    

由於 compileComponents 是非同步的,所以它使用從 @angular/core/testing 中匯入的實用工具函式 waitForAsync

Because compileComponents is asynchronous, it uses the waitForAsyncutility function imported from @angular/core/testing.

欲知詳情,請參閱 waitForAsync 部分。

Please refer to the waitForAsync section for more details.

減少設定程式碼

Reduce the setup

只有這個檔案的最後三行才是真正測試元件的,並且所有這些都斷言了 Angular 可以建立該元件。

Only the last three lines of this file actually test the component and all they do is assert that Angular can create the component.

該檔案的其它部分是做設定用的樣板程式碼,可以預見,如果元件演變得更具實質性內容,就會需要更進階的測試。

The rest of the file is boilerplate setup code anticipating more advanced tests that might become necessary if the component evolves into something substantial.

下面你將學習這些高階測試特性。現在,你可以從根本上把這個測試檔案減少到一個更容易管理的大小:

You'll learn about these advanced test features below. For now, you can radically reduce this test file to a more manageable size:

app/banner/banner-initial.component.spec.ts (minimal)
      
      describe('BannerComponent (minimal)', () => {
  it('should create', () => {
    TestBed.configureTestingModule({declarations: [BannerComponent]});
    const fixture = TestBed.createComponent(BannerComponent);
    const component = fixture.componentInstance;
    expect(component).toBeDefined();
  });
});
    

在這個例子中,傳給 TestBed.configureTestingModule 的元資料物件只是聲明瞭要測試的元件 BannerComponent

In this example, the metadata object passed to TestBed.configureTestingModule simply declares BannerComponent, the component to test.

      
      TestBed.configureTestingModule({declarations: [BannerComponent]});
    

沒有必要宣告或匯入任何其他東西。預設的測試模組預先配置了像來自 @angular/platform-browserBrowserModule 這樣的東西。

There's no need to declare or import anything else. The default test module is pre-configured with something like the BrowserModule from @angular/platform-browser.

稍後你會用 importsproviders 和更多可宣告物件的引數來呼叫 TestBed.configureTestingModule(),以滿足你的測試需求。可選方法 override可以進一步微調此配置的各個方面。

Later you'll call TestBed.configureTestingModule() with imports, providers, and more declarations to suit your testing needs. Optional override methods can further fine-tune aspects of the configuration.

createComponent()

在配置好 TestBed 之後,你就可以呼叫它的 createComponent() 方法了。

After configuring TestBed, you call its createComponent() method.

      
      const fixture = TestBed.createComponent(BannerComponent);
    

TestBed.createComponent() 會建立 BannerComponent 的實例,它把一個對應元素新增到了測試執行器的 DOM 中,並返回一個ComponentFixture物件。

TestBed.createComponent() creates an instance of the BannerComponent, adds a corresponding element to the test-runner DOM, and returns a ComponentFixture.

呼叫 createComponent 後不能再重新配置 TestBed

Do not re-configure TestBed after calling createComponent.

createComponent 方法會凍結當前的 TestBed 定義,並把它關閉以防止進一步的配置。

The createComponent method freezes the current TestBed definition, closing it to further configuration.

你不能再呼叫任何 TestBed 配置方法, 無論是 configureTestingModule()get() 還是 override... 方法都不行。如果你這樣做,TestBed 會丟擲一個錯誤。

You cannot call any more TestBed configuration methods, not configureTestingModule(), nor get(), nor any of the override... methods. If you try, TestBed throws an error.

ComponentFixture

ComponentFixture 是一個測試挽具,用於與所建立的元件及其對應的元素進行互動。

The ComponentFixture is a test harness for interacting with the created component and its corresponding element.

可以透過測試夾具(fixture)訪問元件實例,並用 Jasmine 的期望斷言來確認它是否存在:

Access the component instance through the fixture and confirm it exists with a Jasmine expectation:

      
      const component = fixture.componentInstance;
expect(component).toBeDefined();
    

beforeEach()

隨著這個元件的發展,你會新增更多的測試。你不必為每個測試複製 TestBed 的配置程式碼,而是把它重構到 Jasmine 的 beforeEach() 和一些支援變數中:

You will add more tests as this component evolves. Rather than duplicate the TestBed configuration for each test, you refactor to pull the setup into a Jasmine beforeEach() and some supporting variables:

      
      describe('BannerComponent (with beforeEach)', () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({declarations: [BannerComponent]});
    fixture = TestBed.createComponent(BannerComponent);
    component = fixture.componentInstance;
  });

  it('should create', () => {
    expect(component).toBeDefined();
  });
});
    

現在新增一個測試程式,它從 fixture.nativeElement 中獲取元件的元素,並查詢預期的文字。

Now add a test that gets the component's element from fixture.nativeElement and looks for the expected text.

      
      it('should contain "banner works!"', () => {
  const bannerElement: HTMLElement = fixture.nativeElement;
  expect(bannerElement.textContent).toContain('banner works!');
});
    

nativeElement

ComponentFixture.nativeElement 的值是 any 型別的。稍後你會遇到 DebugElement.nativeElement,它也是 any 型別的。

The value of ComponentFixture.nativeElement has the any type. Later you'll encounter the DebugElement.nativeElement and it too has the any type.

Angular 在編譯時不知道 nativeElement 是什麼樣的 HTML 元素,甚至可能不是 HTML 元素。該應用可能執行在非瀏覽器平臺(如伺服器或 Web Worker)上,在那裡本元素可能具有一個縮小版的 API,甚至根本不存在。

Angular can't know at compile time what kind of HTML element the nativeElement is or if it even is an HTML element. The app might be running on a non-browser platform, such as the server or a Web Worker, where the element may have a diminished API or not exist at all.

本指南中的測試都是為了在瀏覽器中執行而設計的,因此 nativeElement 的值始終是 HTMLElement 或其派生類別之一。

The tests in this guide are designed to run in a browser so a nativeElement value will always be an HTMLElement or one of its derived classes.

知道了它是某種 HTMLElement ,你就可以使用標準的 HTML querySelector 深入瞭解元素樹。

Knowing that it is an HTMLElement of some sort, you can use the standard HTML querySelector to dive deeper into the element tree.

這是另一個呼叫 HTMLElement.querySelector 來獲取段落元素並查詢橫幅文字的測試:

Here's another test that calls HTMLElement.querySelector to get the paragraph element and look for the banner text:

      
      it('should have <p> with "banner works!"', () => {
  const bannerElement: HTMLElement = fixture.nativeElement;
  const p = bannerElement.querySelector('p')!;
  expect(p.textContent).toEqual('banner works!');
});
    

DebugElement

Angular 的測試夾具可以直接透過 fixture.nativeElement 提供元件的元素。

The Angular fixture provides the component's element directly through the fixture.nativeElement.

      
      const bannerElement: HTMLElement = fixture.nativeElement;
    

它實際上是一個便利方法,其最終實現為 fixture.debugElement.nativeElement

This is actually a convenience method, implemented as fixture.debugElement.nativeElement.

      
      const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
    

使用這種迂迴的路徑訪問元素是有充分理由的。

There's a good reason for this circuitous path to the element.

nativeElement 的屬性依賴於其執行時環境。你可以在非瀏覽器平臺上執行這些測試,那些平臺上可能沒有 DOM,或者其模擬的 DOM 不支援完整的 HTMLElement API。

The properties of the nativeElement depend upon the runtime environment. You could be running these tests on a non-browser platform that doesn't have a DOM or whose DOM-emulation doesn't support the full HTMLElement API.

Angular 依靠 DebugElement 抽象來在其支援的所有平臺上安全地工作。 Angular 不會建立 HTML 元素樹,而會建立一個 DebugElement 樹來封裝執行時平臺上的原生元素nativeElement 屬性會解開封裝 DebugElement 並返回特定於平臺的元素物件。

Angular relies on the DebugElement abstraction to work safely across all supported platforms. Instead of creating an HTML element tree, Angular creates a DebugElement tree that wraps the native elements for the runtime platform. The nativeElement property unwraps the DebugElement and returns the platform-specific element object.

由於本指南的範例測試只能在瀏覽器中執行,因此 nativeElement 在這些測試中始終是 HTMLElement ,你可以在測試中探索熟悉的方法和屬性。

Because the sample tests for this guide are designed to run only in a browser, a nativeElement in these tests is always an HTMLElement whose familiar methods and properties you can explore within a test.

下面是把前述測試用 fixture.debugElement.nativeElement 重新實現的版本:

Here's the previous test, re-implemented with fixture.debugElement.nativeElement:

      
      it('should find the <p> with fixture.debugElement.nativeElement)', () => {
  const bannerDe: DebugElement = fixture.debugElement;
  const bannerEl: HTMLElement = bannerDe.nativeElement;
  const p = bannerEl.querySelector('p')!;
  expect(p.textContent).toEqual('banner works!');
});
    

這些 DebugElement 還有另一些在測試中很有用的方法和屬性,你可以在本指南的其他地方看到。

The DebugElement has other methods and properties that are useful in tests, as you'll see elsewhere in this guide.

你可以從 Angular 的 core 函式庫中匯入 DebugElement 符號。

You import the DebugElement symbol from the Angular core library.

      
      import { DebugElement } from '@angular/core';
    

By.css()

雖然本指南中的測試都是在瀏覽器中執行的,但有些應用可能至少要在某些時候執行在不同的平臺上。

Although the tests in this guide all run in the browser, some apps might run on a different platform at least some of the time.

例如,作為優化策略的一部分,該元件可能會首先在伺服器上渲染,以便在連線不良的裝置上更快地啟動本應用。伺服器端渲染器可能不支援完整的 HTML 元素 API。如果它不支援 querySelector,之前的測試就會失敗。

For example, the component might render first on the server as part of a strategy to make the application launch faster on poorly connected devices. The server-side renderer might not support the full HTML element API. If it doesn't support querySelector, the previous test could fail.

DebugElement 提供了適用於其支援的所有平臺的查詢方法。這些查詢方法接受一個謂詞函式,當 DebugElement 樹中的一個節點與選擇條件匹配時,該函式返回 true

The DebugElement offers query methods that work for all supported platforms. These query methods take a predicate function that returns true when a node in the DebugElement tree matches the selection criteria.

你可以藉助從函式庫中為執行時平臺匯入 By 類別來建立一個謂詞。這裡的 By 是從瀏覽器平臺匯入的。

You create a predicate with the help of a By class imported from a library for the runtime platform. Here's the By import for the browser platform:

      
      import { By } from '@angular/platform-browser';
    

下面的例子用 DebugElement.query() 和瀏覽器的 By.css 方法重新實現了前面的測試。

The following example re-implements the previous test with DebugElement.query() and the browser's By.css method.

      
      it('should find the <p> with fixture.debugElement.query(By.css)', () => {
  const bannerDe: DebugElement = fixture.debugElement;
  const paragraphDe = bannerDe.query(By.css('p'));
  const p: HTMLElement = paragraphDe.nativeElement;
  expect(p.textContent).toEqual('banner works!');
});
    

一些值得注意的地方:

Some noteworthy observations:

當你使用 CSS 選擇器進行過濾並且只測試瀏覽器原生元素的屬性時,用 By.css 方法可能會有點過度。

When you're filtering by CSS selector and only testing properties of a browser's native element, the By.css approach may be overkill.

HTMLElement 方法(比如 querySelector()querySelectorAll())進行過濾通常更簡單,更清晰。

It's often easier and more clear to filter with a standard HTMLElement method such as querySelector() or querySelectorAll().