🍕

JestでTypeScriptのtest

2023/02/23に公開

Jest

Jestをnpm install

npm install jest --save-dev

packaga.jsonに以下を追加。npm testでtestが実施されるようになる

  "scripts": {
    "test": "jest"
  },

この時点でnpm testをすると以下のように表示される。
No tests found, exiting with code 1

Jestはデフォルトで*.spec.jsという名前のファイルを探してテストを実行する。

ts-jest

JestはJavaScipt用のテストフレームワークであり、TypeScript用にはts-jestがある。ts-jestはTypeScriptからJestへのブリッジができ、ts-jestはJavaScriptを生成せずにTyepeScriptのtestを実行できる。

ts-jestのインストールと、configureは以下のコマンドで実施できる。

npm install ts-jest --save-dev
npx ts-jest config:init
npm install typescript --save-dev
npm install @types/jest --save-dev

試しに最初のTypeScriptのテストを書いてみる。

first_jest_spec
test('should be false!!!', () => {
    expect(true).toBeFalsy();
});

test関数は二つの引数を受け取り、第一引数はテスト名を受け取り、第二引数で実際の実行する関数を受け取る。ここでは「should be false!!!」というテスト名、Jestのexpect関数を受け取っている。

これを実行すると下記のような表示になる。

watch mode

プロジェクト内のファイルが変更された際、自動でテストを走らせることもできる。package.jsonに下記のように記述。

{
    ...
    "scripts": {
        "test": "jest --watchAll --verbose"
    },
    ...
}


デフォルトでは全てのテストがファイルの変更があった際実施される。
いくつかのショートカットが用意されており、例えばfキーでfailedテストのみ実行するといったことも可能。oキーで関連テストのみ実行といったことも可能。

group test

ロジック毎にテストをグルーピングしたい場合は、下記のようにできる。

group.spec
describe("a group of tests", () => {
    test("first test", () => {
        expect("string value").toEqual("string value")
    })
    it("second test", () => {
        expect("hello").not.toEqual("world");
    })
    it("third test", () => {
        expect("abc").not.toEqual("def");
    })
});

Matchers

いくつかのMatcherを紹介。他のものは公式ドキュメント参照

.toBe(value)

プリミティブな値が等しいかのtest。
これはfalse。

to_be.spec
it("toBe", () => {
    expect(1).toBe(2);
});

.toEqual(value)

オブジェクトインスタンスの値が等しいかのtest。
trueになる。

to_equal.spec
it("should match with toEqual", () => {
    let objA = { id: 1 };
    let objB = { id: 1 };
    expect(objA).toEqual(objB);
});

.toContain(value)

アイテムが含まれているかのtest.これはtrue.

to_contain.spec
it("should contain a value", () => {
    expect("abcde").toContain("cde");
});

いくつかのMathcerはnotを使用できる。

it("should not contain a value", () => {
    expect("abcde").not.toContain("123");
});

.toContainEqual(value)

特定のアイテムが配列内に含まれるかのtest。trueになる

it("should contain an array item", () => {
    let objArray = [
        { id: 1 },
        { id: 2 }
    ];
    expect(objArray).toContainEqual({ id: 2 });
});

.toThrowError(error?)

エラーが呼ばれるかのtest。↓はtrue.

error.spec
function throwsError() {
    throw new Error("this is an error");
}

it("should throw an error", () => {
    expect(
        () => { throwsError() }
    ).toThrowError(new Error("this is an error"));
});

Setup and TearDown

テストを実行する前、もしくは後に、特定のclassをインスタンス化したり、値を用意したい場合がある。
例えば以下のようなclassがあり、それぞれのテスト実行時にこのクラスをインスタンス化したい。

class GlobalCounter {
    counter: number = 0;
    increment() {
        this.counter++;
    }
}

下のコードでは、beforeAllでGlobalCounterのインスタンスを作成しglogalCounter変数に格納している。これはdescribe内の各テストの前に毎回実施される。
そしてbeforeEachでcounterプロパティに0を格納。
最後にafterEachで各テストの実行後にconsole.logで結果を出力している。

set_up.sepec
describe("a group of set up tests", () => {
    let globalCounter: GlobalCounter;
    beforeAll(() => {
        globalCounter = new GlobalCounter();
    })
    beforeEach(() => {
        globalCounter.counter = 0;
    })
    afterEach(() => {
        console.log(`global Counter count: ${globalCounter.counter}`);
    })
    it("first test", () => {
        globalCounter.increment();
        expect(globalCounter.counter).toEqual(1);
    })
    it("second test", () => {
        expect(globalCounter.counter).toEqual(0);
    })
    it("third test", () => {
        globalCounter.increment();
        globalCounter.increment();
        expect(globalCounter.counter).toEqual(2);
    })
})

mock

mockのテストもできる。

mock.spec
class MyCallBack {
  public execCallBack(
      value: number,
      callback: (value: number) => number
  ) {
    console.log("executing CallBack");
    callback(value);
  }
}

describe("mock test", () => {
  let mockCallBack = jest.fn(x => x + 1);
  let myCallBack = new MyCallBack();
    it("should call the callback", () => {
        myCallBack.execCallBack(1, mockCallBack);
        expect(mockCallBack).toHaveBeenCalled();
    })
    it("returned value is plus 1 on parameter", () => {
        myCallBack.execCallBack(1, mockCallBack);
        expect(mockCallBack.mock.results[0].value).toBe(2);
    })
});

例えば、引数に1を足して返すというcallBackを使用するclassがある。
このcallBackを上記のようにjest.fnでmock化し、 MyCallBack()クラスのMyCallBackexecCallBackの関数に渡している。
一つ目のテストではcallBackが呼ばれたことをテストし、二つ目のテストでは、引数に1を足した数字が返ってくることをテストしている。上のコードではmockCallBack.mock.results[0].value
の部分でcallBackの返却値を受け取れる。

async await

async await構文もテストで使用できる。

class AsyncAwait {
    delayedPromise(): Promise<string> {
        return new Promise<string>(
            (resolve: (str: string) => void,
             reject: (str: string) => void) => {
                setTimeout(() => {
                    console.log(`2. returning success`);
                    resolve('success');
                }, 1000)
            }
        )
    }
}

例えば、1秒後に"success"という文字列を返すPromiseを扱うクラスがある。

これをasync awaitを使用して下記のようにテストできる。

async_await.spec
describe('async test with await', () => {
    let asyncWithPromise = new AsyncAwait();
    it('should return success', async () => {
        console.log(`1. calling delayedPromise`);
        let result = await asyncWithPromise.delayedPromise();
        console.log(`3. expecting result`);
        expect(result).toBe('success');
    })
});

console.logも1→2→3の順で実行されているのが確認できる。

jsdom

jsdomというライブラリを使ってHTMLのテストもできる。
必要なライブラリをインストール

npm install jsdom --save-dev
npm install jquery
npm install @types/jsdom --save-dev
npm install @types/jquery --save-dev
npm install --save-dev jest-environment-jsdom

jest.configに下記を追記。

jest.config
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jest-environment-jsdom',
};

テストコード。
ここではsetTestDivという関数を用意して、ここでは引数で渡ってきた文字列で<div id="test_div"></div>のHTMLを更新できることをテストしている。

jest_dom.spec
/**
 * @jest-environment jsdom
 */
import * as $ from 'jquery';
const setTestDiv = (html: string) => {
    $("#test_div").html(html);
}

it('should set text on div',  () => {
    document.body.innerHTML = '<div id="test_div"></div>';
    let html = $("#test_div");
    setTestDiv('hello world');
    expect(html.text()).toBe('hello world');
}); 

Discussion