- Angular
- TypeScript
- 技術 💻
Angular テスト Vitest 入門|v22の書き方・移行・実行結果つき
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期連続の黒字決算。