業務用ツール

自動テストは基本的に、なんらかの問題があった場合にエラーをスローするか、またはエラーを引き起こすコードです。ほとんどのライブラリやテスト フレームワークには、テストの作成を容易にするさまざまなプリミティブが用意されています。

前のセクションで説明したように、これらのプリミティブにはほとんどの場合、独立したテストを定義する方法(テストケースと呼ばれます)とアサーションを提供する方法が含まれています。アサーションは、結果のチェックと、なんらかの問題があった場合にエラーをスローすることを組み合わせた方法で、すべてのテスト プリミティブの基本的なプリミティブと考えることができます。

このページでは、これらのプリミティブに対する一般的なアプローチについて説明します。選択したフレームワークは次のようになりますが、これは正確なリファレンスではありません。

次に例を示します。

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

この例では、「数学テスト」と呼ばれるテストのグループ(スイート)を作成し、それぞれがアサーションを実行する 3 つの独立したテストケースを定義します。通常、このようなテストケースは、テストランナーのフィルタフラグなどを使用して、個別に対処または実行できます。

プリミティブとしてのアサーション ヘルパー

Vitest を含むほとんどのテスト フレームワークには、assert オブジェクト上にアサーション ヘルパーのコレクションが含まれています。これにより、戻り値やその他の状態をexpectationと照らし合わせてすばやく確認できます。多くの場合、その期待値は「既知の正常な」値です。上記の例では、13 番目のフィボナッチ数が 233 であることがわかっているため、assert.equal を使用して直接確認できます。

また、値が特定の形式をとる、別の値より大きい、他のプロパティを持つといった想定も考えられます。このコースでは、考えられるすべてのアサーション ヘルパーを網羅することはできませんが、テスト フレームワークでは常に、少なくとも次の基本的なチェックが提供されます。

  • 「真正」チェックは「OK」チェックと表現されることが多く、条件が真であることをチェックします。これは、何かが成功したか正しいかを確認する if の記述方法と一致します。通常、assert(...) または assert.ok(...) として、1 つの値と任意のコメントを受け取ります。

  • 数学テストの例のような、オブジェクトの戻り値または状態が既知の正常な値と等しくなることを期待する等価チェック。これらはプリミティブ等式(数値や文字列など)または参照等式(これらは同じオブジェクト)に使用します。内部では、これらは == 比較または === 比較による単なる「真実」チェックです。

    • JavaScript では、大まかな等価性(==)と厳密な等価性(===)が区別されます。ほとんどのテスト ライブラリには、それぞれ assert.equal メソッドと assert.strictEqual メソッドが用意されています。
  • 高度な等価チェック。オブジェクト、配列、その他の複雑なデータ型の内容のチェックと、オブジェクトを走査して比較する内部ロジックのチェックに等価チェックを拡張します。JavaScript には 2 つのオブジェクトまたは配列の内容を比較する機能が組み込まれていないため、これらは重要です。たとえば、[1,2,3] == [1,2,3] は常に false です。多くの場合、テスト フレームワークには deepEqual ヘルパーまたは deepStrictEqual ヘルパーが含まれています。

通常、2 つの値を比較するアサーション ヘルパーは(「真実」チェックだけではありません)、通常は 2 つまたは 3 つの引数を取ります。

  • テスト対象のコードから生成された、または検証する状態を記述する実際の値。
  • 想定値。通常はハードコードされます(たとえば、リテラル数値や文字列など)。
  • 想定された内容や失敗した可能性のある内容を説明するコメント(省略可)。この行が失敗した場合に含まれます。

また、システムの状態を単独で正しく確認できることはまれであるため、アサーションを組み合わせてさまざまなチェックを作成することも非常に一般的です。次に例を示します。

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

Vitest は、内部的に Chai アサーション ライブラリを使用して、アサート ヘルパーを提供します。このリファレンスを調べて、どのアサーションとヘルパーがコードに適しているかを確認すると便利です。

流暢なアサーションと BDD のアサーション

一部のデベロッパーは、動作駆動型開発(BDD)または Fluent スタイルのアサーションと呼ばれるアサーション スタイルを好むことがあります。これらは「expect」ヘルパーとも呼ばれます。想定を確認するためのエントリ ポイントは expect() という名前のメソッドであるためです。

ヘルパーは、assert.okassert.strictDeepEquals などの単純なメソッド呼び出しとして記述されたアサーションと同じように動作することが想定されていますが、一部のデベロッパーでは、この方が読みやすいと感じています。BDD アサーションは次のようになります。

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

このスタイルのアサーションは、メソッド チェーンと呼ばれる手法により機能します。メソッド チェーンでは、expect によって返されるオブジェクトを、以降のメソッド呼び出しで継続的にチェーンできます。前の例の to.bethat.does など、呼び出しの一部は関数を持たず、呼び出しを読みやすくするためにのみ含まれています。また、テストが失敗した場合に自動コメントを生成する可能性もあります。(特に、チェーンで障害を明確に記述する必要があるため、expect では通常、省略可能なコメントはサポートされません)。

多くのテスト フレームワークは、Fluent/BDD と通常のアサーションの両方をサポートしています。Vitest は、Chai の両方のアプローチをエクスポートし、BDD に対する独自のやや簡潔なアプローチを備えています。一方、Jest には、デフォルトで expect メソッドのみが含まれています。

ファイル間でテストをグループ化する

テストを作成する際は、すべてのテストを 1 つのファイルに含めるのではなく、複数のファイルにまたがってテストを作成するのが一般的です。つまり、暗黙的にグループ化する傾向があります。実際、テストランナーは通常、事前定義されたフィルタまたは正規表現によってファイルがテスト用であることのみを認識します。たとえば、vitest には、拡張子が「.test.jsx」や「.spec.ts」(「.test」と「.spec」、およびいくつかの有効な拡張子)で終わるプロジェクト内のすべてのファイルが含まれます。

コンポーネント テストは、多くの場合、次のディレクトリ構造のように、テスト対象コンポーネントのピアファイルに配置されます。

ディレクトリ内のファイルのリスト(UserList.tsx、UserList.test.tsx など)。
コンポーネント ファイルと関連するテストファイル

同様に、単体テストはテスト対象コードの隣に配置する傾向があります。エンドツーエンド テストは、それぞれ独自のファイルに配置できます。統合テストは、独自の固有のフォルダに配置することもできます。これらの構造は、複雑なテストケースが拡大し、テスト専用のサポート ライブラリなど、テスト以外の独自のサポート ファイルを必要とする場合に役立ちます。

ファイル内でテストをグループ化する

これまでの例で説明したように、suite() の呼び出し内にテストを配置し、test() で設定したテストをグループ化するのが一般的です。スイートは通常、それ自体のテストではありませんが、渡されたメソッドを呼び出して、関連するテストまたは目標をグループ化することで、構造を提供できます。test() の場合、渡されたメソッドはテスト自体のアクションを記述します。

アサーションと同様に、Fluent/BDD にはグループ化テストとかなり標準的な同等性があります。一般的な例を次のコードで比較します。

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

ほとんどのフレームワークで、suitedescribetestit と同様に動作しますが、アサーションの作成に expectassert を使用する場合には大きな違いがあります。

ツールによっては、スイートやテストの配置方法が微妙に異なります。たとえば、Node.js の組み込みテストランナーでは、test() の呼び出しをネストして、暗黙的にテスト階層を作成できます。ただし、Vitest では suite() を使用したこの種のネストのみが可能であり、別の test() 内で定義された test() は実行しません。

アサーションの場合と同様に、技術スタックが提供するグループ化方法の正確な組み合わせはそれほど重要ではありません。このコースではこれらを抽象的に扱いますが、選択したツールにどのように適用できるかを理解する必要があります。

ライフサイクル メソッド

テストをグループ化する理由の 1 つは、たとえファイル内のトップレベルであっても、すべてのテストに対して、またはテストのグループに対して 1 回実行される setup メソッドと teardown メソッドを提供することです。ほとんどのフレームワークでは、次の 4 つの方法が用意されています。

すべての「test()」または「it()」 スイートに 1 回
テスト実行前 「beforeEach()」 beforeAll()
テスト実行後 「afterEach()」 「afterAll()」

たとえば、各テストの前に仮想ユーザー データベースを事前に入力し、テスト後にクリアできます。

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

これは、テストを簡素化するのに役立ちます。すべてのテストで複製するのではなく、共通のセットアップ コードとティアダウン コードを共有できます。また、セットアップ コードとティアダウン コード自体がエラーをスローする場合は、テスト自体の失敗とは関係ない構造上の問題を示している可能性があります。

一般的なアドバイス

ここでは、これらのプリミティブについて考える際に覚えておくべきヒントをいくつか紹介します。

プリミティブはガイド

ここで紹介するツールとプリミティブは、Vitest、Jest、Mocha、Web Test Runner、その他の特定のフレームワークと完全には一致しません。ここでは一般的なガイドとして Vitest を使用していますが、選択したフレームワークに対応付けるようにしてください。

必要に応じてアサーションを組み合わせる

テストは基本的に、エラーをスローする可能性のあるコードです。すべてのランナーは、個別のテストケースを記述するプリミティブ(多くの場合、test())を提供します。

ただし、そのランナーが assert()expect()、アサーション ヘルパーも提供している場合、この部分は利便性に関するものであり、必要に応じてスキップできます。他のアサーション ライブラリや従来の if ステートメントなど、エラーをスローする可能性のあるコードはすべて実行できます。

IDE の設定が大きな助けになる

IDE(VSCode など)がオートコンプリートと、選択したテストツールのドキュメントにアクセスできるようにすると、生産性が向上します。たとえば、Chai アサーション ライブラリassert には 100 を超えるメソッドがあり、適切なドキュメントのドキュメントをインラインで表示すると便利です。

これは、テストメソッドを使用してグローバル名前空間を設定する一部のテスト フレームワークでは特に重要です。これは微妙な違いですが、テスト ライブラリがグローバル名前空間に自動的に追加されている場合は、テスト ライブラリをインポートせずに使用できることがよくあります。

// some.test.js
test('using test as a global', () => { … });

ヘルパーが自動的にサポートされてもインポートすることをおすすめします。これにより、IDE でこれらのメソッドを簡単に検索できるようになります。(React のビルド時にこの問題が発生したことがあるかもしれません)。コードベースには魔法のような React グローバルがあるものの、そうではないコードベースがあり、React を使用してすべてのファイルにインポートする必要があります。

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });