かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

TypeScript で Jest 使ってみた

TypeScript(というか JavaScript) のユニットテストって何がいいのかなぁというのがわからなかったので、とりあえず自分の観測範囲でよくみる感じの Jest 試してみました。

とりあえずシンプルに以下のコマンドをうってコンソールで始めてみようと思います。

npm init -y
tsc --init
npm i -D jest
npm i -D @types/jest
npm i -D @types/node

tsconfig.json"outDir": "./dist", を設定してビルド結果が dist フォルダーに出るようにします。そして package.jsonscriptstestjest を設定して、jest の設定に rootDirdist にしておきます。package.json は以下のような感じになりました。

{
  "name": "jestlab",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/jest": "^24.0.15",
    "@types/node": "^12.6.1",
    "jest": "^24.8.0"
  },
  "jest": {
    "rootDir": "./dist"
  }
}

calc.ts というファイルを作って以下のようにしてみました。

export default class Calc {
    public add(x: number, y: number): number {
        return x + y;
    }
}

__tests__/Calc.ts を作ってテストを書いてみる。describe 関数でテストをグルーピングして it(test関数でも同じみたい) とか beforeEach, afterEach あたりでテスト本番とかテスト前処理や後処理とか書くのかな?

とりあえずこんな感じにテスト書いて

import Calc from '../calc';

describe('sum test', () => {
    var c = new Calc();
    beforeEach(() => {
        c = new Calc();
    });
    it('1 + 1 = 2', () => {
        expect(c.add(1, 1)).toBe(2);
    });
    it('2 + 2 = 4', () => {
        expect(c.add(2, 2)).toBe(4);
    });
});

npm test 叩いてみたら以下のような結果になりました。

$ npm test

> jestlab@1.0.0 test /home/okazuki/labs/jestlab
> jest

 PASS  dist/__tests__/calc.js
  sum test
    ✓ 1 + 1 = 2 (7ms)
    ✓ 2 + 2 = 4 (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.786s
Ran all test suites.

モックしたい

モック機能もあるみたいなのでついでに。

repo.ts を作って…

export default class Repo {
    async get(): Promise<string> {
        await new Promise(r => setInterval(r, 10000));
        return "Hello";
    }
}

これをモックしたいと思います。本番だと REST API とか呼んだりする感じ。今回も 10 秒待つのでテストのたびに呼び出されたらたまったもんじゃない感じです。

import Repo from '../repo'
import RepoClient from '../repoclient';

jest.mock('../repo');

describe('mock sample', () => {
    it('generateMessage', async () => {
        var getMock = jest.fn(() => Promise.resolve('Jest hello'));
        (<jest.Mock>Repo).mockImplementation(() => {
            return {
                get: getMock,
            };
        });
        var c = new RepoClient();
        expect(await c.generateMessage('world')).toBe('Jest hello world');
        expect(getMock).toBeCalled();
    });
});

とりあえずモックしたいモジュールを import して jest.mock でモックに差し替えるように設定する。 その後 jest.Mock にキャストして mockImplementation で new された時のダミー実装を返す感じ。単純な関数のモックは jest.fn でやるみたい。jest.fn に対しては toBeCalled みたいな専用のアサート関数があるようです。

なるほどね。import をモックを差し込むポイントとして使う機能があるのなら DI コンテナみたいな仕組みなくてもテスト出来ますね。

モック実装もタイプセーフな雰囲気にしたかったらインターフェース??repo.ts を、とりあえずこんな感じにして

export interface Repo {
    get(): Promise<string>;
}

export class RepoImpl implements Repo {
    async get(): Promise<string> {
        await new Promise(r => setInterval(r, 10000));
        return "Hello";
    }
}

repoclient.tsRepoImpl を使って

import {RepoImpl} from './repo';

export default class RepoClient {
    private readonly repo = new RepoImpl();

    public async generateMessage(x: string) {
        var r = await this.repo.get();
        return `${r} ${x}`;
    }
}

テストはインターフェースのモックを返すみたいな?

import {RepoImpl, Repo} from '../repo'
import RepoClient from '../repoclient';

jest.mock('../repo');

describe('mock sample', () => {
    it('generateMessage', async () => {
        var getMock = jest.fn(() => Promise.resolve('Jest hello'));
        (<jest.Mock>RepoImpl).mockImplementation(() => {
            return {
                get: getMock,
            } as Repo;
        });
        var c = new RepoClient();
        expect(await c.generateMessage('world')).toBe('Jest hello world');
        expect(getMock).toBeCalled();
    });
});

こうしておくとモックの実装部分で型の間違いが検出出来て便利かも?試しに間違えてみた。

f:id:okazuki:20190709093516p:plain

まとめ

とりあえず動いたけど、世間とあってるのだろうか。