关于 angularjs-unit-testing
用于为 AngularJS 1.x 应用编写、重构和维护单元测试的技能,覆盖常见组件与异步行为。提供 Jasmine/Karma 维护以及使用 Jest 测试和迁移的指导。
主要功能
- 01测试控制器、服务、过滤器和指令
- 02模拟 HTTP 请求与依赖
- 03测试 Promise 和异步行为
- 04使用 Jest 或维护 Jasmine/Karma 测试套件
- 05分析覆盖率并组织测试套件
- 06用确定性模式减少测试波动
适用场景
- 01为旧版 AngularJS 应用添加单元测试
- 02维护现有 Jasmine/Karma 测试套件
- 03将 AngularJS 测试迁移到 Jest
- 04排查间歇性测试失败
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ greedychipmunk/ agent-skills --skill angularjs-unit-testing安装后在 Claude Code 中输入/angularjs-unit-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
INDEX.mdREADME.mdresources/angularjs-testing-reference.mdbest-practices-checklist.mdcoverage-analysis-guide.mdhttp-mocking-guide.mdjest-migration-guide.mdmock-helpers-guide.mdtesting-patterns-guide.mdscripts/run-tests.shtemplates/controller.spec.jsdirective.spec.jsfilter.spec.jsfixtures.jsservice.spec.js详细介绍
AngularJS 单元测试技能
概述
此技能专注于为 AngularJS(1.x)应用编写、重构和维护高质量的单元测试。内容涵盖控制器、服务、筛选器、指令、HTTP 模拟、Promise 和依赖注入,帮助你全面测试 AngularJS 代码库,确保其可靠性。
注意:AngularJS 已于 2021 年 12 月结束生命周期,仅会收到关键安全修复。新项目应使用 Angular 19+。对于维护 AngularJS 代码库的团队,此技能提供最新的测试模式、工具和迁移指南。请参阅本文档底部的迁移路径。
技能能力
核心测试能力
- 控制器:为控制器逻辑、状态管理和事件处理编写测试
- 服务:测试工厂/服务依赖、HTTP 调用和业务逻辑
- 筛选器:验证筛选器转换和边界情况
- 指令:测试指令编译、链接和 DOM 操作
- HTTP 模拟:使用
$httpBackend(Jasmine/Karma)或 MSW/fetch 模拟(Jest)模拟 HTTP 调用 - Promise 与异步操作:处理
$q、延迟对象和$timeout - 依赖注入:使用模拟依赖和真实依赖进行测试
- 作用域管理:测试作用域生命周期、侦听器和事件广播
- 覆盖率分析:生成并解读代码覆盖率报告
- 测试组织:遵循最佳实践和可维护性原则来组织测试
- 框架选择:新工作和混合代码仓库使用 Jest;现有测试套件使用 Jasmine/Karma
测试模式
此技能采用以下测试模式:
-
AAA 模式(准备-执行-断言)
- 将测试组织为清晰的准备、执行和验证阶段
- 提升可读性和可维护性
-
模拟与监视
- 使用 Jasmine 间谍或 Jest 模拟来模拟函数并验证调用
- 模拟 HTTP 响应和服务依赖
- 模拟用户交互
-
测试固件
- 创建可复用的测试数据和辅助函数
- 使用 beforeEach/afterEach 进行通用的设置/清理
- 减少测试重复
-
边界情况测试
- 测试边界条件、空状态和错误场景
- 验证错误处理和优雅降级
- 测试异步操作和竞态条件
-
快照测试(Jest)
- 捕获预期的组件输出
- 检测 UI 渲染中的意外变更
-
确定性异步操作
- 相较于依赖时序的断言,优先使用虚拟计时器、可控的 Promise 和隔离的状态
测试框架与测试运行器
Jasmine(传统默认选项)
对于保留现有 AngularJS + Karma 测试套件的情况,Jasmine 仍是最稳妥的选择。
关键概念:
describe():将相关测试归入一个测试套件it():定义单个测试用例expect():创建断言beforeEach()/afterEach():设置和清理钩子spyOn()/jasmine.createSpy():模拟函数并跟踪调用
设置:
npm install --save-dev jasmine karma karma-jasmine karma-chrome-launcher
适用场景:
- 维护现有的 Jasmine/Karma 测试套件
- 希望对传统 AngularJS 代码做尽可能小的改动
Jest(推荐)
Jest 是维护和迁移 AngularJS 测试的现代默认选择。它可以并行运行测试,提供更强大的模拟 API,支持快照,并内置覆盖率报告。
核心概念:
describe():将相关测试分组test()或it():定义单个测试用例expect():创建断言beforeEach()/afterEach():设置和清理钩子jest.fn():创建模拟函数jest.spyOn():监视现有方法jest.mock():模拟模块
设置:
npm install --save-dev jest jest-preset-angular angular-mocks
npm install @angular/core
Jest 配置(jest.config.js):
module.exports = {
preset: 'jest-preset-angular',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/setup-jest.js'],
transform: {
'^.+\.js$': 'babel-jest'
},
collectCoverage: true,
collectCoverageFrom: ['src/**/*.js', '!src/**/*.spec.js']
};
适用场景:
- 希望通过并行执行更快获得反馈
- 需要开箱即用的更强大模拟、快照和覆盖率功能
- 正在为 Angular 迁移准备 AngularJS 代码库
Karma(旧版测试运行器)
Karma 是传统上与 Jasmine 搭配使用的旧版浏览器测试运行器。它仍可用于现有测试套件,但自 2021 年以来未发布重大版本,不应作为新的投入方向。
配置:
karma.conf.js:主配置文件- 指定浏览器环境、要加载的文件和插件
- 支持代码覆盖率报告和 CI 集成
Jest 迁移指南
将 AngularJS 测试套件从 Jasmine/Karma 迁移到 Jest 时,请按以下步骤操作:
-
安装核心工具
npm install --save-dev jest jest-preset-angular angular-mocks npm install @angular/core如果代码库采用混合架构或正在积极迁移到 Angular,请添加
@angular/core。 -
创建 Jest 设置文件
- 添加
setup-jest.js或setup-jest.ts - 在其中加载
angular、angular-mocks和任何共享测试 polyfill
- 添加
-
为 AngularJS 文件配置 Jest
- 使用
jest.config.js,并设置testEnvironment: 'jsdom' - 为旧版 JavaScript 源文件添加转换器
- 将模板或特定于 DOM 的设置保留在 Jest 引导文件中
- 使用
-
在 Jest 中加载 AngularJS 模块
beforeEach(() => { require('angular'); require('angular-mocks'); angular.mock.module('myApp'); }); -
迁移间谍和存根
spyOn(obj, 'method')→jest.spyOn(obj, 'method')jasmine.createSpy()→jest.fn()jasmine.createSpyObj()→jest.fn()或显式模拟对象
-
在可行时替换
$httpBackend- 对于新的 Jest 测试,优先使用
fetch模拟或 MSW - 仅在立即重写成本较高的旧版测试中保留
$httpBackend
- 对于新的 Jest 测试,优先使用
-
在测试之间重置状态
- 使用
jest.clearAllMocks()/jest.resetAllMocks() - 在
beforeEach()中重新创建 AngularJS 模块和服务
- 使用
处理环境不稳定导致的测试波动
旧版 AngularJS 测试套件经常失败,是因为环境不稳定,而不是代码有问题。
测试波动的常见来源:
- 时序问题和竞态条件
- 测试之间共享状态
- 浏览器环境差异
- 依赖网络的测试和真实外部服务
- 时区、区域设置和依赖日期的逻辑
确定性测试模式:
- 对计划任务和防抖/节流逻辑使用伪计时器
- 通过显式解析 Promise 并刷新 digest,控制异步任务
- 在
afterEach()中重置共享状态、模拟对象和模块缓存 - 在单元测试中避免依赖真实浏览器或网络
- 优先使用固定测试数据,而不是生成的或基于时间的值
js-env-sanitizer 模式:
- 在每项测试前后快照并恢复依赖环境的全局对象
- 隔离
window、document、localStorage、Date、Math.random、功能标志和 DOM 变更 - 这对长期运行的 AngularJS 测试套件尤其有用,因为隐藏的环境耦合会导致间歇性失败
旧版代码库的测试金字塔建议
旧版 AngularJS 代码库的测试金字塔往往是倒置的:端到端测试过多,单元测试过少。
推荐结构:
- 底层:大量快速的控制器、服务、过滤器和指令单元测试
- 中层:较少的集成测试,覆盖模块连接、路由和 API 边界
- 顶层:少量端到端测试,仅覆盖关键用户流程
建议:
- 优先将覆盖范围转向单元测试
- 将集成测试保留在中层,而不是底层
- 谨慎使用 e2e 测试,因为它们速度较慢,且对环境更敏感
测试结构
Jasmine 测试结构(旧版默认方式)
所有 Jasmine 测试都遵循以下标准结构:
describe('Component Name', function() {
var componentUnderTest, dependencies;
beforeEach(module('myApp'));
beforeEach(inject(function($injector) {
componentUnderTest = $injector.get('ComponentName');
dependencies = $injector.get('DependencyName');
}));
afterEach(function() {
// Cleanup code
});
describe('Functionality Group', function() {
it('should do something specific', function() {
// Arrange
var input = 'test';
// Act
var result = componentUnderTest.method(input);
// Assert
expect(result).toBe('expected');
});
});
});
Jest 测试结构(推荐)
Jest 测试遵循类似的结构,并采用现代模拟方式和更简洁的清理流程:
describe('Component Name', () => {
let componentUnderTest;
let dependency;
beforeEach(() => {
jest.clearAllMocks();
// Setup code or mock initialization
dependency = { method: jest.fn() };
componentUnderTest = require('./component');
});
afterEach(() => {
// Cleanup code
});
describe('Functionality Group', () => {
test('should do something specific', () => {
// Arrange
const input = 'test';
// Act
const result = componentUnderTest.method(input, dependency);
// Assert
expect(result).toBe('expected');
});
});
});
主要区别:
- 现代测试代码中,Jest 使用
jest.fn()/jest.spyOn()代替 Jasmine 间谍 - Jest 使用
test()或it()(两者均可) - Jest 会自动发现
.spec.js和.test.js文件 - Jest 提供内置的快照测试和代码覆盖率功能
最佳实践
1. 测试组织
- 每个组件对应一个测试文件(例如,
controller.js对应controller.spec.js) - 使用
describe()将测试整理到逻辑分组中 - 使用有意义的测试名称描述预期行为
2. DRY 原则(不要重复自己)
- 将通用的初始化逻辑提取到
beforeEach()块中 - 创建可复用的测试数据样例
- 使用辅助函数减少重复
3. 测试独立性
- 每个测试都应相互独立,并可按任意顺序运行
- 在
afterEach()中清理资源 - 避免测试之间共享状态
4. 逼真的模拟
- 逼真地模拟外部依赖
- 尽可能使用实际的数据结构
- 避免过度简化或不切实际的模拟
5. 全面的覆盖
- 目标是达到 80% 以上的代码覆盖率
- 测试正常流程、边缘情况和错误场景
- 测试异步操作和竞争条件
6. 性能
- 保持测试快速
- 使用内存模拟,而不是真实 HTTP 请求
- 避免不必要的数据库操作
7. 可维护性
- 编写易于理解的测试
- 使用 AAA 模式,提升清晰度
- 使用注释说明复杂的测试逻辑
- 组件发生变化时重构测试
常见测试场景
测试控制器
describe('UserController', function() {
var $scope, controller;
beforeEach(module('myApp'));
beforeEach(inject(function($controller, $rootScope) {
$scope = $rootScope.$new();
controller = $controller('UserController', {
$scope: $scope
});
}));
it('should initialize with default values', function() {
expect($scope.users).toBeDefined();
});
it('should load users on init', function() {
expect($scope.users.length).toBeGreaterThan(0);
});
});
测试服务
describe('UserService', function() {
var userService, $httpBackend;
beforeEach(module('myApp'));
beforeEach(inject(function(_UserService_, _$httpBackend_) {
userService = _UserService_;
$httpBackend = _$httpBackend_;
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
});
it('should fetch users from API', function() {
var expectedUsers = [{ id: 1, name: 'John' }];
$httpBackend.expectGET('/api/users').respond(expectedUsers);
userService.getUsers().then(function(users) {
expect(users).toEqual(expectedUsers);
});
$httpBackend.flush();
});
});
测试 Promise
describe('PromiseService', function() {
var service, $q, $rootScope;
beforeEach(inject(function(_Service_, _$q_, _$rootScope_) {
service = _Service_;
$q = _$q_;
$rootScope = _$rootScope_;
}));
it('should handle promise resolution', function() {
var deferred = $q.defer();
var result;
service.asyncOperation().then(function(data) {
result = data;
});
deferred.resolve('success');
$rootScope.$apply();
expect(result).toBe('success');
});
});
测试组件指令
AngularJS 1.5+ 组件指令(bindings、controllerAs)是新代码的推荐模式,也是之后迁移到 Angular 最容易采用的方式。
describe('userCard component', function() {
var $compile, $rootScope, element, scope;
beforeEach(module('myApp'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
scope.user = { name: 'Alice', role: 'admin' };
}));
it('should render user name and role', function() {
element = $compile('<user-card user="user"></user-card>')(scope);
scope.$digest();
var isolated = element.isolateScope().$ctrl;
expect(isolated.user.name).toBe('Alice');
expect(element.text()).toContain('admin');
});
it('should call onSelect when clicked', function() {
scope.onSelect = jasmine.createSpy('onSelect');
element = $compile('<user-card user="user" on-select="onSelect(user)"></user-card>')(scope);
scope.$digest();
element.isolateScope().$ctrl.onSelect({ user: scope.user });
expect(scope.onSelect).toHaveBeenCalledWith(scope.user);
});
});
使用请求匹配器测试 $httpBackend
对于包含动态片段或查询参数的 API,请使用正则表达式或函数匹配器,而不是精确的 URL 字符串:
// Match any GET to /api/users with query params
$httpBackend.expectGET(/\/api\/users\?.*page=/).respond(200, mockResponse);
// Match by function
$httpBackend.expectGET(function(url) {
return url.indexOf('/api/users') === 0 && url.indexOf('page=') > -1;
}).respond(200, mockResponse);
测试 $on / $broadcast 事件
describe('event-driven service', function() {
var $rootScope, service;
beforeEach(inject(function(_$rootScope_, _EventService_) {
$rootScope = _$rootScope_;
service = _EventService_;
}));
it('should react to user:updated event', function() {
var handler = jasmine.createSpy('handler');
$rootScope.$on('user:updated', handler);
$rootScope.$broadcast('user:updated', { id: 42 });
expect(handler).toHaveBeenCalled();
expect(handler.calls.argsFor(0)[1]).toEqual({ id: 42 });
});
});
调试测试
Jasmine 调试
// Skip a test
xit('should do something', function() { ... });
// Run only this test
fit('should do something', function() { ... });
// Console logging in tests
it('should debug', function() {
console.log('Current state:', $scope);
expect(true).toBe(true);
});
Jest 调试
// Skip a test
test.skip('should do something', () => { ... });
// Run only this test
test.only('should do something', () => { ... });
// Debug with Node inspector
// Run: node --inspect-brk node_modules/.bin/jest --runInBand
与 CI/CD 集成
GitHub Actions 示例
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- run: npm ci
- run: npm test -- --coverage
- uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
Jenkins 示例
pipeline {
stages {
stage('Test') {
steps {
sh 'npm ci'
sh 'npm test'
publishHTML([
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
}
}
}
资源
故障排除
测试未运行
- 检查模块是否已加载:
beforeEach(module('myApp')) - 确认依赖项已注入:
beforeEach(inject(...)) - 检查是否存在语法错误
异步测试超时
- 确保 Promise 已解析:
$rootScope.$apply()或$httpBackend.flush() - 使用
done()回调:it('...', function(done) { ... done(); }) - 对于 Jest:使用
async/await或返回一个 Promise
HTTP 模拟未生效
- 确认在调用服务之前已设置模拟
- 检查预期中的 URL 是否完全匹配
- 对未模拟的请求使用
passThrough()
覆盖率缺口
- 查看覆盖率报告中未经测试的分支
- 测试错误条件和边缘情况
- 正确模拟外部依赖
现代运行时兼容性
AngularJS 是为较早的 Node.js 和浏览器生态系统构建的,但经过正确配置后,仍可在现代运行时中运行。
Node.js 22 LTS
- AngularJS 1.8.x 无需修改即可在 Node 22 LTS 上运行
- 如果测试使用 Karma 和真实浏览器,请搭配无头 Chrome 使用
karma-chrome-launcher - 对于 Jest:
testEnvironment: 'jsdom'可处理浏览器全局对象,无需真实浏览器
CI 中的无头 Chrome
# GitHub Actions — headless Chrome with Karma
- uses: actions/setup-chrome@v1
with:
chrome-version: stable
- run: npm test
// karma.conf.js — headless Chrome for CI
browsers: ['ChromeHeadlessNoSandbox'],
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
}
现代运行时中的常见陷阱
- V8 严格模式:依赖隐式全局变量或
arguments.callee的 AngularJS 代码会在严格模式下出错。在测试文件中使用'use strict',以便及早发现这些问题。 - jsdom 与真实浏览器:某些依赖真实布局或 CSS 计算的指令测试可能无法在 jsdom 中运行。请改用 Karma + 无头 Chrome 运行这些测试。
- npm audit 失败:AngularJS 依赖项可能存在已知漏洞。使用
npm audit --production将实际风险与仅开发环境的警告区分开来,并考虑在package.json中使用overrides固定已修补的传递依赖。
迁移路径:AngularJS → Angular
需要从 AngularJS 迁移时,可参考以下概念映射:
| AngularJS | Angular 19+ |
|---|---|
| 模块 / 控制器 | 独立组件或 NgModules、可注入服务 |
$scope / $rootScope | 组件状态、@Input() / @Output()、信号 |
$http / $resource | HttpClient |
| 指令 | 使用现代 API 的组件和指令 |
$q / 摘要循环 | RxJS、Promise/async-await、信号 |
$routeProvider | Angular Router |
angular.module() DI | 可摇树优化的提供者、inject() |
| 全局变量和临时 DOM 操作 | 依赖注入和可测试的抽象 |
混合迁移:使用 @angular/upgrade 让 AngularJS 和 Angular 并行运行。逐个迁移组件,而不是一次性重写整个应用。在整个迁移过程中,AngularJS 测试套件都会保持运行——对于混合代码库,Jest 是最佳测试运行器,因为它同时支持 AngularJS 和 Angular 测试文件。
后续步骤
- 从小处着手:为单个组件编写测试
- 选择合适的测试运行器:仅为现有测试套件保留 Jasmine/Karma;新工作和混合代码库优先使用 Jest
- 了解模式:研读测试模式指南
- 使用模板:参考适用于组件类型的模板文件
- 持续改进:根据覆盖率和反馈完善测试
- 自动化:将测试集成到 CI/CD 流水线中
- 规划迁移:准备就绪后,使用
@angular/upgrade进行渐进式迁移——测试套件也会随之迁移
专业方向:使用 Jasmine 和 Jest 进行 AngularJS 单元测试
版本:2.0
最后更新:2026 年 5 月