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. 02HTTPリクエストと依存関係のモック
  3. 03Promiseと非同期処理のテスト
  4. 04Jestの利用とJasmine/Karmaテストの保守
  5. 05カバレッジ分析とテスト構成
  6. 06決定的な手法によるテストの不安定さの軽減

活用シーン

  1. 01レガシーAngularJSアプリへの単体テスト追加
  2. 02既存のJasmine/Karmaテストスイートの保守
  3. 03AngularJSテストの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 のモック化、プロミス、依存性注入など、AngularJS のコードベースを十分にテストし、信頼性を保つために必要なすべてを扱います。

注:AngularJS は 2021 年 12 月にサポート終了となりました。現在は重大なセキュリティ修正のみ提供されています。新しいプロジェクトでは Angular 19 以降を使用してください。AngularJS のコードベースを保守するチーム向けに、このスキルでは最新のテストパターン、ツール、移行ガイダンスを提供します。ドキュメントの末尾にある移行パスをご覧ください。

スキルの機能

テストの主要スキル
  • コントローラー:コントローラーのロジック、状態管理、イベント処理のテストを作成します
  • サービス:ファクトリー/サービスの依存関係、HTTP 呼び出し、ビジネスロジックをテストします
  • フィルター:フィルターによる変換とエッジケースを検証します
  • ディレクティブ:ディレクティブのコンパイル、リンク、DOM 操作をテストします
  • HTTP のモック化:$httpBackend(Jasmine/Karma)または MSW/fetch モック(Jest)を使って HTTP 呼び出しをモックします
  • プロミスと非同期処理:$q、遅延オブジェクト、$timeout を扱います
  • 依存性注入:モックした依存関係と実際の依存関係を使ってテストします
  • スコープ管理:スコープのライフサイクル、ウォッチャー、イベントのブロードキャストをテストします
  • カバレッジ分析:コードカバレッジレポートを生成し、解釈します
  • テストの構成:ベストプラクティスと保守性の原則に沿ってテストを構成します
  • フレームワークの選択:新規作業やハイブリッドリポジトリには Jest、既存のテストスイートには Jasmine/Karma を使用します
テストパターン

このスキルでは、以下のテストパターンを実装します。

  1. AAA パターン(Arrange-Act-Assert)

    • セットアップ、実行、検証の各フェーズを明確にしてテストを構成します
    • 可読性と保守性を高めます
  2. モック化とスパイ

    • Jasmine のスパイまたは Jest のモックを使って関数をモックし、呼び出しを検証します
    • HTTP レスポンスとサービスの依存関係をモックします
    • ユーザーの操作をシミュレートします
  3. テストフィクスチャ

    • 再利用可能なテストデータとヘルパー関数を作成します
    • 共通のセットアップ/後処理には beforeEach/afterEach を使用します
    • テストの重複を減らします
  4. エッジケースのテスト

    • 境界条件、空の状態、エラーのシナリオをテストします
    • エラー処理と適切な縮退動作を検証します
    • 非同期処理と競合状態をテストします
  5. スナップショットテスト(Jest)

    • コンポーネントに期待される出力を記録します
    • UI 描画に意図しない変更がないか検出します
  6. 決定論的な非同期処理

    • タイミングに左右されるアサーションよりも、フェイクタイマー、制御されたプロミス、分離された状態を優先します

テストフレームワークとテストランナー

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 に移行する際は、次の手順を使用してください。

  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、および共有テスト用ポリフィルを読み込みます
  3. AngularJS ファイル用に Jest を設定する

    • testEnvironment: 'jsdom' を指定した jest.config.js を使用します
    • レガシー 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 のテストスイートでは、コードに問題があるのではなく、環境が不安定なために失敗することがよくあります。

不安定さの一般的な原因:

  • タイミングの問題や競合状態
  • テスト間で共有される状態
  • ブラウザー環境の違い
  • ネットワークに依存するテストや実際の外部サービス
  • タイムゾーン、ロケール、日付に依存するロジック

決定性のあるテストパターン:

  • スケジュールされた処理やデバウンス/スロットルのロジックには、フェイクタイマーを使用します
  • 明示的なプロミスの解決と 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'
        ])
      }
    }
  }
}

リソース

トラブルシューティング

テストが実行されない
  • モジュールが読み込まれていることを確認します: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、プロミス/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月

コメント