2024/02/22(木)JavaScriptでtry-catchとif文でエラー処理にかかる時間を調べた
投稿日:
Node.jsとGoogle Chrome, Microsoft Edgeを用いて、try-catchとif文でエラー処理にかかる時間がどのくらい違うのかを調べた。
計測手法
次の4パターンを100万回実行した結果を記載している。
- Result型としてエラーオブジェクトをreturnし、if文で判定する方式
- Result型としてエラーインスタンスをreturnし、if文で判定する方式
- エラーオブジェクトをthrowし、try-catchで判定する方式
- エラーインスタンスをthrowし、try-catchで判定する方式
確認に使用したコード:https://gist.github.com/Lycolia/304bc9e825e821c2d582f3ef9f700817
計測結果
CPUによって処理速度がかなり変動するが、いずれの環境でも処理速度の速さは以下の通り。
- Result型としてエラーオブジェクトをreturnし、if文で判定する方式
- エラーオブジェクトをthrowし、try-catchで判定する方式
- Result型としてエラーインスタンスをreturnし、if文で判定する方式
- エラーインスタンスをthrowし、try-catchで判定する方式
計測に使用したCPU
| CPU | コア | スレッド | クロック | L2キャッシュ | L3キャッシュ |
|---|---|---|---|---|---|
| Intel Core i7 13700 | 16 | 24 | 5.20GHz | 24.00MB | 30.00MB |
| Intel Core i7 1265U | 10 | 12 | 4.80GHz | 6.50MB | 12.00MB |
| AMD Ryzen 5 5600G | 6 | 12 | 3.90GHz | 3.00MB | 16.00MB |
Intel Core i7 13700端末
16C24T, 5.20GHz, L2 24.00MB, L3 30.00MB。ミドルタワーPC。
Node.js v16.20.2
処理環境
| Env | Ver |
|---|---|
| CPU | Intel Core i7 13700 |
| OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 802 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 5,942 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 1,847 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 6,527 |
Node.js v18.19.1
処理環境
| Env | Ver |
|---|---|
| CPU | Intel Core i7 13700 |
| OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 887 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 5,826 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 2,006 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 6,464 |
Node.js v20.11.1
処理環境
| Env | Ver |
|---|---|
| CPU | Intel Core i7 13700 |
| OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 720 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 5,375 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 1,690 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 5,978 |
Microsoft Edge 121.0.2277.128
処理環境
| Env | Ver |
|---|---|
| CPU | Intel Core i7 13700 |
| OS | Windows 11 Pro 22H2(22621.3155) |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 4,246 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 12,329 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 11,985 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 14,105 |
Google Chrome 122.0.6261.58
処理環境
| Env | Ver |
|---|---|
| CPU | Intel Core i7 13700 |
| OS | Windows 11 Pro 22H2(22621.3155) |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 3,507 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 10,793 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 10,482 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 12,452 |
Intel Core i7 1265U端末
10C12T, 4.80GHz, L2 6.50MB, L3 12.00MB。ノートPC。
Node.js v16.20.2
処理環境
| Env | Ver |
|---|---|
| CPU | Intel Core i7 1265U |
| OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 972 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 11,416 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 2,288 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 13,993 |
Node.js v18.19.1
処理環境
| Env | Ver |
|---|---|
| CPU | Intel Core i7 1265U |
| OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 1,102 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 11,333 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 2,441 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 12,827 |
Node.js v20.11.1
処理環境
| Env | Ver |
|---|---|
| CPU | Intel Core i7 1265U |
| OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 888 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 10,881 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 2,269 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 12,254 |
Microsoft Edge 121.0.2277.128
処理環境
| Env | Ver |
|---|---|
| CPU | Intel Core i7 1265U |
| OS | Windows 11 Pro 22H2(22621.3155) |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 7,526 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 34,761 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 39,005 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 65,752 |
Google Chrome 122.0.6261.58
処理環境
| Env | Ver |
|---|---|
| CPU | Intel Core i7 1265U |
| OS | Windows 11 Pro 22H2(22621.3155) |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 6,303 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 16,460 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 17,979 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 23,378 |
AMD Ryzen 5 5600G端末
6C12T, 3.90GHz, L2 3.00MB, L3 16.00MB。ミニタワーPC。
この端末はOSがUbuntuであるため、これまでのWindows環境との単純比較はできない。
Node.js v16.20.2
処理環境
| Env | Ver |
|---|---|
| CPU | AMD Ryzen 5 5600G |
| OS | Ubuntu 22.04.3 LTS |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 1,786 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 11,242 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 3,880 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 12,716 |
Node.js v18.19.1
処理環境
| Env | Ver |
|---|---|
| CPU | AMD Ryzen 5 5600G |
| OS | Ubuntu 22.04.3 LTS |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 1,933 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 11,094 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 3,839 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 12,767 |
Node.js v20.11.1
処理環境
| Env | Ver |
|---|---|
| CPU | AMD Ryzen 5 5600G |
| OS | Ubuntu 22.04.3 LTS |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 1,714 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 10,741 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 3,444 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 11,978 |
Microsoft Edge 121.0.2277.128
処理環境
| Env | Ver |
|---|---|
| CPU | AMD Ryzen 5 5600G |
| OS | Ubuntu 22.04.3 LTS |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 4,348 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 21,584 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 15,543 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 23,119 |
Google Chrome 122.0.6261.57
これだけバージョンが合わなかった。
処理環境
| Env | Ver |
|---|---|
| CPU | AMD Ryzen 5 5600G |
| OS | Ubuntu 22.04.3 LTS |
処理結果
| 処理方式 | 処理時間(ms) |
|---|---|
| Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 4,842 |
| Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 16,607 |
| エラーオブジェクトをthrowし、try-catchで判定する方式 | 15,051 |
| エラーインスタンスをthrowし、try-catchで判定する方式 | 18,095 |
全体サマリ表
凡例
- Result型としてエラーオブジェクトをreturnし、if文で判定する方式
- Result型としてエラーインスタンスをreturnし、if文で判定する方式
- エラーオブジェクトをthrowし、try-catchで判定する方式
- エラーインスタンスをthrowし、try-catchで判定する方式
CPUによってかなり差が出ているが、Intel CPUの13700と1265Uを比較した場合、例外を作らない1, 3のケースでは処理に大きな差がないが、例外を作る2, 4のケースでは有意な差が認められるので、モバイル向けとデスクトップ向けでCPUの処理効率に差があることがわかる。
AMD CPUと比較した場合、Node.js環境で例外を作らないケースではIntel CPUに大きく引けを取るが、ブラウザ上でのそれではパフォーマンスは高い。また例外を作るケースでもNode.js・ブラウザ共にIntel CPUと比較した場合のパフォーマンスが比較的よいという、面白い結果になっている。但しOSが違うことの影響もあるため、単純比較はできないが…。
あとがき
ひとまずtry-catchを使うと遅くなるということが確かめられたので良かったし、環境によって劇的遅くなるケースがあるというのは思わない収穫だった。
これを試そうと思った切欠はtry-catchが乱用されているコードを見てパフォーマンス劣化に繋がっているのではないかという疑問を抱いたからだ。try-catchでパフォーマンスの劣化が起きることは知識としても経験としても知っていたのだが、ちゃんとレポートしたことがなかったので今回まとめてみた。
try-catchの利用によりパフォーマンスの劣化が起きるというのは、古い時代にあった開発のお作法を知っている人であれば、それなりに常識だとは思うが、最近は知られていないか、知られていても意識していないことが少なくないと思われるので、今回記事にしてみた感じだ。
100万回の実行なので細かい話になるとは思うがパフォーマンスチューニングは細かいことの積み重ねでもあるので、むやみやたらに例外を使わないことは重要だと思う。そもそも例外は制御しづらいので、可能な限りif文で処理を書くことが望ましいだろう。
処理が遅延する原因としては二つあると考えていて、一つは例外生成時に顕著であることからスタックトレースの生成を初めとしたエラーオブジェクトの生成に時間がかかっているのと、もう一つはcatchでも遅延が出ることから、コールスタックから最寄りのcatchを辿るのに時間を取られていると思われる。今回の検証では特に出してないが、以前確認した時の記憶が確かであれば、catchに入らない限り、tryだけで顕著に処理が遅延することはなかったと思う。
あと、どうでもいいことだがconsole.log()で結果を出したときにEdgeだけObjectのKeyの順序が他と違ったので転記しているときに微妙に不便だった。何回やっても同じだったので恐らくEdgeだけキーをソートするアルゴリズムが違うのだと思う。まぁObjectとかHashとかMapとかDictionaryみたいなやつは順序が保証されないので別にいいっちゃいいんだけど、まさか実装によってソート方式が違うというのは思いもしなかった。
EdgeとChromeで処理結果が優位に違うのも、恐らくこれが原因なのだろう。分からないが多分JSのエンジンの実装が違う気がする。
それとLinuxのEdgeにはマウスジェスチャー機能がないという悲しいことも分かった。
参考
2023/11/22(水)Node.js組み込みテストランナーとJest比較メモ
投稿日:
JestからNode.js組み込みのテストランナーに移行する時の方法や注意点をまとめたメモ
- 確認環境
- Node.jsの組み込みテストランナーとは
- Jest関数との対応リスト
describe(name, fn),it(name, fn, timeout?),test(name, fn, timeout?)beforeAll(fn, timeout?),afterAll(fn, timeout?),beforeEach(fn, timeout?),afterEach(fn, timeout?).toBe(value),.not.toBe(value).toStrictEqual(value),.not.toStrictEqual(value).toHaveLength(number),.toBeNull(),.toBeUndefined(),.toBeNaN().toBeTruthy().toBeInstanceOf(Class).toThrow,not.toThrowexpect.arrayContaining(array),expect.objectContaining(object),expect.stringContaining(string)jest.spyOn(object, methodName, accessType?).toHaveBeenCalled(),.toHaveBeenCalledTimes(number).toHaveBeenCalledWith(arg1, arg2, ...)jest.fn(implementation?)jest.useFakeTimers(fakeTimersConfig?),jest.runAllTimers()jest.useRealTimers()mockFn.mockClear()mockFn.mockReset()- モック機能についての備考
確認環境
| Env | ver |
|---|---|
| Node.js | 20.8.0 |
| Jest | 29.7 |
Node.jsの組み込みテストランナーとは
英語圏ではNode.js built-in test runnerとか呼ばれている存在で、Node.jsの組み込みテストランナーのこと。恐らくDenoのテストランナーに対抗して生まれた気配がする(Node.jsはDenoに機能追加があると真似する傾向があるため)
Node.jsの組み込みテストランナーは機能が二分されており、テストランナーとアサートに分かれている。Jestみたいにエコシステムが発達しておらず、標準ではdescribeなどはimportする必要がある。TypeScriptをテストする場合は間にレジスター[1]を嚙ます必要がある。噛まし方はswc/jestでESM, CJS混在のコードをJestを使ってテストする有力な方法は今のところ多分ないに書いた。
Jest関数との対応リスト
describe, it, beforeAll, afterAll, beforeEach, afterEach辺りは違和感がなかったが、それ以外は軒並み互換性がないので大きく書き換えが必要だと感じた。便利なMatcherは完膚なきまでに全滅している。お陰で覚えることが減ったのは逆に良くなったと感じる
なお、not始まりの機能は実際の動作を確認しておらず、Jestと同じ機能かどうかは確認していないことに留意すること
describe(name, fn), it(name, fn, timeout?), test(name, fn, timeout?)
Node.jsのTest機能では、以下となる
describe([name][, options][, fn])it([name][, options][, fn])test([name][, options][, fn])
基本的な差はないがJestにあったtimeout引数はNode.jsではoptionsパラメーターで設定するようになっている模様。今回調べるまで存在自体を知らなかったのもあり、このtimeoutがJestと同じ機能かどうかは確認していない
import { describe, it } from 'node:test';
describe('hoge', () => {
it('hoge', () => {
// ここにテストコード
});
it.todo('piyo');
});
beforeAll(fn, timeout?), afterAll(fn, timeout?), beforeEach(fn, timeout?), afterEach(fn, timeout?)
Node.jsのTest機能では、以下となる
before([fn][, options])after([fn][, options])beforeEach([fn][, options])afterEach([fn][, options])
基本的な差はないがJestにあったtimeout引数はNode.jsではoptionsパラメーターで設定するようになっている模様。今回調べるまで存在自体を知らなかったのもあり、このtimeoutがJestと同じ機能かどうかは確認していない
+import { after, before, beforeEach, afterEach, describe } from 'node:test';
+
describe('test', () => {
- beforeAll(() => {
+ before(() => {
console.log('before');
});
beforeEach(() => {
console.log('beforeEach');
});
afterEach(() => {
console.log('afterEach');
});
- afterAll(() => {
+ after(() => {
console.log('after');
});
});
.toBe(value), .not.toBe(value)
Node.jsのTest機能では、以下となる
assert.strictEqual(actual, expected[, message])assert.notStrictEqual(actual, expected[, message])
+import { describe, it } from 'node:test';
+import assert from 'node:assert';
+
describe('test', () => {
it('test', () => {
const actual = 123;
- expect(actual).toBe(123);
+ assert.deepStrictEqual(actual, 123);
});
});
.toStrictEqual(value), .not.toStrictEqual(value)
Node.jsのTest機能では、以下となる
assert.deepStrictEqual(actual, expected[, message])assert.notDeepStrictEqual(actual, expected[, message])
+import { describe, it } from 'node:test';
+import assert from 'node:assert';
+
describe('test', () => {
it('test', () => {
const actual = {
hoge: {
piyo: {
id: 1,
value: 'one'
}
},
fuga: [[123, 456], 'ABC', 'EFG']
};
- expect(actual).toStrictEqual({
+ assert.deepStrictEqual(actual, {
hoge: {
piyo: {
id: 1,
value: 'one'
}
},
fuga: [[123, 456], 'ABC', 'EFG']
});
});
});
.toHaveLength(number), .toBeNull(), .toBeUndefined(), .toBeNaN()
Node.jsのTest機能では、assert.strictEqual()となる
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert';
+
describe('test', () => {
it('test', () => {
const arr = [1, 2, 3];
const undef = undefined;
const nil = null;
const nan = NaN;
- expect(arr).toHaveLength(3);
- expect(undef).toBeUndefined();
- expect(nil).toBeNull();
- expect(nan).toBeNaN();
+ assert.strictEqual(arr.length, 3);
+ assert.strictEqual(undef, undefined);
+ assert.strictEqual(nil, null);
+ assert.strictEqual(nan, NaN);
});
});
.toBeTruthy()
Node.jsのTest機能では、以下となる
assert.ok(value[, message])
否定版は不明
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert';
+
describe('test', () => {
it('test', () => {
- expect(true).toBeTruthy();
- expect({}).toBeTruthy();
- expect([]).toBeTruthy();
- expect(42).toBeTruthy();
- expect('0').toBeTruthy();
- expect('false').toBeTruthy();
- expect(new Date()).toBeTruthy();
- expect(-42).toBeTruthy();
- expect(12n).toBeTruthy();
- expect(3.14).toBeTruthy();
- expect(Infinity).toBeTruthy();
- expect(-Infinity).toBeTruthy();
+ assert.ok(true);
+ assert.ok({});
+ assert.ok([]);
+ assert.ok(42);
+ assert.ok('0');
+ assert.ok('false');
+ assert.ok(new Date());
+ assert.ok(-42);
+ assert.ok(12n);
+ assert.ok(3.14);
+ assert.ok(Infinity);
+ assert.ok(-Infinity);
});
});
.toBeInstanceOf(Class)
Node.jsのTest機能では、assert.ok()となる
結果がTruthyであればなんでもpassするので注意
+import { describe, it } from 'node:test';
+import assert from 'node:assert';
+
describe('test', () => {
it('test', () => {
const actual = new Error('hoge');
- expect(actual).toBeInstanceOf(Error);
+ assert.ok(actual instanceof Error);
});
});
.toThrow, not.toThrow
Node.jsのTest機能では、以下となる
assert.throws(fn[, error][, message])assert.doesNotThrow(fn[, error][, message])
Jestより便利になっており、Error型以外も扱えるので後述する
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert';
+
describe('test', () => {
it('test', () => {
const err = new Error('test');
- expect(() => {
+ assert.throws(() => {
throw err;
- }).toThrow(err);
+ }, err);
});
});
Node.jsではJestと異なりError型以外も扱える
Jestの`.toThrow()はError型以外を扱うことができず、オブジェクトをThrowするようなコードではエラーになる
describe('test', () => {
it('test', () => {
const obj = { id: 1, value: 'hoge' };
// このテストは失敗する。またTypeScriptの型エラーにもなる
expect(() => {
throw obj;
}).toThrow(obj);
// このテストは失敗する。またTypeScriptの型エラーにもなる
expect(() => {
throw obj;
}).toThrow({ id: 1, value: 'hoge' });
});
});
しかしNode.jsであればこれはエラーにならない。例えば次のテストコードは成功する
import { describe, it, mock } from 'node:test';
import assert from 'node:assert';
describe('test', () => {
it('test', () => {
const obj = { id: 1, value: 'hoge' };
assert.throws(() => {
throw obj;
}, obj);
assert.throws(
() => {
throw obj;
},
{ id: 1, value: 'hoge' }
);
});
});
expect.arrayContaining(array), expect.objectContaining(object), expect.stringContaining(string)
恐らく非対称マッチャーはないので自分でロジックを書いてassert.strictEqual()で判定するしかないと思われる。元々微妙な機能だったのでやむなし
jest.spyOn(object, methodName, accessType?)
Node.jsのTest機能では、node:testからmockをimportして以下を使う
mock.method(object, methodName[, implementation][, options])
+import { describe, it, mock } from 'node:test';
+
describe('test', () => {
it('test', () => {
- jest.spyOn(console, 'log');
+ mock.method(console, 'log');
});
});
本来の挙動を塞ぎたい場合
例えば以下のようにテストコードを書いた場合、execSync()が実際の挙動で動作してしまい、テストとして機能しない。
import { describe, it, mock } from 'node:test';
import assert from 'node:assert';
import child_process, { execSync } from 'node:child_process';
const mockedExecSync = mock.method(child_process, 'execSync');
describe('execSync', () => {
it('execSyncが正しい引数で呼ばれること', () => {
execSync('false');
assert.strictEqual(mockedExecSync.mock.calls[0].arguments[0], 'false');
});
});
このような場合、以下のようにmock.method()の第三引数を指定してやると封じることができる。単体テストの観点では基本的に第三引数には空関数を入れておくのが望ましいだろう。
import { describe, it, mock } from 'node:test';
import assert from 'node:assert';
import child_process, { execSync } from 'node:child_process';
- const mockedExecSync = mock.method(child_process, 'execSync');
+ const mockedExecSync = mock.method(child_process, 'execSync', () => {});
describe('execSync', () => {
it('execSyncが正しい引数で呼ばれること', () => {
execSync('false');
assert.strictEqual(mockedExecSync.mock.calls[0].arguments[0], 'false');
});
});
モックパターン
module.exportsされている関数のモック
import foo from 'foo';形式でmock.method()の第一引数を埋める
import { describe, it, mock } from 'node:test';
import assert from 'node:assert';
import child_process, { execSync } from 'node:child_process';
const mockedExecSync = mock.method(child_process, 'execSync', () => {});
describe('execSync', () => {
it('execSyncが正しい引数で呼ばれること', () => {
execSync('false');
assert.strictEqual(mockedExecSync.mock.calls[0].arguments[0], 'false');
});
});
上記が正常に動作することはNode.js v20.0.0時点のコードで確認している
Global objectから生えている関数のモック
以下のようにmock.method()の第一引数にGlobal objectを設定すればよい
import { describe, it, mock } from 'node:test';
import assert from 'node:assert';
describe('exit', () => {
// exitが実際に走って落ちるのでmock.methodの第三引数を指定している
const mockedExit = mock.method(process, 'exit', () => {});
it('call exit', () => {
process.exit(1);
assert.strictEqual(mockedExit.mock.calls.length, 1);
});
});
Named exportされている関数のモックは今のところ無理そう
ファイルモックをする手段がないので正攻法では無理そう
https://github.com/nodejs/help/issues/4298
2024-10-15追記
Node.js v22.3.0でテスト走行時に--experimental-test-module-mocksを渡すことで近いことができるようになった模様だが、試したところ上手く動かないし、spy的な使い方はできなさそうだ。mock.method()との組み合わせも試してみたが、上手くいかなかった。
ObjectやNamespaceでラップされている関数のモック
実装例(Object)
export const Hoge = {
validateFoo() {
// 例外を飛ばす可能性のある何かの処理
},
hoge() {
Hoge.validateFoo();
return 1;
},
};
実装例(Namespace)
export namespace Hoge {
export const validateFoo = () => {
// 例外を飛ばす可能性のある何かの処理
};
export const hoge = () => {
validateFoo();
return 1;
};
}
実装例に対するテストコード
ObjectもNamespaceも同じ書き方でテスト可能
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { Hoge } from './hoge';
describe('hoge', () => {
it('validateFooが例外をスローした場合、例外がスローされること', (t) => {
t.mock.method(Hoge, 'validateFoo', () => {
throw new Error('foo');
});
assert.throws(() => {
Hoge.hoge();
}, Error('foo'));
});
it('全ての関数が正常終了した場合、戻り値を返すこと', () => {
const actual = Hoge.hoge();
assert.strictEqual(actual, 1);
});
});
.toHaveBeenCalled(), .toHaveBeenCalledTimes(number)
Node.jsのTest機能では、assert.strictEqual()でモックから生えてるやつを調べる。returnも同様の手法で実現できる
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert';
+
describe('test', () => {
it('test', () => {
- const spiedConsoleLog = jest.spyOn(console, 'log');
+ const mockedConsoleLog = mock.method(console, 'log');
console.log();
- expect(spiedConsoleLog).toHaveBeenCalled();
- expect(spiedConsoleLog).toHaveBeenCalledTimes(1);
+ assert.deepStrictEqual(mockedConsoleLog.mock.calls.length, 1);
});
});
.toHaveBeenCalledWith(arg1, arg2, ...)
Node.jsのTest機能では、assert.strictEqual()でモックから生えてるやつを調べる。returnも同様の手法で実現できる。
Jestでは.toEqual()処理されるがNode.jsの組み込みテストランナーの場合、厳密比較ができるので便利
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert';
+
describe('test', () => {
it('test', () => {
- const spiedConsoleLog = jest.spyOn(console, 'log');
+ const mockedInfo = mock.method(console, 'log');
console.log('test');
- expect(spiedConsoleLog).toHaveBeenCalledWith('test');
+ assert.deepStrictEqual(mockedInfo.mock.calls[0].arguments[0], 'test');
});
});
jest.fn(implementation?)
Node.jsのTest機能では、以下となる
mock.fn([original[, implementation]][, options])
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert';
+
+// 動作確認用の関数
const testTarget = (cbParam: string, callbackFn: (param: string) => number) => {
return callbackFn(cbParam);
};
describe('test', () => {
it('test', () => {
- const mockFn = jest.fn((_: string) => {
+ const mockFn = mock.fn((_: string) => {
return 5;
});
const actual = testTarget('hoge', mockFn);
- expect(mockFn).toBeCalledWith('hoge');
- expect(mockFn).toReturnWith(5);
- expect(actual).toBe(5);
+ assert.deepStrictEqual(mockFn.mock.calls[0].arguments[0], 'hoge');
+ assert.deepStrictEqual(mockFn.mock.calls[0].result, 5);
+ assert.deepStrictEqual(actual, 5);
});
});
jest.useFakeTimers(fakeTimersConfig?), jest.runAllTimers()
Node.jsのTest機能では、以下となる
mock.timers.enable([timers])mock.timers.runAll()
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert';
+
+describe('test', () => {
- jest.useFakeTimers();
+ mock.timers.enable();
+ it('test', () => {
- const mockFn = jest.fn();
+ const mockFn = mock.fn();
setTimeout(() => {
mockFn();
}, 9999);
- expect(mockFn).not.toHaveBeenCalled();
- jest.runAllTimers();
- expect(mockFn).toHaveBeenCalledTimes(1);
+ assert.deepStrictEqual(mockFn.mock.calls.length, 0);
+ mock.timers.runAll();
+ assert.deepStrictEqual(mockFn.mock.calls.length, 1);
});
});
jest.useRealTimers()
Node.jsのTest機能では、以下となる
mock.timers.reset()
+import { mock } from 'node:test';
-jest.useRealTimers();
+mock.timers.reset();
mockFn.mockClear()
Node.jsのTest機能では、以下となる
ctx.resetCalls()
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert';
+
describe('test', () => {
it('test', () => {
- const mockFn = jest.fn((param: string) => `<${param}>`);
+ const mockFn = mock.fn((param: string) => `<${param}>`);
mockFn('hoge');
- expect(mockFn).toHaveBeenCalledTimes(1);
- expect(mockFn).toReturnWith('<hoge>');
+ assert.deepStrictEqual(mockFn.mock.calls.length, 1);
+ assert.deepStrictEqual(mockFn.mock.calls[0].result, '<hoge>');
- mockFn.mockClear();
+ mockFn.mock.resetCalls();
- expect(mockFn).toHaveBeenCalledTimes(0);
+ assert.deepStrictEqual(mockFn.mock.calls.length, 0);
mockFn('piyo');
- expect(mockFn).toHaveBeenCalledTimes(1);
- expect(mockFn).toReturnWith('<piyo>');
+ assert.deepStrictEqual(mockFn.mock.calls.length, 1);
+ assert.deepStrictEqual(mockFn.mock.calls[0].result, '<piyo>');
});
});
mockFn.mockReset()
Node.jsのTest機能では、以下となる
mockFn.mock.restore()
振る舞いが微妙に違うため後述する
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert';
+
describe('test', () => {
it('test', () => {
- const spiedConsoleLog = jest
- .spyOn(console, 'log')
- .mockImplementation((param: any) => {
- return `<${param}>`;
- });
+ const mockedConsoleLog = mock.method(console, 'log', (param: any) => {
+ return `<${param}>`;
+ });
console.log('hoge');
- expect(spiedConsoleLog).toHaveBeenCalledTimes(1);
- expect(spiedConsoleLog).toReturnWith('<hoge>');
+ assert.deepStrictEqual(mockedConsoleLog.mock.calls.length, 1);
+ assert.deepStrictEqual(mockedConsoleLog.mock.calls[0].result, '<hoge>');
- spiedConsoleLog.mockReset();
+ mockedConsoleLog.mock.restore();
});
});
JestとNode.jsでの振る舞いの差異
但しJestとNode.jsのTest機能では微妙に差異がある
例えばJestでは以下の実装が正しくPASSするが
describe('test', () => {
it('test', () => {
const mockFn = jest
.spyOn(console, 'log')
.mockImplementation((param: any) => {
return `<${param}>`;
});
console.log('hoge');
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toReturnWith('<hoge>');
mockFn.mockReset();
expect(mockFn).toHaveBeenCalledTimes(0);
console.log('piyo');
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).not.toHaveBeenCalledWith();
expect(mockFn).toReturnWith(undefined);
});
});
Node.jsで以下の実装を書いても同じように機能しない
import { describe, it, mock } from 'node:test';
import assert from 'node:assert';
describe('test', () => {
it('test', () => {
const mockFn = mock.method(console, 'log', (param: any) => {
return `<${param}>`;
});
console.log('hoge');
assert.deepStrictEqual(mockFn.mock.calls.length, 1);
assert.deepStrictEqual(mockFn.mock.calls[0].result, '<hoge>');
mockFn.mock.resetCalls();
mockFn.mock.restore();
assert.deepStrictEqual(mockFn.mock.calls.length, 0);
console.log('piyo');
// 以降のテストはいずれも落ちる
assert.deepStrictEqual(mockFn.mock.calls.length, 1);
assert.deepStrictEqual(mockFn.mock.calls[0].result, '<piyo>');
});
});
但しモック実装を削除したうえで再度呼び出すという行為には意味がないので、特に問題にはならないと思われる
モック機能についての備考
it([name][, options][, fn])の第三引数のコールバックの第一引数にはTestContextが入っており、これを使ってモックすることもできる
これを使う場合、スコープアウトでモックが復元されるため、例えば以下のような関数をテストするときに便利である。
実装
export namespace Hoge {
export const validateFoo = () => {
// 例外を飛ばす可能性のある何かの処理
};
export const validateBar = () => {
// 例外を飛ばす可能性のある何かの処理
};
export const validateBaz = () => {
// 例外を飛ばす可能性のある何かの処理
};
export const hoge = () => {
validateFoo();
validateBar();
validateBaz();
return 1;
};
}
テストコード
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { Hoge } from './hoge';
describe('hoge', () => {
it('validateFooが例外をスローした場合、例外がスローされること', (t) => {
t.mock.method(Hoge, 'validateFoo', () => {
throw new Error('foo');
});
assert.throws(() => {
Hoge.hoge();
}, Error('foo'));
});
it('validateBarが例外をスローした場合、例外がスローされること', (t) => {
t.mock.method(Hoge, 'validateBar', () => {
throw new Error('bar');
});
assert.throws(() => {
Hoge.hoge();
}, Error('bar'));
});
it('validateBazが例外をスローした場合、例外がスローされること', (t) => {
t.mock.method(Hoge, 'validateBaz', () => {
throw new Error('baz');
});
assert.throws(() => {
Hoge.hoge();
}, Error('baz'));
});
it('全ての関数が正常終了した場合、戻り値を返すこと', () => {
const actual = Hoge.hoge();
assert.strictEqual(actual, 1);
});
});
- 標準出力にトランスパイルした結果を出力してくれるもの ↩
2021/07/04(日)Babelとtscの比較
投稿日:
Babelとtscどっちがいいのか気になったので調べて見たメモ
結論から言うと基本的にはtscで良い
確認環境
| Env | Ver |
|---|---|
| @babel/cli | 7.14.5 |
| @babel/core | 7.14.6 |
| @babel/preset-env | 7.14.7 |
| @babel/preset-typescript | 7.14.5 |
| typescript | 4.3.2 |
Babelとは何か?
What is Babel? よりBabelとはJavaScriptのコンパイラと説明されている
Babelがしてくれること
- 構文の変換
- corejsを使ったPolyfill
- ソースコードの変換(codemods
- その他色々
基本的にはES6+をES6にしてくれると考えれば良さそうです
でもそれって別にtscでもいいいよねって思う
Babelの導入
基本はこれ
npm i -D @babel/core @babel/cli @babel/preset-env
Babelの設定
.babelrcを作ってその中にJSONを書いていく
こんな感じ
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"ie": "11",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
Babelの機能について
コンポーネント
@babel/cli
BabelのCLI、これがないと始まらない
@babel/core
Babel本体、CLIがよしなにしてくれる
@babel/preset-env
構文の変換やPolyfillを設定できる
@babel/preset-typescript
TSをトランスパイルしてくれる
BabelとBrowserlist
BabelはBrowserlistの設定を認識して自動でPolyfillを挿入してくれます
なお、Babel 7.4.0以前では設定方法が異なる可能性があります
サンプルコード
IE11をターゲットにした設定のサンプルです
以下のコマンドを流せばPolyfillされたJSが出ることを確認できます
npx babel src -d dest --extensions ".ts"
.babelrc
{
"presets": [
["@babel/preset-env", { "corejs": 3, "useBuiltIns": "usage" }],
["@babel/preset-typescript"]
]
}
.browserlist
ie 11
Babelとtscでトランスパイルしてみる
割と違うコードが出てきます
元のソース
const sp = new URLSearchParams('?aaa=bbb&ccc');
console.log(sp);
const prm = new Promise((res) => res(true));
console.log(prm);
[...Array(10)].forEach((_, i) => console.log(i));
console.log(globalThis.Date());
export {};
Babel
.babelrc
{
"presets": [["@babel/preset-env"], ["@babel/preset-typescript"]]
}
ビルド結果
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
function _toConsumableArray(arr) {
return (
_arrayWithoutHoles(arr) ||
_iterableToArray(arr) ||
_unsupportedIterableToArray(arr) ||
_nonIterableSpread()
);
}
function _nonIterableSpread() {
throw new TypeError(
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))
return _arrayLikeToArray(o, minLen);
}
function _iterableToArray(iter) {
if (
(typeof Symbol !== "undefined" && iter[Symbol.iterator] != null) ||
iter["@@iterator"] != null
)
return Array.from(iter);
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) {
arr2[i] = arr[i];
}
return arr2;
}
var sp = new URLSearchParams("?aaa=bbb&ccc");
console.log(sp);
var prm = new Promise(function (res) {
return res(true);
});
console.log(prm);
_toConsumableArray(Array(10)).forEach(function (_, i) {
return console.log(i);
});
console.log(globalThis.Date());
tsc
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"allowJs": true,
"checkJs": true,
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"noImplicitAny": true,
"moduleResolution": "node",
"isolatedModules": true,
"baseUrl": ".",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"]
}
ビルド結果
"use strict";
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
};
Object.defineProperty(exports, "__esModule", { value: true });
var sp = new URLSearchParams('?aaa=bbb&ccc');
console.log(sp);
var prm = new Promise(function (res) { return res(true); });
console.log(prm);
__spreadArray([], Array(10)).forEach(function (_, i) { return console.log(i); });
console.log(globalThis.Date());
//# sourceMappingURL=index.js.map
結論
tscで問題ない
tscの方が綺麗なコードが出てきてるので、Polyfill要らなければtscで問題ないです
因みにES6+をES5にするのもできるのでJSのトランスパイルにも使えます
core-jsを使うならBabel
ちょいちょい触っててBabelの利点はcore-jsがあれば.browserlistを使えるので、そこでターゲットを指定してやればPolyfillを勝手に差し込んでくれるところですね
但しCRAではBabelに対する.browserlistはほぼ無価値
あとはCreate React AppはビルドにBabelを採用しているので、FW側でビルドパイプラインがあるときには採用したほうが楽です
(態々書き換える意味もないので)
ただreact-scripts 4.0.3にはcore-jsが入っていないので、基本的に.browserlistを書いたところでPolyfillは入らないため、あんまり存在感はないです
