angularjs-unit-testing について
AngularJS 1.xアプリの単体テストを作成、リファクタリング、保守するスキルです。主要なコンポーネントや非同期処理を扱い、Jasmine/Karmaの保守とJestによるテスト・移行を案内します。
主な機能
- 01コントローラー、サービス、フィルター、ディレクティブのテスト
- 02HTTPリクエストと依存関係のモック
- 03Promiseと非同期処理のテスト
- 04Jestの利用とJasmine/Karmaテストの保守
- 05カバレッジ分析とテスト構成
- 06決定的な手法によるテストの不安定さの軽減
活用シーン
- 01レガシーAngularJSアプリへの単体テスト追加
- 02既存のJasmine/Karmaテストスイートの保守
- 03AngularJSテストの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 のモック化、プロミス、依存性注入など、AngularJS のコードベースを十分にテストし、信頼性を保つために必要なすべてを扱います。
注:AngularJS は 2021 年 12 月にサポート終了となりました。現在は重大なセキュリティ修正のみ提供されています。新しいプロジェクトでは Angular 19 以降を使用してください。AngularJS のコードベースを保守するチーム向けに、このスキルでは最新のテストパターン、ツール、移行ガイダンスを提供します。ドキュメントの末尾にある移行パスをご覧ください。
スキルの機能
テストの主要スキル
- コントローラー:コントローラーのロジック、状態管理、イベント処理のテストを作成します
- サービス:ファクトリー/サービスの依存関係、HTTP 呼び出し、ビジネスロジックをテストします
- フィルター:フィルターによる変換とエッジケースを検証します
- ディレクティブ:ディレクティブのコンパイル、リンク、DOM 操作をテストします
- HTTP のモック化:
$httpBackend(Jasmine/Karma)または MSW/fetch モック(Jest)を使って HTTP 呼び出しをモックします - プロミスと非同期処理:
$q、遅延オブジェクト、$timeoutを扱います - 依存性注入:モックした依存関係と実際の依存関係を使ってテストします
- スコープ管理:スコープのライフサイクル、ウォッチャー、イベントのブロードキャストをテストします
- カバレッジ分析:コードカバレッジレポートを生成し、解釈します
- テストの構成:ベストプラクティスと保守性の原則に沿ってテストを構成します
- フレームワークの選択:新規作業やハイブリッドリポジトリには Jest、既存のテストスイートには Jasmine/Karma を使用します
テストパターン
このスキルでは、以下のテストパターンを実装します。
-
AAA パターン(Arrange-Act-Assert)
- セットアップ、実行、検証の各フェーズを明確にしてテストを構成します
- 可読性と保守性を高めます
-
モック化とスパイ
- Jasmine のスパイまたは Jest のモックを使って関数をモックし、呼び出しを検証します
- HTTP レスポンスとサービスの依存関係をモックします
- ユーザーの操作をシミュレートします
-
テストフィクスチャ
- 再利用可能なテストデータとヘルパー関数を作成します
- 共通のセットアップ/後処理には beforeEach/afterEach を使用します
- テストの重複を減らします
-
エッジケースのテスト
- 境界条件、空の状態、エラーのシナリオをテストします
- エラー処理と適切な縮退動作を検証します
- 非同期処理と競合状態をテストします
-
スナップショットテスト(Jest)
- コンポーネントに期待される出力を記録します
- UI 描画に意図しない変更がないか検出します
-
決定論的な非同期処理
- タイミングに左右されるアサーションよりも、フェイクタイマー、制御されたプロミス、分離された状態を優先します
テストフレームワークとテストランナー
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']
};
以下の場合に使用します:
- 並列実行によって、より迅速にフィードバックを得たい場合
- より優れたモック、スナップショット、カバレッジ機能をすぐに利用したい場合
- AngularJS のコードベースを Angular への移行に向けて準備する場合
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、および共有テスト用ポリフィルを読み込みます
-
AngularJS ファイル用に Jest を設定する
testEnvironment: 'jsdom'を指定したjest.config.jsを使用します- レガシー 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 のテストスイートでは、コードに問題があるのではなく、環境が不安定なために失敗することがよくあります。
不安定さの一般的な原因:
- タイミングの問題や競合状態
- テスト間で共有される状態
- ブラウザー環境の違い
- ネットワークに依存するテストや実際の外部サービス
- タイムゾーン、ロケール、日付に依存するロジック
決定性のあるテストパターン:
- スケジュールされた処理やデバウンス/スロットルのロジックには、フェイクタイマーを使用します
- 明示的なプロミスの解決と 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 では、モダンなテストコードで Jasmine のスパイの代わりに
jest.fn()/jest.spyOn()を使用します - Jest では
test()またはit()を使用します(どちらも機能します) - Jest は
.spec.jsファイルと.test.jsファイルを自動検出します - Jest には、スナップショットテストとコードカバレッジの機能が組み込まれています
ベストプラクティス
1. テストの整理
- コンポーネントごとにテストファイルを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'
])
}
}
}
}
リソース
- Jasmine のドキュメント
- Karma テストランナー
- Jest のドキュメント
- jest-preset-angular
- AngularJS テストガイド
- Angular テストガイド
- Angular アップデートガイド
トラブルシューティング
テストが実行されない
- モジュールが読み込まれていることを確認します:
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、プロミス/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月