at backyard

Color my life with the chaos of trouble.

小さなテストライブラリをNode.jsで作ってみる(テストライブラリの自作)

以前から気になっていたことの一つに、mochaやjestなどのテストライブラリはどういう動きをしているのだろうか?というのがあった。

describeitexpect などなど、独自の関数をテストコード内に書いてテストを実行していくが、別にこれらのコードは直接importしているわけではなく、mochaやjestなどのテストランナー(?)部分で実行されていく。
(ということは、テストランナー側でimportされている関数を利用してテストを実行していく、という形だろうか?)

普段から使っているこれらのツールの内部の仕組みを考えるために、自身で小さなテスト用ライブラリを作成してみたので、そちらを元にテスト用ライブラリの実装について考えてみたいと思う。

目次

注意点

なお、ここに書いてあるのはあくまで自身が手探りでテスト用ライブラリの作成をおこなった備忘録であり、 mochajest がこのように実装しているということではないので、実装方法が気になる方は実際のコードを読んでみることをおすすめする。

今回、自身でも勉強するにあたり、mochaやjestのコードも読んでみたが、短時間でこれらのライブラリの処理を理解するのは難易度が高く感じたので(自身のスキル不足もあるだろうが)、いつか時間が取れれば内部実装をもっとしっかり読んでみたいところである。

実際のコード

実際のコードはGitHub上にある。

このポストを書いたときよりもバージョンアップしているかもしれないが、実装方針的には変わっていない。
(もし実装が大きく変わりそうなタイミングなどがあればタグを付けておくことにする)

github.com

describe, it, expect関数の実行について

テストコードを書く際に describe などの独自関数をテストコード内に実装するが、当然ながらこれらのコードはそのまま実行しようとしても ReferenceError: describe is not defined といった感じの未定義エラーが出る。

そのため、テスト実行時にはこれらの関数を読み込んでいる状態でテストコードを解釈してテストを走らせる必要があるわけだが、その話は一旦後回しにして、まずは describe などの関数自体から見ていく。

今回私が実装したコードは下記のようなコードとなる。
(ここに書くために簡略化している)

const expect = (actual) => {
  return {
    toBe(expected) {
      if (actual === expected) {
        console.log('Succeeded');
      } else {
        throw new Error(`Failed! Actual: ${actual}, Expected: ${expected}`);
      }
    },
    // toBe以外にも様々な関数をここに実装していく
  };
};

const it = (testName, fn) => {
  console.log(`Test name: ${testName}`);

  try {
    fn();
  } catch (err) {
    console.log(err);
    throw new Error('Test failed');
  }
};

const describe = (suiteName, fn) => {
  console.log(`Test suite name: ${suiteName}`);

  try {
    fn();
  } catch (err) {
    console.log(err.message);
  }
};

module.exports = {
  describe,
  it,
  expect,
};

このような感じでそれぞれテスト用の関数を定義することで、下記のようなテストコードが動くようになる

const { describe, it, expect } = require('./test-lib');

describe('Test sample', () => {
  it('Sum', () => {
    expect(1 + 2).toBe(3);
  });
  ・
  ・
});

ただ、実際のテストコードはテストライブラリの関数をファイルの先頭で読み込んでいたりはしない。

実際には jest などのコマンドを実行することで指定されたディレクトリ内にあるテストコードを読み出してテストを行ったりしている。

というわけで、次にそこの部分を担当してくれるテストランナーを作ることにする。

テスト実行時にeval関数を利用して、テストコードを解釈・実行する

ここでは eval関数を用いてテストコードを解釈・実行させるという手法を用いる。

下記のファイルはtestFilePath という変数にテストファイルのパスが格納されているという前提になっており、このファイルをNode.jsから実行することで、テストファイルをこのファイル上で実行される。

const fs = require('fs');
const { describe, it, expect } = require('./mini-test-lib');

const run = async () => {
  try {
    // testFilePathというパスにテストが書かれたファイルが格納されているとする
    const code = await fs.promises.readFile(testFilePath, 'utf8');
    eval(code);
  } catch (err) {
    console.log('Error');
    console.log(err.message);
  }
};

(async () => {
  await run();
})();

evalで実行される前に describe などの関数も読み込まれているので、問題なくテストコードは実行される。

ひとまずこれでテストランナーとしての最低限の役割は果たすことができるかと思う。

最後に

今回はdescribe などのテスト用の関数の定義と、テストファイルを読み込んで実際にテストを走らせるためのテストランナーを作成してみた。

結構手探りで実装してみた感じはあるが、テスト用ライブラリがどのように実行されるのかについて少しだけ理解できた気がする。
(ただ繰り返すが、 mochajest がこのような仕組みで実装されているかは分からない)

ひとまず最初の一歩をなんとか踏み出せたという気持ちである。