Angular のテストを Vitest で書くには、ng new で作ったプロジェクトのまま ng test を実行するだけです。 Angular 22 では Vitest が既定のテストランナーで、@angular/build:unit-test ビルダーがビルドと Vitest の起動をまとめて行います。describe / it / expect / vi はグローバルに使え、TestBed もそのまま使えます。

この記事では Angular 22.2.0 で実際にプロジェクトを作り、コンポーネント・signal・サービス・HTTP・ルーティング・タイマーのテストをコピペで動く形 で紹介します。さらに jsdom とブラウザモード(Playwright/Chromium)の両方で実行した出力と所要時間、カバレッジの出し方、既存の Karma/Jasmine プロジェクトに 公式の移行スキーマを実行した結果(出たエラーと直し方を含む)、Jasmine → Vitest の書き換え対応表 まで、1ページで確認できるようにまとめました。

Angular × Vitest の早見表

やりたいこと コマンド・書き方 補足
テストを1回だけ実行 ng test --watch=false TTY でなければ既定で1回実行
テスト名を1行ずつ表示 ng test --reporters=verbose default / dots / json / junit なども可
一部のファイルだけ実行 ng test --include src/app/tasks ディレクトリ・ファイル指定可
テスト名で絞る ng test --filter "^TaskApi" 正規表現
対象ファイルを一覧 ng test --list-tests ビルドせずに表示
カバレッジ npm i -D @vitest/coverage-v8 → ng test --coverage レポーターは --coverage-reporters
ブラウザで実行 npm i -D @vitest/browser-playwright playwright → ng test --browsers=chromiumHeadless 既存の Chromium は CHROME_BIN で指定
モック関数 vi.fn() / vi.spyOn(obj, 'method') サービスは { provide: X, useValue: { m: vi.fn() } } で差し替え
タイマーを進める vi.useFakeTimers() → vi.advanceTimersByTime(ms) fakeAsync / tick の置き換え
変更検知を待つ await fixture.whenStable() ゾーンレス(v22 既定)の基本形
signal の input を設定 fixture.componentRef.setInput('label', '在庫')
HTTP をモック provideHttpClient(), provideHttpClientTesting() HttpTestingController で応答を返す
ルーティング RouterTestingHarness.create() navigateByUrl で遷移
Karma から移行 ng update @angular/cli --name=migrate-karma-to-vitest 設定ファイルを移行
Jasmine の書き方を変換 ng g @schematics/angular:refactor-jasmine-vitest --fake-async で fakeAsync も変換

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular テスト Vitest 入門|v22の書き方・移行・実行結果つき」

検証環境とプロジェクトの作成

この記事の出力はすべて Angular 22.2.0 / Node.js 24.21.0 / Vitest 5.0.2 / jsdom 30 系 / TypeScript 6.0.3 / Playwright 1.63.0(@vitest/browser-playwright 5.0.2)で実行したものです。

npx -y -p @angular/cli@22 ng new vt-app --skip-git --defaults --style=css --ssr=false --routing --ai-config=none
cd vt-app
npx ng test --watch=false
Application bundle generation complete. [1.755 seconds]

 RUN  v5.0.2 /…/vt-app

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Duration  1.19s (environment 56%, setup 19%, transform 10%, tests 9%, import 5%, worker 1%)

ng new の直後から Vitest でテストが通ります。 生成された設定のポイントは次の3つです。

ファイル 中身 意味
angular.json "test": { "builder": "@angular/build:unit-test" } ビルドと Vitest 実行をまとめるビルダー
tsconfig.spec.json "types": ["vitest/globals"] describe や vi を import なしで使える
package.json vitest、jsdom(devDependencies) 既定はブラウザなし(jsdom)で実行

Angular 22 の新規プロジェクトはゾーンレス(zone.js なし)で、コンポーネントの変更検知の既定も OnPush です。 テストでは fixture.detectChanges() よりも、await fixture.whenStable() で「変更検知が落ち着くまで待つ」書き方が基本になります。

コンポーネントと signal をテストする

signal の input は setInput、表示の更新は whenStable で待ちます。 computed の値はクラスから直接読んで確認できます。

// src/app/counter/counter.ts
import { Component, computed, input, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <p class="label">{{ label() }}: <span class="count">{{ count() }}</span></p>
    <p class="double">2倍: {{ double() }}</p>
    <button type="button" (click)="increment()">+1</button>
  `,
})
export class Counter {
  readonly label = input('カウント');
  readonly count = signal(0);
  readonly double = computed(() => this.count() * 2);

  increment() {
    this.count.update((n) => n + 1);
  }
}
// src/app/counter/counter.spec.ts
import { TestBed } from '@angular/core/testing';
import { Counter } from './counter';

describe('Counter', () => {
  it('ボタンを押すと表示とcomputedが更新される', async () => {
    const fixture = TestBed.createComponent(Counter);
    fixture.componentRef.setInput('label', '在庫');
    await fixture.whenStable();

    const el: HTMLElement = fixture.nativeElement;
    expect(el.querySelector('.label')?.textContent).toContain('在庫: 0');

    el.querySelector('button')!.click();
    await fixture.whenStable();

    expect(el.querySelector('.count')?.textContent).toBe('1');
    expect(el.querySelector('.double')?.textContent).toContain('2倍: 2');
    expect(fixture.componentInstance.double()).toBe(2);
  });
});

HTTP を provideHttpClientTesting でテストする

provideHttpClient() と provideHttpClientTesting() を並べて登録し、HttpTestingController で応答を返します。 Observable は firstValueFrom で Promise にすると、await expect(...).resolves で素直に書けます。

// src/app/tasks/task-api.ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';

export interface Task {
  id: number;
  title: string;
  done: boolean;
}

@Injectable({ providedIn: 'root' })
export class TaskApi {
  private readonly http = inject(HttpClient);

  list(): Observable<Task[]> {
    return this.http.get<Task[]>('/api/tasks');
  }

  add(title: string): Observable<Task> {
    return this.http.post<Task>('/api/tasks', { title, done: false });
  }
}
// src/app/tasks/task-api.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { firstValueFrom } from 'rxjs';
import { TaskApi } from './task-api';

describe('TaskApi', () => {
  let api: TaskApi;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    api = TestBed.inject(TaskApi);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => httpMock.verify());

  it('GET /api/tasks の結果を返す', async () => {
    const promise = firstValueFrom(api.list());

    const req = httpMock.expectOne('/api/tasks');
    expect(req.request.method).toBe('GET');
    req.flush([{ id: 1, title: '備品を発注する', done: false }]);

    await expect(promise).resolves.toEqual([{ id: 1, title: '備品を発注する', done: false }]);
  });

  it('サーバーエラーを受け取る', async () => {
    const promise = firstValueFrom(api.list());
    httpMock.expectOne('/api/tasks').flush('NG', { status: 500, statusText: 'Server Error' });
    await expect(promise).rejects.toMatchObject({ status: 500 });
  });
});

provideHttpClient() は provideHttpClientTesting() より前に書きます。 afterEach の httpMock.verify() で「応答していないリクエストが残っていないか」も確認できます。

ルーティングを RouterTestingHarness でテストする

RouterTestingHarness を使うと、URL パラメータの表示とリンクによる遷移を実際のルーターで確認できます。

// src/app/pages/pages.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';

@Component({
  selector: 'app-home',
  imports: [RouterLink],
  template: `<h2>ホーム</h2><a routerLink="/items/42">詳細へ</a>`,
})
export class HomePage {}

@Component({
  selector: 'app-item-detail',
  template: `<h2>備品 #{{ id() }}</h2>`,
})
export class ItemDetailPage {
  readonly id = toSignal(inject(ActivatedRoute).paramMap.pipe(map((p) => p.get('id'))));
}
// src/app/pages/pages.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideRouter, Router } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
import { HomePage, ItemDetailPage } from './pages';

describe('ルーティング', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        provideRouter([
          { path: '', component: HomePage },
          { path: 'items/:id', component: ItemDetailPage },
        ]),
      ],
    });
  });

  it('URLパラメータが画面に出る', async () => {
    const harness = await RouterTestingHarness.create();
    await harness.navigateByUrl('/items/7', ItemDetailPage);
    expect(harness.routeNativeElement?.textContent).toContain('備品 #7');
  });

  it('リンクをクリックすると遷移する', async () => {
    const harness = await RouterTestingHarness.create('/');
    (harness.routeNativeElement!.querySelector('a') as HTMLAnchorElement).click();
    await harness.fixture.whenStable();
    expect(TestBed.inject(Router).url).toBe('/items/42');
    expect(harness.routeNativeElement?.textContent).toContain('備品 #42');
  });
});

タイマーは vi.useFakeTimers で進める

setTimeout を使う処理は、Vitest のフェイクタイマーで時間を進めてテストします。 fakeAsync / tick の代わりになる、zone.js を使わない書き方です。

// src/app/search/search-box.ts
import { Component, output } from '@angular/core';

@Component({
  selector: 'app-search-box',
  template: `<input placeholder="検索" (input)="onInput($any($event.target).value)" />`,
})
export class SearchBox {
  readonly search = output<string>();
  private timer?: ReturnType<typeof setTimeout>;

  onInput(value: string) {
    clearTimeout(this.timer);
    this.timer = setTimeout(() => this.search.emit(value.trim()), 300);
  }
}
// src/app/search/search-box.spec.ts
import { TestBed } from '@angular/core/testing';
import { SearchBox } from './search-box';

describe('SearchBox(fake timers)', () => {
  beforeEach(() => vi.useFakeTimers());
  afterEach(() => vi.useRealTimers());

  it('300ms 入力が止まったら1回だけ emit する', () => {
    const fixture = TestBed.createComponent(SearchBox);
    const emitted: string[] = [];
    fixture.componentInstance.search.subscribe((v) => emitted.push(v));

    const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
    for (const v of ['ペ', 'ペン', 'ペン ']) {
      input.value = v;
      input.dispatchEvent(new Event('input'));
      vi.advanceTimersByTime(100);
    }
    expect(emitted).toEqual([]);

    vi.advanceTimersByTime(200);
    expect(emitted).toEqual(['ペン']);
  });
});

フェイクタイマー中に await fixture.whenStable() を使うと、検証では Error: Test timed out in 5000ms. で止まりました。 ゾーンレスの変更検知の予約もタイマーで動くためです。対処は次のどちらかです。

対処 書き方
変更検知を手動で進める TestBed.tick() を呼ぶ
実時間も少しずつ進める vi.useFakeTimers({ shouldAdvanceTime: true, advanceTimeDelta: 1 })(公式の変換スキーマもこの形を出力)

実行結果(jsdom)

ここまでのテストに、POST の本文を確かめるテストと、signal を持つサービスを vi.fn() のモックで確かめるテストを1件ずつ加えた6ファイル・10テストを --reporters=verbose で実行した結果です。

npx ng test --watch=false --reporters=verbose
 ✓ |vt-app| src/app/pages/pages.spec.ts > ルーティング > URLパラメータが画面に出る 56ms
 ✓ |vt-app| src/app/pages/pages.spec.ts > ルーティング > リンクをクリックすると遷移する 19ms
 ✓ |vt-app| src/app/tasks/task-api.spec.ts > TaskApi > GET /api/tasks の結果を返す 5ms
 ✓ |vt-app| src/app/counter/counter.spec.ts > Counter > ボタンを押すと表示とcomputedが更新される 9ms
 ✓ |vt-app| src/app/search/search-box.spec.ts > SearchBox(fake timers) > 300ms 入力が止まったら1回だけ emit する 10ms
 …(ほか5件も ✓)
 Test Files  6 passed (6)
      Tests  10 passed (10)
   Duration  1.64s (environment 56%, setup 16%, tests 12%, transform 10%, import 5%, worker 1%)

カバレッジを出す

カバレッジは @vitest/coverage-v8 を入れて ng test --coverage で出します。 入れずに実行すると Code coverage requires either "@vitest/coverage-v8" or "@vitest/coverage-istanbul" to be installed. と案内されます。

npm i -D @vitest/coverage-v8@5
npx ng test --watch=false --coverage
 % Coverage report from v8
No files with missing coverage.
7 files fully covered.

=============================== Coverage summary ===============================
Statements   : 100% ( 134/134 )
Branches     : 100% ( 62/62 )
Functions    : 100% ( 28/28 )
Lines        : 100% ( 73/73 )
================================================================================

HTML レポートは coverage/ に出力されます。 対象の絞り込みは --coverage-include / --coverage-exclude、形式は --coverage-reporters(html・lcov・text-summary・cobertura など)で指定でき、同じ項目を angular.json の test.options に書くこともできます。

ブラウザモード(Playwright/Chromium)で実行する

実際のブラウザで動かしたいテストは、ブラウザモードで実行できます。 必要なのはプロバイダーのパッケージと --browsers の指定だけです。

npm i -D @vitest/browser-playwright@5 playwright
npx ng test --watch=false --browsers=chromiumHeadless

ブラウザ名の末尾に Headless を付けるとヘッドレスで起動します(環境変数 CI があるときも自動でヘッドレス)。angular.json に書く場合は次のとおりです。

"test": {
  "builder": "@angular/build:unit-test",
  "options": {
    "browsers": ["chromiumHeadless"]
  }
}

既にインストール済みの Chromium を使う

Playwright が期待するブラウザが無いと、次のエラーになります。

Error: browserType.launch: Executable doesn't exist at /opt/pw-browsers/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell

手元の Chromium を使うときは、環境変数 CHROME_BIN に実行ファイルのパスを入れます。 @angular/build のブラウザ設定は、Playwright プロバイダーで chrome / chromium を使うとき CHROME_BIN を launchOptions.executablePath として渡します(22.2.0 のビルダーのソースで確認)。

CHROME_BIN=/path/to/chromium npx ng test --watch=false --browsers=chromiumHeadless --reporters=verbose
 ✓ |vt-app (chromium)| src/app/tasks/task-api.spec.ts > TaskApi > GET /api/tasks の結果を返す 15ms
 …(10件すべて ✓)
 Test Files  6 passed (6)
      Tests  10 passed (10)
   Duration  4.47s (setup 41%, worker 25%, import 17%, tests 16%)

同じ10テストが、コードを1行も変えずにブラウザでも通りました。

jsdom とブラウザモードの比較

同じマシン(2コア)で繰り返し実行したときの Vitest の Duration です。 他の処理と同時に動かしていたため、値には幅があります。

項目 jsdom(既定) ブラウザモード(Chromium)
Vitest の Duration 1.19〜2.86 秒(5回) 3.33〜4.47 秒(3回)
ng test 全体の実時間 約4〜10 秒 約8〜12 秒
追加パッケージ なし(jsdom は ng new 済み) @vitest/browser-playwright、playwright、ブラウザ本体
得意なこと ロジック・DOM の基本操作を素早く確認 レイアウト、scrollTo、実ブラウザ固有の API
表示 |vt-app| |vt-app (chromium)|

普段は jsdom で素早く回し、レイアウトやブラウザ固有の API が関わるテストだけブラウザモードで確認する という使い分けがしやすい結果でした。

Karma/Jasmine から Vitest へ移行する(実行結果つき)

Angular 22 には、Karma 設定を移行する migrate-karma-to-vitest と、テストコードを書き換える refactor-jasmine-vitest の2つの公式スキーマがあります。 検証用に、次の構成の Karma プロジェクトを用意して実行しました。

  • @angular/build:karma ビルダー、karma.conf.js あり(customLaunchers で ChromeHeadless に --no-sandbox)
  • polyfills: ["zone.js", "zone.js/testing"]
  • Jasmine の createSpyObj・spyOn・jasmine.any・calls.count()・fakeAsync/tick・xit・toBeTrue を使ったテスト

移行前は Karma で TOTAL: 5 SUCCESS(6件中1件は xit でスキップ)でした。

手順1:設定を移行する(migrate-karma-to-vitest)

npx ng update @angular/cli --name=migrate-karma-to-vitest
    Project "karma-app" uses the "kjhtml" reporter. This has not been automatically mapped. ...
    Project "karma-app" uses a custom Karma configuration file "karma.conf.js". Tests have been migrated to use Vitest, but you may need to manually migrate custom settings ...
    --- Karma to Vitest Migration Summary ---
    Projects migrated: 1
UPDATE tsconfig.spec.json (409 bytes)
UPDATE angular.json (2723 bytes)
UPDATE package.json (1059 bytes)
  Migration completed (3 files modified).

このスキーマが変えた内容は次のとおりです。

変更前 変更後
"builder": "@angular/build:karma" "builder": "@angular/build:unit-test"、"runner": "vitest"
test の polyfills build に testing 構成を作って移動し、"buildTarget": ":build:testing"
karmaConfig のレポーター "reporters": ["default"]、"coverageReporters": ["html", "text-summary"]
tsconfig.spec.json の "types": ["jasmine"] "types": ["vitest/globals"]
devDependencies vitest を追加

対象は @angular/build:application でビルドしているアプリケーションだけです。 それ以外は「先に application ビルダーへ移行してください」と表示されてスキップされます。karma.conf.js はカスタム設定があったため自動では削除されず、手動移行の対象として一覧に出ました。

手順2:jsdom を入れる

このままテストを実行すると、DOM 環境が無いと表示されました。

The following packages are required but were not found:
  - A DOM environment is required for non-browser tests. Please install either "jsdom" or "happy-dom".
npm i -D jsdom

手順3:テストコードを変換する(refactor-jasmine-vitest)

npx ng g @schematics/angular:refactor-jasmine-vitest --verbose
    Processing: /src/app/legacy/stock-list.spec.ts
      - L8: Transformed type `jasmine.SpyObj` to `MockedObject`.
      - L11: Transformed `jasmine.createSpyObj()` to an object literal with `vi.fn()`.
      - L12: Transformed spy strategy `.and.returnValue()` to `.mockReturnValue()`.
      - L36: Transformed `spyOn` to `vi.spyOn`.
      - L43: Transformed `xit` to `it.skip`.
      - L44: Transformed matcher ".toBeTrue()" to ".toBe()".
      (ほか5件)
    - 1 file(s) transformed.

変換結果のレポート(jasmine-vitest-日時.md)もプロジェクト直下に作られます。 このスキーマのオプションは次のとおりです。

オプション 既定 内容
--fake-async false fakeAsync / tick を Vitest のフェイクタイマーに変換
--add-imports false vitest からの明示的な import を追加
--browser-mode false toHaveClass をブラウザモードのマッチャーとして残す
--include 全体 特定のファイル・ディレクトリだけ変換
--project 自動 対象プロジェクト
--file-suffix .spec.ts テストファイルの接尾辞
--report true 変換レポートの Markdown を出力
--verbose false 変換内容を1行ずつ表示

手順4:残った2つのエラーを直す

変換後に実行すると、型エラーが1つ出ました。 createSpyObj から変換されたオブジェクトが、private フィールドを持つクラスの型 MockedObject<StockService> と合わないためです。

✘ [ERROR] TS2322: Type '{ list: Mock<Procedure>; log: Mock<Procedure>; }' is not assignable to type 'MockedObject<StockService>'.
  Property 'http' is missing in type '{ list: Mock<Procedure>; log: Mock<Procedure>; }' but required in type 'StockService'.

直し方は、変換後のオブジェクトリテラルの末尾に as unknown as MockedObject<StockService> を付けることです。 使うメソッドだけを持つモックとして扱えます。

次に、fakeAsync のテストだけが失敗しました。

 × 500ms 後にメッセージが出る 13ms
Error: Expected to be running in 'ProxyZone', but it was not found.

fakeAsync の直し方は2通りあり、どちらも検証で通りました。

方法 やること 向いている場面
A. fakeAsync をフェイクタイマーに変換(推奨) ng g @schematics/angular:refactor-jasmine-vitest --fake-async zone.js への依存を減らしたい
B. zone.js のパッチを足す build の testing 構成の polyfills に "zone.js/plugins/vitest-patch" を追加 まず全部を通してから少しずつ直したい

方法 A の変換結果(差分)です。

// describe の先頭に追加される
beforeEach(() => {
  vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true });
});
afterEach(() => {
  vi.useRealTimers();
});

// fakeAsync(() => { ...; tick(500); ... }) が次の形に変わる
it('500ms 後にメッセージが出る', async () => {
  fixture.detectChanges();
  await vi.advanceTimersByTimeAsync(500);
  fixture.detectChanges();
  expect(fixture.nativeElement.querySelector('.msg').textContent).toBe('読み込み完了');
});

変換後は Tests 5 passed | 1 skipped (6) で、Karma のときと同じ結果になりました。

方法 B の polyfills は、zone.js・zone.js/testing・zone.js/plugins/vitest-patch の3つがそろって初めて動きました。 検証で足りないものがあったときのエラーは次のとおりです。

polyfills の指定 結果
["zone.js/plugins/vitest-patch"] のみ ReferenceError: Zone is not defined
["zone.js", "zone.js/plugins/vitest-patch"] Error: Missing ProxyZoneSpec
["zone.js", "zone.js/testing", "zone.js/plugins/vitest-patch"] 成功
zone.js なし(v22 の新規プロジェクト) Error: zone-testing.js is needed for the fakeAsync() test helper but could not be found.

unit-test ビルダーの options には polyfills を書けません(Unknown option "polyfills" になります)。build ターゲットに構成を作り、buildTarget で指す形にします。

"build": { "configurations": { "testing": {
  "polyfills": ["zone.js", "zone.js/testing", "zone.js/plugins/vitest-patch"]
} } },
"test": { "builder": "@angular/build:unit-test", "options": { "buildTarget": ":build:testing" } }

ゾーンレスのアプリで zone.js を読み込むと、whenStable で表示を待つテストが通らなくなりました(expected '' to contain 'Hello, vt-app')。同じテストが zone.js なしでは通っていたので、zone.js の有無で TestBed の変更検知の動きが変わったと考えられます。--providers-file でゾーンレスを明示すると、fakeAsync のテストも含めて全部通りました。

// src/test-providers.ts(angular.json の test.options に "providersFile": "src/test-providers.ts")
import { provideZonelessChangeDetection } from '@angular/core';

export default [provideZonelessChangeDetection()];

v22 で既存コンポーネントのテストが変わる点

Angular 22 ではコンポーネントの変更検知の既定が OnPush になりました(型定義の ChangeDetectionStrategy に「OnPush is enabled by default」と記載)。検証では、setTimeout の中で普通のフィールドを書き換えるコンポーネントのテストが、fixture.detectChanges() を呼んでも表示が変わらず失敗しました。以前と同じ動きにしたいコンポーネントには changeDetection: ChangeDetectionStrategy.Eager を指定するか、値を signal で持つ形に直すと通ります。

Jasmine → Vitest 書き換え対応表

refactor-jasmine-vitest のソース(22.2.0)で確認した変換内容です。 「自動」はスキーマが変換するもの、「手動」は TODO コメントが付くものです。

Jasmine Vitest 変換
spyOn(obj, 'm') vi.spyOn(obj, 'm') 自動
jasmine.createSpy('name') vi.fn().mockName('name') 自動
jasmine.createSpyObj('S', ['a', 'b']) { a: vi.fn(), b: vi.fn() } 自動(型は要確認)
jasmine.SpyObj<T> MockedObject<T> 自動
.and.returnValue(v) .mockReturnValue(v) 自動
.and.callFake(fn) .mockImplementation(fn) 自動
.and.callThrough() (削除。vi.spyOn は既定で元の処理を呼ぶ) 自動
.and.throwError(e) .mockImplementation(() => { throw e }) 自動
.and.resolveTo(v) / .and.rejectWith(e) .mockResolvedValue(v) / .mockRejectedValue(e) 自動
spy.calls.count() vi.mocked(spy).mock.calls.length 自動
spy.calls.mostRecent().args vi.mocked(spy).mock.lastCall 自動
spy.calls.reset() .mockClear() 自動
jasmine.any(T) / jasmine.objectContaining(o) expect.any(T) / expect.objectContaining(o) 自動
toBeTrue() / toBeFalse() toBe(true) / toBe(false) 自動
toHaveBeenCalledOnceWith(x) toHaveBeenCalledTimes(1) + toHaveBeenCalledWith(x) 自動
fit / fdescribe it.only / describe.only 自動
xit / xdescribe it.skip / describe.skip 自動
jasmine.clock().install() / .tick(ms) vi.useFakeTimers() / vi.advanceTimersByTime(ms) 自動
fakeAsync(() => { tick(ms) }) async () => { await vi.advanceTimersByTimeAsync(ms) } --fake-async 指定時に自動
jasmine.addMatchers(...) expect.extend(...) 手動
expectAsync(p).toBePending() 直接の対応なし 手動

よくある質問

Q. 新規プロジェクトで Karma を選ぶことはできますか? A. できます。ng new --test-runner=karma で Karma 構成になります(この場合は @angular/build:unit-test ビルダーに "runner": "karma" が付く形でした)。既定は vitest です。

Q. fakeAsync はもう使えないのですか? A. zone.js と zone.js/testing、zone.js/plugins/vitest-patch を polyfills に入れれば Vitest でも動きます(検証済み)。ただし公式ガイドはネイティブの async と Vitest のフェイクタイマーへの移行を強く勧めており、refactor-jasmine-vitest --fake-async で自動変換できます。

Q. jsdom とブラウザモードはどちらを使えばよいですか? A. まずは jsdom で十分です。検証では同じテストが jsdom で約1.2〜2.9秒、ブラウザモードで約3.3〜4.5秒でした。レイアウト計算や scrollTo など、jsdom に無い API を使うテストだけブラウザモードで動かすと、速さと確かさを両立できます。

まとめ

  • Angular 22 は ng new の時点で Vitest が既定。ng test だけでビルドとテストが動く
  • ゾーンレスが既定なので、表示の確認は await fixture.whenStable()、signal の input は setInput
  • HTTP は provideHttpClient() + provideHttpClientTesting()、ルーティングは RouterTestingHarness
  • タイマーは vi.useFakeTimers()。フェイクタイマー中は whenStable ではなく TestBed.tick() で進める
  • カバレッジは @vitest/coverage-v8 + --coverage、ブラウザモードは @vitest/browser-playwright + --browsers(既存 Chromium は CHROME_BIN)
  • Karma からは migrate-karma-to-vitest → jsdom 追加 → refactor-jasmine-vitest --fake-async の順で移行でき、残りは型の調整程度

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular テスト Vitest 入門|v22の書き方・移行・実行結果つき」

株式会社RJCでは、Vitest のような新しいテストの仕組みも実際に動かして確かめ、その結果をチームで振り返っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?