はじめに
以前こちらの記事で書いた github actions のパイプラインの高速化の検討について、高速テストフレームワークとして期待されている Vitest についても検証したと述べたのですが、
今回はその Vitest に関する移行検証記事です。
-
github actions の job を高速にするために取った対策
- ※上の記事では vitest の方が遅かった記載をしていますが、今回テストを再実行してみたところ vitest の方が速度が速かったため、裏で何かしら別のプロセスが動いていたかもしれないです。
Vitest とは
- vitest.dev
- Vite 環境のために開発された高速テストフレームワーク
- Jest と互換性がある
- まだメジャーバージョンではない(記事執筆時
v0.22.1
)
結論
-
Vitest への移行結果
Jest に比べて Vitest の方が 10%少しテスト実行速度が上昇していることが確認できました。 -
移行量
項目 実績 修正ファイル数 388 ファイル 修正テスト数 2067 ケース -
テスト実行時間比較
Vitest Jest 280.07s 317.90s
移行前環境
- React v17(CRA ベースで初期構築)
- Typescript
- npm ベース
- Sonarqube による静的解析を利用するため、
jest-sonar-reporter
を利用 -
styled-component
導入済 -
testing-library
を使ったテスト -
Enzymeのshallow
を使った snapshot テスト -
Enzymeのmount
を使った snapshot テスト
移行時の Vitest バージョン
{
"happy-dom": "^5.0.0",
"vite": "^2.9.9",
"vite-tsconfig-paths": "^3.5.0",
"vitest": "^0.14.2",
"vitest-sonar-reporter": "^0.2.1",
"c8": "^7.11.3"
}
方針
- 既存で Success となるテストは移行後も全て Success となること
- 基本的には Vitest 公式の MigrationGuide に従って移行していく
手順
ライブラリインストール
npm i -D vitest #vitest の core
npm i -D @vitejs/plugin-react #vitest の react 対応
npm i -D happy-dom #vitest に直接依存はないが、js-dom 互換で SSR にも対応、パフォーマンスも倍以上速い らしい happy-dom を導入してみる
npm i -D vite #setup.ts の defineConfig のためにインストール
npm i @testing-library/jest-dom/extend-expect -D
npm i vite-tsconfig-paths -D #read from tsconfig paths and baseUrl
vite.config.ts の作成
-
vite.config.ts
vite.config.tsimport { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [react(), tsconfigPaths()], test: { globals: true, environment: "happy-dom", // jsdomの代わりにhappy-domを設定した setupFiles: ["./src/setup.ts", "./src/setupTests.ts"], //Jestで使っていたテスト設定をセットアップ }, });
package.json に Vitest 起動用の script を入れる
-
vitest run
で Vitest を実行できます -
Vitest の場合は並列実行数(worker)はデフォルトだと、PC が許す限りのメモリ分並列実行させます
- worker の数を制御したい場合は
VITEST_MIN_THREADS=6 VITEST_MAX_THREADS=6
で設定することができます - 今回は元々自分が Jest で設定していた worker の数と同じにしました(実行比較のため)
- worker の数を制御したい場合は
-
before(cra ベースの Jest)
package.json{ "scripts": { "test": "react-scripts test --env=jsdom --maxWorkers=6 --watchAll=false" } }
-
after
package.json{ "scripts": { "test": "VITEST_MIN_THREADS=6 VITEST_MAX_THREADS=6 vitest run" } }
これで基本的には Jest->Vitest への移行は完了です。
ピュアな Jest であれば Vitest でそのまま動くと思います。
テスト実行方法について
単体での実行
-
npm run test hoge.test.ts
-
※Jest のようにパス付きファイル指定はできないもよう
以下実行できませんでした
npm run test ./src/pages/hoge/hoge.test.ts
ディレクトリ単位での実行
npm run test --dir ./src/pages/hoge/
ブランチ間の差分の実行
npm run test --changed origin/master
話がちょっと逸れますがこの機能めちゃめちゃ重要だと思います。
CICD パイプラインのテスト実行 step で威力を発揮します。
例えば PullRequest での毎回のテスト実行の際、このオプションをつけるだけで修正差分に関連するテストだけを自動で選択して実行してくれるので、
テスト実行時間やパイプラインの時間消費の効率が上がります。
vscode の設定
vscode のデバッガ設定
- launch.json
- node_modules の場所を指定するパスに注意
launch.json
{ "version": "0.2.0", "configurations": [ { "name": "vitest:Debug Current Test File", "type": "pwa-node", "request": "launch", "autoAttachChildProcesses": true, "skipFiles": ["<node_internals>/**", "**/node_modules/**"], "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", "args": ["run", "${relativeFile}"], "smartStep": true, "console": "integratedTerminal" } ] }
- node_modules の場所を指定するパスに注意
vscode の run 設定変更(settings.json)
- Jest
- jest-Runner extension をインストールする
- Vitest
- vitest-Runner extension をインストールする
coverage 設定の移行
cra ベースの場合だと coverage の設定は package.json で記載していると思いますが、それをvitest.config.ts
へ移行します
-
Jest
package.json{ "jest": { "collectCoverageFrom": [ "src/pages/**/*.{js,jsx,ts,tsx}", "src/common/**/*.{js,jsx,ts,tsx}", "src/index.tsx" ] } }
-
coverage
フォルダの中にカバレッジファイルができる(lcov
)
-
-
Vitest
-
Vitest での coverage 生成は c8 というライブラリ を使う
npm i --save-dev c8
-
[追記:20230130 vitest:0.28.3] Vitest での coverage 生成は @vitest/coverage-c8 というライブラリ を使う
npm i --save-dev @vitest/coverage-c8
vite.config.tsexport default defineConfig({ plugins: [react(), tsconfigPaths()], test: { coverage: { reporter: ["text", "lcov"], reportsDirectory: "coverage", include: [ "src/pages/**/*.{js,jsx,ts,tsx}", "src/common/**/*.{js,jsx,ts,tsx}", "src/index.tsx", ], }, }, });
-
text
が標準出力、lcov
が output ファイルを出力する
-
-
sonar-reporter の設定
-
Jest
-
起動時の引数に
--coverage --testResultsProcessor jest-sonar-reporter
を入れるpackage.json{ "scripts": { "test:ci": "react-scripts test --env=jsdom --maxWorkers=6 --watchAll=false --coverage --testResultsProcessor jest-sonar-reporter" } }
-
jest-sonar-reporter
を使うpackage.json{ "jestSonar": { "reportPath": "report", "reportFile": "test-report.xml", "reportedFilePath": "relative", "relativeRootDir": "<rootDir>/../", "indent": 4 } }
-
-
Vitest
vitest-sonar-reporter
を使う- vitest-sonar-reporter
npm install --save-dev vitest-sonar-reporter
- vite.config.ts
vite.config.ts
export default defineConfig({ plugins: [react(), tsconfigPaths()], test: { reporters: ["default", "vitest-sonar-reporter"], outputFile: "report/test-report.xml", }, });
-
reporters: ["default"]
が標準出力を表す
-
Jest との差分
Mock の宣言
jest.mock
をvi.mock
に置き換える必要があります。
-
Jest
jest.mock(...)
-
Vitest
import { vi } from 'vitest'; vi.mock(...)
instance の型の明示的な設定
Typescript における mock の型の明示的な設定に違いがあります
- jest
(MediaClient.getHoge as jest.Mock).mock.calls[0][0]
- vitest
(MediaClient.getHoge as SpyInstanceFn).mock.calls[0][0]
- [追記:20230130 vitest:0.28.3]
(MediaClient.getHoge as unknown as SpyInstance)
automocking の未サポート
-
jest における automocking
<root>/__mocks__
は現状サポートしていません- We don't support automocking. You need to manually call vi.mock("path"). Or you can call it once in setupFiles.
-
setup.ts
で明示的に mock をすることによって、各テストの実行前に mock がされるようになるのでそれによって回避できます。
-
moment
を automocking していた例-
before
-
<root>/__mocks__/moment.ts
で mock していた
-
-
after
-
setup.ts
setup.tsimport "@testing-library/jest-dom/extend-expect"; import { vi } from "vitest"; import * as moment from "./__mocks__/moment"; vi.doMock("moment", () => moment);
-
-
default を spyon できない
-
[追記:20230130 vitest:0.28.3] 新バージョンで解消済み
-
Jest での default spyon
import * as getHogeApi from "../apicall/getHoge"; const mockGetHogeApi = jest.spyOn(getHogeApi, "default");
これを Vitest でそのまま実行すると以下のエラーになります
TypeError: Cannot redefine property: default ❯ src/pages/hoge.test.tsx:18:39 18| const mockGetHogeApi = vi.spyOn(getHogeApi, 'd… | ^
-
Vitest での置き換え
-
vi.mock
とvi.mocked
を利用して置き換えますimport { vi } from "vitest"; vi.mock("../apicall/getHoge", async () => { const actual: any = await vi.importActual("../apicall/getHoge"); const mocked = {}; for (const key of Object.keys(actual)) { if (typeof actual[key] === "function") { mocked[key] = vi.fn(actual[key]); } else { mocked[key] = actual[key]; } } return mocked; }); import * as getReservePresetListApi from "../apicall/getHoge"; const mockGetReservePresetListApi = vi.mocked( getReservePresetListApi ).default;
-
-
参考
-
Cannot spyOn default export of module
- 最新だと
tinyspy
を使うことで回避できる模様(未検証)
- 最新だと
-
Cannot spyOn default export of module
mock のやり方
-
default
が必要なケースがあります -
Jest
jest.mock("moment", () => () => ({ format: () => "2019–09–30T10:11:59+09:00", }));
-
Vitest
vi.mock("moment", () => { return { default: () => ({ format: () => "2019–09–30T10:11:59+09:00", }), }; });
Done callback が非推奨
-
test 自体は通りますが、Vitest だとエラー判定されます。
it(`test`, (done) => { // GIVEN // WHEN when(); setTimeout(() => { try { // THEN expect(); done(); // TypeError: An argument for 'value' was not provided. } catch (e) { done(e); } }, 300); });
Serialized Error: { "errors": [ [Error: done() callback is deprecated, use promise instead], [Error: done() callback is deprecated, use promise instead], ], }
-
回避策
-
-
上のドキュメントの実装でも通せますが、Typescript では done の引数エラーが発生します
it(`test`, () => new Promise((done) => { // GIVEN // WHEN when(); setTimeout(() => { try { // THEN expect(); done(); // TypeError: An argument for 'value' was not provided. } catch (e) { done(e); } }, 300); }));
-
new Promise
に型をつければエラーは消えますが、今度は引数ありのほうが逆にエラーとなりますit(`test`, () => new Promise<void>((done) => { // GIVEN // WHEN when(); setTimeout(() => { try { // THEN expect(); done(); // pass } catch (e) { done(e); // TypeError: An argument for 'value' was not provided. } }, 300); }));
-
結論としては
done
とは別にreject
を使用しますit(`test`, () => new Promise<void>((done, reject) => { // GIVEN // WHEN when(); setTimeout(() => { try { // THEN expect(); done(); } catch (e) { reject(e); } }, 300); }));
-
-
リテラル表記のit.each
のエラー
- Jest での
it.each
テストにてエラーが発生
it.each`
input | expFromTime | expToTime
${"morning"} | ${"04:00"} | ${"10:00"}
${"noon"} | ${"10:00"} | ${"17:00"}
${"night"} | ${"17:00"} | ${"03:59"}
${"all"} | ${null} | ${null}
${"hoge"} | ${null} | ${null}
`(
`timePeriodテスト
timePeriod:$inputのケース`,
async ({ input, expFromTime, expToTime }) => {
// GIVEN
// WHEN
// THEN
);
-
vitest-eachに沿って書き換える
-
公式通り修正
it.each([ ['morning', '04:00', '10:00'], ['noon', '10:00', '17:00'], ['night', '17:00', '03:59'], ['all', null, null], ['hoge', null, null], ]) ( `timePeriodテスト timePeriod:$sのケース`, async ( input, expFromTime, expToTime ) => { // GIVEN // WHEN // THEN );
- printf 形式のパラメータ指定
%s: string %d: number %i: integer %f: floating point value %j: json %o: object %#: index of the test case %%: single percent sign ('%')
-
require による import はサポート外
-
require
による import はサポート外としている
試験的な導入による warning
メジャーバージョンではないため出ている
(node:46501) ExperimentalWarning: buffer.Blob is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:46501) ExperimentalWarning: buffer.Blob is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
- 試験的な導入なので warning が出る。無視で OK です
エラーの対処一覧
外部ライブラリの default モックでエラー
-
外部ライブラリである
superagent
をモックしていたのですがエラーになりましたvi.mock("superagent", () => ({ put: vi.fn().mockReturnValue({ set: vi.fn().mockReturnValue({ send: vi.fn().mockReturnValue({ end: vi.fn().mockImplementation((cb) => { cb("", {}); }), }), }), }), }));
Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ Test Files 1 failed | 1 passed (2) Tests 1 failed | 2 passed (3) Time 9.58s (in thread 5.02s, 190.59%) ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Vitest caught 1 unhandled error during the test run. This might cause false positive tests. Please, resolve all the errors to make sure your tests are not affected. ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ TypeError: __vite_ssr_import_0__.default.put(...).set is not a function ❯ src/apicall/hoge.ts:56:9 54| if (isOK(result)) { 55| request.put(result.data.url) 56| .set('Content-Type', contentType) | ^ 57| .send(file) 58| .end((error, uploadResult) => { ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ error Command failed with exit code 1.
-
以下のように書き換える必要がある(default の下に設定する)
vi.mock("superagent", () => { return { default: { put: vi.fn().mockReturnValue({ set: vi.fn().mockReturnValue({ send: vi.fn().mockReturnValue({ end: vi.fn().mockImplementation((cb) => { cb("", {}); }), }), }), }), }, }; });
vite.config.ts にてenvironment: happy-dom
を指定すると以下のエラーが発生する
-
styled-componentsとの併用で発生
-
happy-dom 側の問題みたいでした 5/24 時点
-
happy-dom:5.1.0
で解消された模様(未検証)
The above error occurred in the <select> component: at select at div at div at I (/.../node_modules/styled-components/dist/styled-components.cjs.js:1:19220) at div at I (/.../node_modules/styled-components/dist/styled-components.cjs.js:1:19220) at div at div at div at ModalPortal (/.../node_modules/react-modal/lib/components/ModalPortal.js:70:5) at Modal (/.../node_modules/react-modal/lib/components/Modal.js:73:5) at PresetDialogLayout (/.../src/pages/Hoge/index.tsx:17:3) at LoadPresetDialog (/.../src/pages/Puga.tsx:19:3) at WrapperComponent (/.../node_modules/@wojtekmaj/enzyme-adapter-utils/build/createMountWrapper.js:112:7) Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries. ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Vitest caught 1 unhandled error during the test run. This might cause false positive tests. Please, resolve all the errors to make sure your tests are not affected. ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ TypeError: Cannot read properties of undefined (reading 'length') ❯ updateOptions node_modules/react-dom/cjs/react-dom.development.js:1880:37 ❯ postMountWrapper$2 node_modules/react-dom/cjs/react-dom.development.js:1950:5 ❯ setInitialProperties node_modules/react-dom/cjs/react-dom.development.js:9157:7 ❯ finalizeInitialChildren node_modules/react-dom/cjs/react-dom.development.js:10201:3 ❯ completeWork node_modules/react-dom/cjs/react-dom.development.js:19470:17 ❯ completeUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:22812:16 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:22787:5 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:22707:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:22670:7 ❯ performSyncWorkOnRoot node_modules/react-dom/cjs/react-dom.development.js:22293:18 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ error Command failed with exit code 1.
-
vitest.config.ts
でenvironment:jsdom
にすると解消されました
mapbox-gl のモックでエラー
- jest で特に問題は出ていませんでしたが、vitest でエラーが発生しました
TypeError: The "obj" argument must be an instance of Blob. Received an instance of Blob
❯ define node_modules/mapbox-gl/dist/mapbox-gl.js:25:41
❯ node_modules/mapbox-gl/dist/mapbox-gl.js:35:1
❯ node_modules/mapbox-gl/dist/mapbox-gl.js:3:81
❯ Object.<anonymous> node_modules/mapbox-gl/dist/mapbox-gl.js:6:2
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: {
"code": "ERR_INVALID_ARG_TYPE",
}
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
-
globalThis に Blob を追加してあげると回避できます
-
setup.ts
import { Blob } from "buffer"; (globalThis.Blob as any) = Blob;
-
-
[追記:20230130 vitest:0.28.3] Blob is not a constructor
- 上記の設定をしてテスト実行すると
TypeError: Blob is not a constructor
というエラーが発生した - 解決
- setup.tsのBlobの設定を戻し、createObjectURLのモックを設定する
// import { Blob } from 'buffer' // (globalThis.Blob as any) = Blob; function noOp () { } if (typeof window.URL.createObjectURL === 'undefined') { Object.defineProperty(window.URL, 'createObjectURL', { value: noOp}) }
- setup.tsのBlobの設定を戻し、createObjectURLのモックを設定する
- 上記の設定をしてテスト実行すると
global の__mocks__
を使うために setup.ts で mock しようとしたらエラー
-
ある一定以上の数をsetup.tsで mock しようとするとエラーになりました
ReferenceError: Cannot access '__vite_ssr_import_5__' before initialization ❯ src/setup.ts:3:48 2| import { vi } from 'vitest'; 3| import * as moment from './__mocks__/moment'; 4| import * as momentTimezone from './__mocks__/moment-timezone.js'; | ^ 5| import * as superagent from './__mocks__/superagent'; 6| import * as leaflet from './__mocks__/leaflet.js'; ❯ async src/__mocks__/leaflet.js:5:31 ❯ async src/setup.ts:16:31
-
ReferenceError: Cannot access 'vite_ssr_import_0' before initialization when using mocks
-
mock
ではなくdoMock
で回避できます-
修正前
setup.tsimport "@testing-library/jest-dom/extend-expect"; import { vi } from "vitest"; import * as moment from "./__mocks__/moment"; import * as momentTimezone from "./__mocks__/moment-timezone"; import * as superagent from "./__mocks__/superagent"; vi.mock("moment", () => moment); vi.mock("moment-timezone", () => momentTimezone); vi.mock("superagent", () => superagent);
-
修正後
setup.tsimport "@testing-library/jest-dom/extend-expect"; import { vi } from "vitest"; import * as moment from "./__mocks__/moment"; import * as momentTimezone from "./__mocks__/moment-timezone"; import * as superagent from "./__mocks__/superagent"; import * as leaflet from "./__mocks__/leaflet"; import { Blob } from "buffer"; (globalThis.Blob as any) = Blob; vi.doMock("moment", () => moment); vi.doMock("moment-timezone", () => momentTimezone); vi.doMock("superagent", () => superagent); vi.doMock("leaflet", () => leaflet);
-
-
ArrayMove の mock エラー
TypeError: Cannot assign to read only property 'arrayMoveImmutable' of object '[object Module]'
import * as ArrayMove from "array-move";
vi.spyOn(ArrayMove, "arrayMoveImmutable");
import * as ArrayMove from "array-move";
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
vi.mocked(ArrayMove).arrayMoveImmutable;
jquery の import エラー(TypeError: default is not a function)
-
import $ from 'jquery';
-
TypeError: default is not a function
というエラーが発生
-
-
Jest
jest.mock("jquery", () => {
const m$ = {
DataTable: jest.fn().mockReturnValue({
on: jest.fn(),
destroy: jest.fn(),
}),
parents: jest.fn().mockReturnValue([
{
classList: {
add: jest.fn(),
},
},
]),
};
return jest.fn(() => m$);
});
- Vitest
- default をつけてあげる
vi.mock("jquery", () => {
const m$ = {
DataTable: vi.fn().mockReturnValue({
on: vi.fn(),
destroy: vi.fn(),
}),
parents: vi.fn().mockReturnValue([
{
classList: {
add: vi.fn(),
},
},
]),
};
return { default: vi.fn(() => m$) };
});
snapshotSaved がタイムアウトエラーを起こす
Vitest caught 1 unhandled error during the test run. This might cause false positive tests.
Please, resolve all the errors to make sure your tests are not affected.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Error ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Error: Errors occurred while running tests. For more information, see serialized error.
❯ Object.runTests file:/.../node_modules/vitest/dist/chunk-vite-node-externalize.0094db73.js:7191:17
❯ processTicksAndRejections node:internal/process/task_queues:96:5
❯ async file:/.../node_modules/vitest/dist/chunk-vite-node-externalize.0094db73.js:10442:9
❯ async Vitest.runFiles file:/.../node_modules/vitest/dist/chunk-vite-node-externalize.0094db73.js:10452:12
❯ async Vitest.start file:/.../node_modules/vitest/dist/chunk-vite-node-externalize.0094db73.js:10379:5
❯ async startVitest file:/.../node_modules/vitest/dist/chunk-vite-node-externalize.0094db73.js:11102:5
❯ async start file:/.../node_modules/vitest/dist/cli.js:665:9
❯ async CAC.run file:/.../node_modules/vitest/dist/cli.js:661:3
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: {
"errors": [
[Error: [birpc] timeout on calling "snapshotSaved"],
],
}
-
一つづつモックを外してみたら
global.Math
のモックが悪さをしていたっぽいvi.spyOn(global.Math, "random").mockReturnValue(0.123456789);
- Math.random を使うのをやめ,
crypto
によるランダム値の取得に変更
- Math.random を使うのをやめ,
-
実装自体を変更しています
※余談ですが、Math.random については SonarQube としても Security の項目でリスクがあると指摘しており、crypto でのランダム値取得が推奨されています。
個別に対応した移行ケース一覧
jest である Mock機能 が vitest にない
TypeError: vi.genMockFromModule is not a function
❯ src/__mocks__/leaflet.ts:6:24
4| import L from 'leaflet';
5|
6| const LeafletMock = vi.genMockFromModule('leaflet')
| ^
7|
8| class ControlMock extends LeafletMock.Control {
❯ async src/setup.ts:11:31
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
-
jest.createMockFromModule()/jest..getMockFromModule(moduleName) -> use vi.mock or vi.importMock
に変わっている -
Jest
const LeafletMock = vi.genMockFromModule("leaflet");
-
Vitest
const LeafletMock = await vi.importMock("leaflet");
enzyme snapshot
-
Jest での Enzyme Snapshot テストを Vitest でそのまま実行すると
exports['test > snapshot'] = 'ShallowWrapper {}';
となってしまう -
どうやら jest.config.js で設定していた
snapshotSerializers: ['enzyme-to-json/serializer'],
が効かなくなっている模様 -
公式ドキュメント
- enzyme の snapshot には対応していない
- jest.snapshot は対応してる
- Jest Snapshot support
-
結論:enzyme-to-json を直接テストの中で使えばいける
-
修正前
it("snapshot", () => { // GIVEN // WHEN target = shallow(<Hoge />); // THEN expect(target).toMatchSnapshot(); });
-
修正後
import toJson from "enzyme-to-json"; it("snapshot", () => { // GIVEN // WHEN target = shallow(<Hoge />); // THEN expect(toJson(target)).toMatchSnapshot(); });
-
describe でテストケースをグルーピングしており、 その下で snapshot を使用している場合、snap ファイルのケース名も移行前と若干変わっているので合わせる必要があります
- そうしないと既存のテストケースと同じ snapshot でテストが検証してくれない
- 修正前
exports[`snapshot testcase`] = `
- 修正後
exports[`snapshot > testcase`] = `
- 修正前
- snap の内容も若干変わっていたりするので確認しながら直す必要があります
- 修正が必要な例 1(型が変わっている)
- onClick={[MockFunction]} + onClick={[Function]}
- 修正が必要な例 2(Object がいらない)
style={ - Object { + { \"display\": \"none\", } }
- 修正が必要な例 1(型が変わっている)
- そうしないと既存のテストケースと同じ snapshot でテストが検証してくれない
each + done callback パターン
each による繰り返しテストと、非同期の done テストを同時に適用したい
- Jest
it.each`
args1 | args2 | expected
${false} | ${0} | ${''}
`('jest each done test',
({ args1, args2, expected }, done: any) => {
- Vitest
it.each([
[false,0,''],
])('vitest each done test',
( args1, args2, expected ) => new Promise<void>((done, reject) => {
location の mock 設定
-
Jest
window.history.pushState({}, '', '/pages/hoge');
- Jest で
window.location.pathname = '/hoge'
をしてもちゃんと反映されない
- Jest で
-
Vitest
window.location.pathname = '/pages/hoge'
-
[追記:20230130 vitest:0.28.3]
vi.stubGlobal('location', { pathname: '/pages/hoge' });
-
[追記:20230130 vitest:0.28.3]
sonar-reporter にてRangeError: Maximum call stack size exceeded
が発生
-
"vitest-sonar-reporter": "^0.2.1"
で解消されました
Test Files 388 passed (388)
Tests 2067 passed (2067)
Time 506.92s (in thread 234.93s, 215.78%)
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Error ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
RangeError: Maximum call stack size exceeded
❯ generateXml node_modules/vitest-sonar-reporter/dist/xml.js:58:12
❯ SonarReporter.onFinished node_modules/vitest-sonar-reporter/dist/sonar-reporter.js:37:35
❯ node_modules/vitest/dist/chunk-vite-node-externalize.6b27b039.mjs:10640:51
❯ Vitest.report node_modules/vitest/dist/chunk-vite-node-externalize.6b27b039.mjs:10638:38
❯ node_modules/vitest/dist/chunk-vite-node-externalize.6b27b039.mjs:10475:18
❯ processTicksAndRejections node:internal/process/task_queues:96:5
❯ Vitest.runFiles node_modules/vitest/dist/chunk-vite-node-externalize.6b27b039.mjs:10479:12
❯ Vitest.start node_modules/vitest/dist/chunk-vite-node-externalize.6b27b039.mjs:10406:5
❯ startVitest node_modules/vitest/dist/chunk-vite-node-externalize.6b27b039.mjs:11140:5
- 調査の履歴
-
RangeError: Maximum call stack size exceeded
-
"vitest": "^0.13.0",
で発生- v0.14.2 で解消予定
- 直ってない。。。
- どうやら
vitest-sonar-reporter
のバグっぽい- issue 書いた
-
RangeError: Maximum call stack size exceeded #12
-
"vitest-sonar-reporter": "^0.2.1"
で解消
-
-
RangeError: Maximum call stack size exceeded #12
- issue 書いた
- どうやら
- 直ってない。。。
- v0.14.2 で解消予定
-
- Why does Jest error with Maximum Call Stack Size Exceeded
- Jest
toMatchSnapshot
causes "Maximum call stack size exceeded"
-
RangeError: Maximum call stack size exceeded
[追記:20230130 vitest:0.28.3] Request is not defined
-
問題
- Three.jsを使用しているコンポーネントのテストにて
ReferenceError: Request is not defined
エラーが発生
- Three.jsを使用しているコンポーネントのテストにて
-
解決
- vitest-fetch-mockをインストールする
[追記:20230214 vitest:0.28.3] coverageにて[JavaScript heap out of memory]
-
問題
- c8を使ったcoverageオプションを付与したテストコマンド実行時にmemory heap errorが発生
-
エラー
<--- Last few GCs --->
[2405:0x577a230] 1009664 ms: Scavenge (reduce) 4047.4 (4118.5) -> 4047.5 (4120.5) MB, 7.0 / 0.0 ms (average mu = 0.245, current mu = 0.192) allocation failure
[2405:0x577a230] 1009686 ms: Scavenge (reduce) 4052.4 (4124.0) -> 4052.4 (4124.5) MB, 7.7 / 0.0 ms (average mu = 0.245, current mu = 0.192) allocation failure
[2405:0x577a230] 1009728 ms: Scavenge (reduce) 4053.2 (4124.8) -> 4053.2 (4124.8) MB, 9.8 / 0.0 ms (average mu = 0.245, current mu = 0.192) allocation failure
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
1: 0xb08e80 node::Abort() [node]
2: 0xa1b70e [node]
3: 0xce1890 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
4: 0xce1c37 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
5: 0xe992a5 [node]
6: 0xe99d86 [node]
7: 0xea82ae [node]
8: 0xea8cf0 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
9: 0xeabc6e v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
10: 0xe6d01f v8::internal::Factory::AllocateRawWithAllocationSite(v8::internal::Handle<v8::internal::Map>, v8::internal::AllocationType, v8::internal::Handle<v8::internal::AllocationSite>) [node]
11: 0xe7494c v8::internal::Factory::NewJSObjectFromMap(v8::internal::Handle<v8::internal::Map>, v8::internal::AllocationType, v8::internal::Handle<v8::internal::AllocationSite>) [node]
12: 0xe74f75 v8::internal::Factory::NewJSArrayWithUnverifiedElements(v8::internal::Handle<v8::internal::FixedArrayBase>, v8::internal::ElementsKind, int, v8::internal::AllocationType) [node]
13: 0xe751e2 v8::internal::Factory::NewJSArray(v8::internal::ElementsKind, int, int, v8::internal::ArrayStorageAllocationMode, v8::internal::AllocationType) [node]
14: 0xf851b1 v8::internal::JsonParser<unsigned short>::BuildJsonArray(v8::internal::JsonParser<unsigned short>::JsonContinuation const&, v8::base::SmallVector<v8::internal::Handle<v8::internal::Object>, 16ul> const&) [node]
15: 0xf8cdf4 v8::internal::JsonParser<unsigned short>::ParseJsonValue() [node]
16: 0xf8dbef v8::internal::JsonParser<unsigned short>::ParseJson() [node]
17: 0xd637bb v8::internal::Builtin_JsonParse(int, unsigned long*, v8::internal::Isolate*) [node]
18: 0x15d9cf9 [node]
Aborted (core dumped)
Error: Process completed with exit code 134.
- 解決
- coverage providerをc8からistanbulに変更する
- 公式:coverage-providers
まとめ
今回の検証で Jest から Vitest への色々なテストケースやライブラリ利用のケースでも移行ができることがわかりました。
主に移行時の苦しんで調べて回避してきた内容を書いてきましたが、Vitestへ 移行中のエンジニアの方々に役立てば幸いです。