测试
Angular CLI 默认使用 Karma + Jasmine 运行单元测试;也可按团队规范迁移 Jest 或 Vitest(需自行配置,非 CLI 默认)。
核心概念
Angular 的测试体系围绕 TestBed 展开:它会创建一个隔离的、临时的「测试用 Angular 模块」,把待测组件、依赖的服务/管道/指令都注册进去,然后生成一个 ComponentFixture(组件的测试宿主),提供访问组件实例、DOM 节点、触发变更检测的能力。理解 TestBed 的生命周期是写好 Angular 单测的基础。
默认单元测试(TestBed)
import { TestBed } from "@angular/core/testing";
import { CounterComponent } from "./counter.component";
describe("CounterComponent", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent],
}).compileComponents();
});
it("点击后计数增加", () => {
const fixture = TestBed.createComponent(CounterComponent);
fixture.detectChanges();
const button: HTMLButtonElement = fixture.nativeElement.querySelector("button")!;
button.click();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain("1");
});
});
TestBed:创建隔离的 Angular 测试模块。fixture.detectChanges():触发变更检测,更新 DOM。每次修改了会影响视图的数据后都要重新调用,否则断言读到的是旧 DOM。
服务的单元测试
服务通常不依赖 DOM,可以直接 new 或通过 TestBed.inject 获取,测试更轻量:
describe("CartService", () => {
let service: CartService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CartService);
});
it("add 后 total 正确计算", () => {
service.add({ id: "1", price: 10 } as CartItem);
service.add({ id: "2", price: 20 } as CartItem);
expect(service.total()).toBe(30);
});
});
模拟依赖
TestBed.configureTestingModule({
imports: [UserListComponent],
providers: [{ provide: UserApi, useValue: { list: () => of([{ id: 1, name: "Test" }]) } }],
});
对于依赖 HttpClient 的服务,推荐使用官方提供的 HttpClientTestingModule(或新版 provideHttpClientTesting)拦截真实请求:
import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing";
import { provideHttpClient } from "@angular/common/http";
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});
const httpMock = TestBed.inject(HttpTestingController);
it("请求了正确的接口", () => {
service.getUser(1).subscribe();
const req = httpMock.expectOne("/api/users/1");
req.flush({ id: 1, name: "Test" });
httpMock.verify(); // 确保没有未处理的请求
});
异步与信号测试
import { fakeAsync, tick, waitForAsync } from "@angular/core/testing";
it("debounce 后才发起搜索", fakeAsync(() => {
component.onInput("abc");
tick(300); // 快进虚拟时钟 300ms,触发 debounceTime
expect(component.results().length).toBeGreaterThan(0);
}));
it("异步初始化完成后渲染数据", waitForAsync(async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain("加载完成");
}));
对基于 Signal 的组件,测试通常更简单:直接调用 signal.set() 后 detectChanges(),无需处理 Zone/Observable 的异步细节。
端到端测试
CLI 可选用 Playwright 或 Cypress 脚手架(ng e2e 相关方案以当前 Angular 测试指南 为准):
ng add @playwright/test # 或社区/官方推荐的当前方案
npx playwright test
// e2e 示例(Playwright)
test("登录后跳转到首页", async ({ page }) => {
await page.goto("/login");
await page.fill('input[name="email"]', "a@b.com");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/home");
});
建议
- 测 行为与 DOM 结果,而非私有字段。
- 异步使用
fakeAsync+tick或waitForAsync/async工具。
最佳实践
- 优先测「用户能看到/操作的行为」(点击按钮后文案变化),少测组件私有实现细节(内部变量、私有方法)。
- HTTP 依赖统一走
HttpTestingController模拟,不要在单测里发真实网络请求。 - 组件测试关注渲染结果与交互结果,业务逻辑复杂的部分尽量抽到独立的、更容易测的 Service/纯函数中。
- CI 中固定 Node/浏览器(Headless Chrome)版本,避免「本地通过、CI 挂」的环境差异问题。
- 逐步为核心业务组件补齐测试,而非追求一次性 100% 覆盖率,优先覆盖高风险路径(支付、登录、权限判断)。
常见坑
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 断言读到旧的 DOM 内容 | 数据变化后忘记调用 fixture.detectChanges() | 每次影响视图的操作后都调用 |
fakeAsync 测试报「还有未清理的计时器」 | 组件内计时器未被 tick 完全消费或未清理 | 用 tick(足够时长),或在测试后清理订阅 |
| HTTP 测试报「预期恰好一次请求」失败 | 组件触发了多次或零次请求 | 检查触发条件,必要时用 expectOne 前先确认请求次数 |
| 测试之间状态串扰 | 服务是 providedIn: 'root' 单例,测试间未重置 | 每个 it 前用 beforeEach 重新 configureTestingModule |
| CI 上 Karma 报浏览器无法启动 | 缺少 Headless Chrome 或相关依赖 | 使用官方 CI 镜像或配置 ChromeHeadlessCI launcher |
延伸阅读
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| Angular 测试指南 | 官方 |
| TestBed | API |
| Jasmine 文档 | 断言库 |
| HttpClientTestingModule | HTTP 模拟 |
| Playwright | 端到端测试 |
相关文章
生态集成
Angular 官方维护路由、HTTP、表单、动画等;UI 与状态扩展由社区与官方 Material 团队提供。涵盖官方/半官方库、常用第三方库、与 Vue/React 生态对照、选型建议与常见坑。
Signal 基础
Signals 是 Angular 16 引入的响应式原语:可读写的 signal、派生的 computed、副作用 effect。用于组件内状态与跨组件细粒度更新,涵盖 API、input signal、与变更检测的关系、最佳实践与常见坑。
RxJS 互操作
Angular 内置 RxJS 处理异步流(HTTP、HttpClient、路由事件、FormControl 的 valueChanges 等)。Signal 适合同步/短链路状态;长生命周期流仍常用 Observable。涵盖 async 管道、toSignal/toObservable、常用操作符、最佳实践与常见坑。
表单
Angular 提供 响应式表单(Reactive Forms) 与 模板驱动表单(Template-driven)。复杂表单推荐 Reactive Forms(模型在 TypeScript 中,易测试)。涵盖 FormGroup/FormArray、Typed Forms、自定义校验器、最佳实践与常见坑。
状态管理
Angular 没有强制单一状态库:服务 + DI、Signal、RxJS 即可覆盖多数场景;大型应用可选用 NgRx。涵盖选型、服务作为状态容器、跨组件共享、NgRx 与 SignalStore、常见坑。
HTTP 与数据
Angular 通过 HttpClient(@angular/common/http)发起 HTTP 请求,返回 RxJS Observable。见 RxJS 互操作、依赖注入。
Series
angular
14 / 14