1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Jest+TypeScriptで単体テスト

Posted at

Jest+TypeScriptで単体テスト

背景

ずいぶん前にJUnit5とMockitoで単体テストする記事を書いたのだが、TypeScriptとJestで同じことをしたいと思うことがあるので、備忘録として記事に残しておくことにした。

バージョンなど

package.jsonから抜粋。プロジェクト雛形はたとえばここなどを参考に作成しているものとして、ここでは割愛。

package.json
  "devDependencies": {
    "@types/jest": "^29.2.5",
    "jest": "^29.3.1",
    "ts-jest": "^29.0.5",
    "typescript": "^4.9.4"
  }

プロダクトコード

テスト対象となる、プロダクトコードを作成。

MainService.ts
import { SubService } from "./SubService";
export class MainService {
    private subService = new SubService();
    public getSum() {
        const sum = this.subService.getNumbers().reduce((p, c) => {
            return p + c;
        }, 0);
        return sum;
    }
}
SubService.ts
export class SubService {
    public getNumbers() {
        return [8, 7, 2, 3, 6, 4, 5, 8, 4, 0];
    }
}

テストコード

MainServicegetSum()を単体テストするコードを作成する。単体テストではthis.subService.getNumbers()の戻り値はモック化されるべきなので、TypeScriptを採用している場合のモック化方法をまとめる。

Javaではリフレクション、TypeScriptではas anyでモック注入

例えば、JavaのJUnitでプロダクトコードのprivateなメンバ変数をモックに置き換えたい場合、リフレクションを使用すると思う。著名なテストフレームワークであるMockitoであれば、@InjectMocksprivateな変数にモックしたインスタンスを注入できるが、これも結局は内部でリフレクションに置き換えているのだと思われる。

では、TypeScriptの場合はどうやってprivateな変数にモックを注入するのか、というとas anyで型制約をなくしてしまうのが一番簡単だ。

import { MainService } from "path/to/MainService";

test('jestのmock機能を使わない方法', () => {
    const service = new MainService();
    (service as any).subService = {   // as anyでprivateを無視して代入する
        getNumbers: () => [10, 20, 30, 40] // モック関数を定義
    };
    expect(service.getSum()).toEqual(100);
});

Jestで関数を部分的にモック化

上記の方法で、subServiceをモックオブジェクトに置き換えて単体テストすることができるが、Jestにはjest.fnなどモック専用の機能が用意されており、これを利用することでモックの呼び出し回数なども検証できるようになる。

import { MainService } from "path/to/MainService";
import { SubService } from "path/to/SubService";

test('jest.fnで作成したモック関数を使う方法', () => {
    const service = new MainService();
    (service as any).subService = {
        getNumbers: jest.fn(() => [10, 20, 30, 40]) // モック関数を定義
    };
    expect(service.getSum()).toEqual(100);
    expect((service as any).subService.getNumbers).toHaveBeenCalledTimes(1);//読み出し回数なども検証できる
});

test('jest.spyOnでインスタンスの特定の関数だけモック関数にする方法', () => {
    const service = new MainService();
    const subService = new SubService(); // 一度インスタンスを生成してから
    jest.spyOn(subService, 'getNumbers').mockImplementation(() => [10, 20, 30, 40]); // 差し替えて
    (service as any).subService = subService; // injectionする
    expect(service.getSum()).toEqual(100);
    expect(subService.getNumbers).toHaveBeenCalledTimes(1);
})

Jestでクラスを全体的にモック化

jest.mockSubServiceクラス自体をモックにすることができる。ただし、TypeScriptの場合はas jest.Mockで型変換しないと型チェック不正でモックを実装できないので注意。

import { MainService } from "../src/sample/MainService";
import { SubService } from "../src/sample/SubService";

// jest.mock でクラスをモックにする方法
jest.mock("../src/sample/SubService")
// 以降、SubService はモック化されている
// 必要なら SubService.mockClear() でモックの呼出記録をクリアする

test("インスタンス全体をモックにする方法", () => {
    (SubService as jest.Mock).mockImplementation(() => {
        // SubServiceをnewしたときに生成されるインスタンスを返す。つまりコンストラクタのモック化
        return {
            getNumbers: () => [10, 20, 30, 40]
        }
    })
    const service = new MainService();
    expect(service.getSum()).toEqual(100);
    expect(SubService).toHaveBeenCalledTimes(1);
});

なお、jest.mockでクラスごとモックにしても、以下のようにすることで特定のメソッドだけをモックにすることができる。

import { MainService } from "../src/sample/MainService";
import { SubService } from "../src/sample/SubService";

// jest.mock でクラスをモックにする方法
jest.mock("../src/sample/SubService")
// 以降、SubService はモック化されている

test("必要なメソッドだけをモックする方法", () => {
    const mockMethod = SubService.prototype.getNumbers as jest.MockedFunction<typeof SubService.prototype.getNumbers>;
    mockMethod.mockImplementation(() => [10, 20, 30, 40]);
    const service = new MainService();
    expect(service.getSum()).toEqual(100);
    expect(mockMethod).toHaveBeenCalledTimes(1);
});

所感

Javaと違って、TypeScriptはas anyしてしまえば型制約を無視して簡単にモックにしたり注入したりできるので、実はそこまでJestの使い方を知らなくてもなんとかなってしまう。だがそれ故にモック化する方法が統一されにくい側面もあり、このあたりはプロジェクト内で共通認識を持っておいたほうがいいかもしれない。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?