跳转至

Angular 测试

此文件使用 Angular 特定内容扩展 common/testing.md。

测试运行器

请使用项目中配置的测试运行器。请检查 angular.json 和 package.json。Angular 项目通常使用 Vitest、Jest 或 Jasmine + Karma。

ng test               # ウォッチモード
ng test --no-watch    # CI モード

TestBed 设置

对于独立组件,直接导入该组件。对于具有外部模板的组件,请调用 compileComponents()。

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserCardComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(UserCardComponent);
  });
});

信号输入

基于信号的输入通过 fixture.componentRef.setInput() 设置:

fixture.componentRef.setInput('user', mockUser);
fixture.detectChanges();

组件 Harness

对于 UI 交互,请优先使用 Angular CDK 组件 Harness,而不是直接 DOM 查询。Harness 对标记变更更具容错性。

import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';

let loader: HarnessLoader;

beforeEach(() => {
  loader = TestbedHarnessEnvironment.loader(fixture);
});

it('triggers save on button click', async () => {
  const button = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
  await button.click();
  expect(saveSpy).toHaveBeenCalled();
});

路由测试

对于依赖路由的组件,请使用 RouterTestingHarness:

import { RouterTestingHarness } from '@angular/router/testing';

it('renders user on navigation', async () => {
  const harness = await RouterTestingHarness.create();
  const component = await harness.navigateByUrl('/users/1', UserDetailComponent);
  expect(component.userId()).toBe('1');
});

异步测试

对于受控异步,请使用 fakeAsync + tick。对于真实异步,请使用 waitForAsync 和 fixture.whenStable()。

it('loads user after delay', fakeAsync(() => {
  const service = TestBed.inject(UserService);
  vi.spyOn(service, 'getUser').mockReturnValue(of(mockUser));

  fixture.detectChanges();
  tick();
  fixture.detectChanges();

  expect(fixture.nativeElement.querySelector('.name').textContent).toBe(mockUser.name);
}));

HTTP 测试

import { provideHttpClientTesting } from '@angular/common/http/testing';
import { HttpTestingController } from '@angular/common/http/testing';

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [provideHttpClient(), provideHttpClientTesting()],
  });
  httpMock = TestBed.inject(HttpTestingController);
});

afterEach(() => httpMock.verify());

服务测试

在没有组件 fixture 的情况下直接注入服务:

describe('UserService', () => {
  let service: UserService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    service = TestBed.inject(UserService);
  });
});

测试对象

  • 服务:所有公共方法、错误路径、HTTP 交互
  • 组件:输入/输出绑定、主要状态的渲染结果、使用 Harness 的用户交互
  • 管道:纯转换 — 无需 TestBed 的普通单元测试
  • 守卫/解析器:使用 RouterTestingHarness 的允许和拒绝状态返回值

E2E 测试

对于关键用户流程,请使用项目中配置的 E2E 框架,例如 Cypress 或 Playwright。

describe('Login flow', () => {
  it('redirects to dashboard on valid credentials', () => {
    cy.visit('/login');
    cy.get('[data-cy=email]').type('user@example.com');
    cy.get('[data-cy=password]').type('password123');
    cy.get('[data-cy=submit]').click();
    cy.url().should('include', '/dashboard');
  });
});
  • 为可交互元素添加 data-cy 属性,以获得稳定的选择器
  • 在 E2E 测试中,选择器不要依赖 CSS 类或文本内容

覆盖率

服务和管道的覆盖率目标应为 80% 以上。组件:请测试行为,而不是实现细节。

技能参考

有关全面的测试模式、Harness 用法和异步最佳实践,请参阅技能:angular-developer。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC