バックドア操作によるテストデータの準備
バックドア操作とは
バックドア操作とは、非公開の API を呼び出してテストデータを準備することを指します。
例えば、Todo プロジェクトで removeTodo 機能(id で todo を削除)が正常に動作するかをテストしたいとします。
しかし、まだ addTodo 関数を実装していない場合があります。
この時、todos に直接 todo を push してテストを行うことができます。
const todo = {
id: 0,
title: "work",
};
store.todos.push(todo);
これがバックドア操作によるテストデータの準備です。
この操作方法はビジネスコードの実装と非常に密接に結びついています。
例えば、上記のコー ドは todo のデータ構造を露呈しています。
後に todo に属性が追加されると、このテストコードもエラーを引き起こす可能性があります。
脆弱なテスト
このようなテストは脆弱なテストです。
このようなテストが増えると、みんながビジネスコードを変更することを恐れるようになります。
しかし、ここではまだ addTodo が実装されていないので、まずは一時的にバックドア操作を使用して removeTodo をテストすることもできます。
その後、addTodo 機能が実装されたら、バックドア操作を置き換える必要があります。
可能な限りバックドア操作を使用せず、round-trip 方法を優先してください。
round-trip とは、ソフトウェアテストにおいて、特定の機能やプロセスが開始点から終了点まで、そして元の開始点に戻るまでの全プロセスを通してテストするアプローチを指します。
プログラムの間接入力
直接入力とは
これは、ビジネスコード内の関数が引数を通じて 直接データを受け取り、計算を行うことを指します。
関数を呼び出し、引数を渡すだけで、これが直接入力の方法です。
function add(a: number, b: number): number {
return a + b;
}
間接入力とは
以下のコードのように、他のモジュール/関数/グローバルオブジェクトなどを通じて、引数以外の方法でデータを入力することを間接入力と呼びます。
export function doubleUserAge(): number {
// userAge を通じてデータを取得
return userAge() * 2;
}
間接入力が特別な処理を必要とする理由:
function userAge() {
return 23;
}
userAge は API のリクエストや store のデータ読み取りを通じて取得される可能性があります。
つまり、age は変更される可能性が高い値です。
const doubleAge = doubleUserAge();
expect(doubleAge).toBe(48);
直接入力のように固定してしまうと、age が更新されるたびにテストをメンテナンスする必要があります。
このようなテストは脆弱なテストです。
よく考えてみると、実際にテストしたいのは * 2 というロジックです。
age の値がいくつであるかは、実は重要ではありません。
userAge の値をコントロールする必要があります。
mock と stub
stub とは何か
stub とは、実際のロジック実装を置き換えるテスト用語です。
stub を使用することで、テストは外部の実際のコード実装から分離され、テストロジックがよりシンプルで理解しやすくなります。
mock と stub の違い
単体テストにおいて:
-
stub は間接入力を制御する方法であり、間接入力の実際の実装を置き換えます。
stub は単に値を返すだけです。
// stub
vi.mock("packageName", () => {
return {
functionName: () => 2,
};
}); -
mock はテストの代用品を指します。
mock はテストの代用品として、行動検証に必要な相互作用情報を記録するだけでなく、検証も提供します。
mock は stub に比べて、相互作用情報の記録と検証機能が追加されています。
mock は stub の基礎に加えて、相互作用情報を記録します。
// mock
vi.mock("packageName", () => {
return {
functionName: vi.fn(() => 2),
};
});
最小準備データ原則
データを準備する際には、その単体テストケースで必要とされるデータのみを提供します。
データが少ないほど、テストケースは読みやすくなります。 この原則に違反すると、コードの保守性と可読性が低下し、心理的な負担が増えます。
単体テストはビジネスコードのユーザーの一つです。
したがって、単体テストはビジネスコードをより良く書くためのドライバーになり得ます。
単体テスト自体は、簡潔さと可読性を重視しています。
単体テストもコードの一部であり、保守が必要です。
もしテストコードの保守にビジネスコードの保守よりも多くの時間がかかるなら、あなたはまだテストを書きますか?
他のされモジュールからエクスポートた関数に依存する
vi.mocked().mockReturnValue()
vi.mock() は path のみを受け取り、その後に mock を行います。
import { userAge } from "./user";
vi.mock("./user");
describe("間接入力の値を制御する", () => {
it("* 2", () => {
vi.mocked(userAge).mockReturnValue(2);
const r = doubleUserAge();
expect(r).toBe(4);
});
});
この方法では、異なるテストケースで異なる値を mock することができます。
第三者ライブラリへの依存
例えば Axios のような第三者モジュールを呼び出す場合、どのようにテストすればいいのでしょうか?
第三者ライブラリ/モジュールの関数を mock することは、自分たちが書いた関数を mock するのと同じです。
唯一の違いは、パスをモジュール名に変更することです。
vi.mock("axios");
it("第三者ライブラリ/モジュール: Axios", async () => {
vi.mocked(axios).mockResolveValue({ name: "nansen", age: 2 });
const r = await doubleUserAge();
expect(r).toBe(4);
});