感谢DevUI社区贡献者 tian_ya 提供的优质好文!
Angular开发者必看:深度解析单元测试核心技巧与最佳实践
一、前言
1. 什么是单元测试
- 定义:单元测试是针对软件中的最小可测试单元(如函数、方法或类)进行的测试,以验证其是否符合预期。
- 目标:确保代码的正确性、可靠性和可维护性。
- 不同测试阶段的区别:
- 单元测试:测试代码的最小单元。
- 集成测试:测试不同模块之间的交互。
- 系统测试:整体系统是否满足需求。
- 验收测试:从用户角度测试功能是否符合需求。
2. 为什么需要单元测试
- 提升代码质量:通过自动化测试发现潜在的bug。
- 支持重构安全:确保代码行为在重构后保持一致。
二、Angular的单元测试
1. 配置 Karma + Jasmine测试环境
使用 Angular CLI 创建项目时,默认已集成 Jasmine 和 Karma。
- Karma:为前端自动化测试提供了跨浏览器测试的能力,集成像
Jasmine等测试框架,启动一个Web服务器将测试脚本放到浏览器中执行。还有一些其他比如生成代码覆盖率的报告等功能。 - Jasmine:一个 JavaScript 测试框架,它不依赖于浏览器、dom 或其它 JavaScript 框架。
配置文件:
karma.conf.js:Karma 配置文件,控制浏览器启动、测试报告生成等;test.ts:入口文件,用于加载所有测试文件;tsconfig.spec.json:编译测试文件所需的 TypeScript 配置。
2. 编写单元测试用例
创建一个.spec.ts后缀的测试文件,写一个简单的测试用例,如下:
demo.spec.ts
typescript
describe('Demo UT', () => {
it('1+1=2', () => {
expect(1 + 1).toBe(2);
});
});
推荐使用AI自动生成基础测试用例,减少重复劳动,提高效率。
提示词
md
为文件:src\app\demo.ts 生成UT用例。
命名方式:
- 测试套命名:[文件名];
- 内层测试套命名:[方法名];
- 用例命名:场景[序号]:[场景名]
测试文件保存路径:当前文件路径下
3. 运行测试
运行下面命令,默认会打开浏览器并实时运行所有测试:
bash
ng test
也可指定参数运行单个单元测试文件,适用于调试特定组件或服务。
bash
ng test --include src/app/demo.spec.ts
浏览器页面上会显示所有用例的执行结果。

三、Jasmine 框架入门
1. 核心概念
| 概念 | 描述 |
|---|---|
| describe() | 定义一组相关的测试用例(测试套件) |
| it() | 定义单个测试用例(规格) |
| beforeEach() / afterEach() | 每次执行测试前后运行的初始化/清理代码 |
| beforeAll() / afterAll() | 整个测试套件开始前/结束后运行一次 |
| expect() | 断言表达式,判断实际值与期望值是否一致 |
常用断言方法如下:
值相等性断言
toBe(expected):判断两个值是否严格相等(===)。toEqual(expected):判断两个对象或值是否相等。not:与其他匹配器使用,表示不符合该条件。
typescript
expect(1).toBe(1); // success
expect({ a: 1 }).not.toBe({ a: 1 }); // success
expect({ a: 1 }).toEqual({ a: 1 }); // success
expect(0).not.toBe(null); // success
真假值和定义性断言
toBeDefined():断言值已定义。toBeUndefined():断言值未定义。toBeNull():断言值为 null。toBeTruthy():断言值为真值。toBeFalsy():断言值为假值。
typescript
expect(0).toBeDefined(); // success
expect(undefined).toBeUndefined(); // success
expect(null).toBeNull(); // success
expect(1).toBeTruthy(); // success
expect(true).toBeTruthy(); // success
expect({}).toBeTruthy(); // success
expect(0).toBeFalsy(); // success
expect('').toBeFalsy(); // success
expect(false).toBeFalsy(); // success
expect(NaN).toBeFalsy(); // success
数值范围断言
toBeLessThan(number): 断言值小于指定的数字。toBeGreaterThan(number): 断言值大于指定的数字。
typescript
expect(5).toBeLessThan(10); // success
expect(10).toBeGreaterThan(5); // success
其他常用断言
toContain(item): 断言数组或字符串中包含某个元素或子串。toBeCloseTo(number, precision): 断言在指定的精度范围内相似。toMatch(RegExp): 断言符合正则表达式。toThrow(): 断言函数执行时抛出错误。
typescript
expect([1, 2, 3]).toContain(2); // success
expect(1.24).toBeCloseTo(1, 0.24); //success
expect('123').toMatch(/\d+/); // success
expect(() => { throw new Error(); }).toThrow(); // success
2. Mock 与 Spy
- Mock:创建模拟对象,代替实际依赖。
使用jasmine.createSpy()创建模拟函数,示例:
typescript
const mockService = jasmine.createSpyObj('Service', ['methodName']);
- Spy:监视方法调用,记录调用次数、参数等。
使用 spyOn() 方法来创建一个Spy对象,该对象会监控指定的方法。
typescript
const myObject = {
myMethod: function(arg) {
return 'real return';
}
};
spyOn(myObject, 'myMethod');
模拟方法行为:
and.returnValue(): 让Spy在被调用时返回一个预设的值。
typescript
spyOn(myObject, 'myMethod').and.returnValue('fake return');
and.callFake(): 提供一个模拟函数,该函数将代替原始函数执行。
typescript
spyOn(myObject, 'myMethod').and.callFake(function(arg) {
return 'mocked: ' + arg;
});
and.throwError(): 模拟抛出一个异常。
typescript
spyOn(myObject, 'myMethod').and.throwError('Something went wrong');
验证调用:在测试中调用被Spy的函数后,可以使用Spy对象的方法来验证它的行为,例如:
| 方法 | 描述 |
|---|---|
| toHaveBeenCalled() | 检查函数是否被调用过 |
| toHaveBeenCalledTimes(number) | 检查函数被调用的次数 |
| toHaveBeenCalledWith(arg1, arg2, ...) | 检查函数是否用特定的参数被调用过 |
- 依赖注入 :利用Angular 的TestBed 来创建一个隔离的测试环境,并提供模拟的依赖项。
TestBed.configureTestingModule: 配置测试模块TestBed.inject:获取和测试需要注入的服务。
typescript
import { TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
import { MyService } from './my.service'; // 你的服务
import { MockMyService } from './mock-my.service'; // Mock服务
describe('MyComponent', () => {
let component: MyComponent;
let service: MyService; // 实际服务类型
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [
// 提供 Mock 服务
{ provide: MyService, useClass: MockMyService }
]
}).compileComponents();
service = TestBed.inject(MyService); // 使用 TestBed.inject 获取 Mock 服务
});
it('should create', () => {
component = TestBed.createComponent(MyComponent).componentInstance;
expect(component).toBeTruthy();
});
it('should call service method', () => {
// 假设 MockMyService 有一个 getData 方法
spyOn(service, 'getData').and.returnValue('mock data');
component.ngOnInit(); // 触发依赖注入的服务方法
expect(service.getData).toHaveBeenCalled(); // 验证服务方法是否被调用
expect(component.data).toBe('mock data'); // 验证组件是否正确使用了服务返回的数据
});
});
3. 异步测试处理
- 使用
done()回调
typescript
// Promise
it('应异步获取数据', (done: DoneFn) => {
fetchData().then(data => {
expect(data).toEqual('expected result');
done();
});
});
// Observable
it('应使用Observable获取数据', (done: DoneFn) => {
queryData().subscribe({
next: (data) => {
expect(data).toBe('expected result');
},
complete: () => {
done();
},
});
});
- 使用 async/await 处理Promise
typescript
it('应该使用 async/await 获取数', async () => {
const data = await fetchData();
expect(data).toEqual('expected result');
});
四、测试覆盖率
1. 测试覆盖率的概念
测试覆盖率是指测试用例执行过程中访问到的源代码行数占总代码的比例,用来衡量测试用例覆盖代码的程度。
高覆盖率确保代码的关键部分被充分测试,有助于发现潜在缺陷。
2. 配置覆盖率插件
- 安装插件:
bash
npm install karma-coverage --save-dev
- 修改配置文件
karma.conf.js,添加以下配置:
javascript
module.exports = function(config) {
config.set({
plugins: [
require('karma-coverage')
],
reporters: ['progress', 'coverage'],
coverageReporter: {
type: 'html',
dir: 'coverage/'
}
});
};
3. 解读覆盖率报告
- 生成报告:运行命令
bash
ng test --code-coverage
如果希望每次运行ng test都同步刷新覆盖率,可以修改angular.json中的architect.test.options添加配置:"codeCoverage": true
json
"options": {
"main": "src/test.ts",
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"codeCoverage": true,
...
}
- 查看报告:在
coverage目录下打开index.html,分析哪些代码未被覆盖。- 行覆盖率(Lines)
- 分支覆盖率(Branches)
- 函数覆盖率(Functions)
- 语句覆盖率(Statements)

加入我们
DevUI团队重磅推出~前端智能化场景解决方案MateChat,为项目添加智能化助手~
源码:gitcode.com/DevCloudFE/...(欢迎star~)