Angular开发者必看:深度解析单元测试核心技巧与最佳实践

感谢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~)

官网:matechat.gitcode.com

相关推荐
Mintopia1 小时前
🌐 动态网络环境下的 WebAIGC 断点续传与容错技术
前端·人工智能·aigc
答案answer2 小时前
一些经典的3D编辑器开源项目
前端·开源·three.js
亿元程序员2 小时前
Creator都快4.0了,怎么能没有这样的功能?
前端
q***64972 小时前
SpringMVC 请求参数接收
前端·javascript·算法
万少2 小时前
流碧卡片 6 小时闪电开发 AI gemini-3-pro-preview ! 秒出小红书爆款图,免下载直接用
前端·后端·ai编程
向葭奔赴♡2 小时前
若依系统权限控制全流程解析
前端·javascript·vue.js·ruoyi·navicat
IT_陈寒2 小时前
Python开发者必知的5个高效技巧,让你的代码性能提升50%
前端·人工智能·后端
u***u6852 小时前
Vue虚拟现实案例
前端·vue.js·vr
q***96582 小时前
springboot3整合knife4j详细版,包会!(不带swagger2玩)
android·前端·后端