mcpField
A
angularjs-unit-testing

angularjs-unit-testing

github.com/greedychipmunk测试与质量2446 次安装1 周前更新
代码仓库

关于 angularjs-unit-testing

用于为 AngularJS 1.x 应用编写、重构和维护单元测试的技能,覆盖常见组件与异步行为。提供 Jasmine/Karma 维护以及使用 Jest 测试和迁移的指导。

主要功能

  1. 01测试控制器、服务、过滤器和指令
  2. 02模拟 HTTP 请求与依赖
  3. 03测试 Promise 和异步行为
  4. 04使用 Jest 或维护 Jasmine/Karma 测试套件
  5. 05分析覆盖率并组织测试套件
  6. 06用确定性模式减少测试波动

适用场景

  1. 01为旧版 AngularJS 应用添加单元测试
  2. 02维护现有 Jasmine/Karma 测试套件
  3. 03将 AngularJS 测试迁移到 Jest
  4. 04排查间歇性测试失败

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/greedychipmunk/agent-skills --skill angularjs-unit-testing

安装后在 Claude Code 中输入/angularjs-unit-testing确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

INDEX.md
README.md
resources/
angularjs-testing-reference.md
best-practices-checklist.md
coverage-analysis-guide.md
http-mocking-guide.md
jest-migration-guide.md
mock-helpers-guide.md
testing-patterns-guide.md
scripts/
run-tests.sh
templates/
controller.spec.js
directive.spec.js
filter.spec.js
fixtures.js
service.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
测试模式

此技能采用以下测试模式:

  1. AAA 模式(准备-执行-断言)

    • 将测试组织为清晰的准备、执行和验证阶段
    • 提升可读性和可维护性
  2. 模拟与监视

    • 使用 Jasmine 间谍或 Jest 模拟来模拟函数并验证调用
    • 模拟 HTTP 响应和服务依赖
    • 模拟用户交互
  3. 测试固件

    • 创建可复用的测试数据和辅助函数
    • 使用 beforeEach/afterEach 进行通用的设置/清理
    • 减少测试重复
  4. 边界情况测试

    • 测试边界条件、空状态和错误场景
    • 验证错误处理和优雅降级
    • 测试异步操作和竞态条件
  5. 快照测试(Jest)

    • 捕获预期的组件输出
    • 检测 UI 渲染中的意外变更
  6. 确定性异步操作

    • 相较于依赖时序的断言,优先使用虚拟计时器、可控的 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 时,请按以下步骤操作:

  1. 安装核心工具

    npm install --save-dev jest jest-preset-angular angular-mocks
    npm install @angular/core
    

    如果代码库采用混合架构或正在积极迁移到 Angular,请添加 @angular/core。

  2. 创建 Jest 设置文件

    • 添加 setup-jest.js 或 setup-jest.ts
    • 在其中加载 angular、angular-mocks 和任何共享测试 polyfill
  3. 为 AngularJS 文件配置 Jest

    • 使用 jest.config.js,并设置 testEnvironment: 'jsdom'
    • 为旧版 JavaScript 源文件添加转换器
    • 将模板或特定于 DOM 的设置保留在 Jest 引导文件中
  4. 在 Jest 中加载 AngularJS 模块

    beforeEach(() => {
      require('angular');
      require('angular-mocks');
      angular.mock.module('myApp');
    });
    
  5. 迁移间谍和存根

    • spyOn(obj, 'method') → jest.spyOn(obj, 'method')
    • jasmine.createSpy() → jest.fn()
    • jasmine.createSpyObj() → jest.fn() 或显式模拟对象
  6. 在可行时替换 $httpBackend

    • 对于新的 Jest 测试,优先使用 fetch 模拟或 MSW
    • 仅在立即重写成本较高的旧版测试中保留 $httpBackend
  7. 在测试之间重置状态

    • 使用 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 迁移时,可参考以下概念映射:

AngularJSAngular 19+
模块 / 控制器独立组件或 NgModules、可注入服务
$scope / $rootScope组件状态、@Input() / @Output()、信号
$http / $resourceHttpClient
指令使用现代 API 的组件和指令
$q / 摘要循环RxJS、Promise/async-await、信号
$routeProviderAngular Router
angular.module() DI可摇树优化的提供者、inject()
全局变量和临时 DOM 操作依赖注入和可测试的抽象

混合迁移:使用 @angular/upgrade 让 AngularJS 和 Angular 并行运行。逐个迁移组件,而不是一次性重写整个应用。在整个迁移过程中,AngularJS 测试套件都会保持运行——对于混合代码库,Jest 是最佳测试运行器,因为它同时支持 AngularJS 和 Angular 测试文件。

后续步骤

  1. 从小处着手:为单个组件编写测试
  2. 选择合适的测试运行器:仅为现有测试套件保留 Jasmine/Karma;新工作和混合代码库优先使用 Jest
  3. 了解模式:研读测试模式指南
  4. 使用模板:参考适用于组件类型的模板文件
  5. 持续改进:根据覆盖率和反馈完善测试
  6. 自动化:将测试集成到 CI/CD 流水线中
  7. 规划迁移:准备就绪后,使用 @angular/upgrade 进行渐进式迁移——测试套件也会随之迁移

专业方向:使用 Jasmine 和 Jest 进行 AngularJS 单元测试
版本:2.0
最后更新:2026 年 5 月

评论

支持 Markdown