ヒトリ歩き

愚痴とかいろいろ書きます

フロントエンドのフの字も知らない私がJestで単体テストをやってみた

フロントエンドのお勉強のためにTODOアプリを作っております。

kotapontan.hatenablog.com github.com

今回、step1.0でコーディングしたTypeScriptの単体テストをJestで実施しました。

Jestとは

シンプルさを重視したJavaScriptテスティングフレームワーク

  • 高速で安全
  • コードカバレッジが取得出来る
  • モッキングが容易
  • 分かりやすいエラーメッセージ

jestjs.io

Jestを使うための準備

ディレクトリ構成

ディレクトリ構成は以下の構成とします。
テストソースは、__tests__配下に置きます。

.
├── README.md
├── __tests__
│   ├── actionevent.test.ts
│   ├── todoitem.test.ts
│   └── todolist.test.ts
├── dist
│   ├── bundle.js
│   └── index.html
├── jest.config.js
├── package-lock.json
├── package.json
├── src
│   ├── actionevent.ts
│   ├── index.ts
│   ├── todoitem.ts
│   └── todolist.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock

Jestに必要なパッケージをインストールする

このサイトを参考に準備をしました。

typescript-jp.gitbook.io

必要なパッケージをインストールします。

$ yarn add -D jest @types/jest ts-jest

Jestを設定する

jest.config.jsをプロジェクトのルートに追加します。

module.exports = {
    "roots": [
      "<rootDir>/"
    ],
    "testMatch": [
      "**/__tests__/**/*.+(ts|tsx|js)",
      "**/?(*.)+(spec|test).+(ts|tsx|js)"
    ],
    "transform": {
      "^.+\\.(ts|tsx)$": "ts-jest"
    },
  }

testMatch設定は、ts/tsx/jsフォーマットで書かれた.test/.specファイルを発見するためのglobのパターンマッチャーです。
transform設定は、Jestにts/tsxファイルに対してts-jestを使うように指示します。。

スクリプトターゲットを追加する

コマンドのエイリアスをpackage.jsonに追加します。

"scripts": {
  //....
  "test": "jest"
}

テストをやってみる

TodoItemクラスはオブジェクトが生成された際にテキストフィールドの文字列を取得します。
また、フィールドに持つ文字列を設定したTextオブジェクトを返す getTodoメソッドを実装している。

export class TodoItem {
    private todo:string = "";

    constructor() {
        const todoTextElement:HTMLInputElement | null =
            document.getElementById("todo") as HTMLInputElement;
        if (!todoTextElement) {
            throw Error("Not Found HTMLElement");
        }

        this.todo = todoTextElement.value;
    }

    public getTodo(): Text {
        const textnode = document.createTextNode(this.todo);
        return textnode;
    }
}

テストソースはこんな感じ

import {TodoItem} from "../src/todoitem";

test("constructor error", () => {

    try {
        const item:TodoItem = new TodoItem();
    } catch (e) {
        expect(e).toBeInstanceOf(Error);
    }

});

test("constructor successful", () => {
    document.body.innerHTML = '<div><input type="text" value="test" id="todo"></div>';
    const item:TodoItem = new TodoItem();
    expect((item as any).todo).toBe("test");
});

test("getTodo test", () => {
    document.body.innerHTML = '<div><input type="text" value="test2" id="todo"></div>';
    const item:TodoItem = new TodoItem();
    const text:Text = item.getTodo();
    expect(text).toBeInstanceOf(Text);
    expect(text.nodeValue).toBe("test2");
});

1つのテストをtest関数に記述していく。
test関数の第1パラメータにテストのタイトル。
test関数の第2パラメータにテストの処理を記述する。(無名関数)

test("テスト名を記述する", () => {

    // ここにテストの処理を書く

});

テスト結果の確認は、expect関数を使用して行う。
expect関数のパラメータに検査対象の変数を指定する。
expect関数に連結して検査関数を実行することになる。
下記の場合は、text.nodeValueが検査対象となりtoBe関数で検査している。 toBe関数は、オブジェクトの比較を行う。比較は、===を使用している。

expect(text.nodeValue).toBe("test2");

検査関数は他にも種類があるので、公式サイトを参照してください。 jestjs.io

作成したテストを実行する。
ここでは、todoitemだけをテストしたいのでjestコマンドのパラメータにテストソースを指定する。

$ ./node_modules/.bin/jest __tests__/todoitem.test.ts
PASS  __tests__/todoitem.test.ts
 ✓ constructor error (2ms)
 ✓ constructor successful (6ms)
 ✓ getTodo test (1ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.987s, estimated 2s
Ran all test suites matching /__tests__\/todoitem.test.ts/i.

全てのテストが成功しました。

SetupとTeardown

JUnitにsetUpとtearDownがあるようにJestにも同様の関数が用意されています。

関数名 実行タイミング
beforeEach テスト関数の実行前に呼ばれる。
afterEach テスト関数の実行後に呼ばれる。
beforeAll テストソースが読み込まれた際に、始めに実行される。
afterAll 全てのテストが終了した際に、実行される。
beforeEach(() => {
    console.log("テスト関数の実行前に呼ばれる");
})

afterEach(() => {
    console.log("テスト関数の実行後に呼ばれる");
});

beforeAll(() => {
    console.log("最初に1回呼ばれるだけ");
});

afterAll(() => {
    console.log("全てのテスト完了後に呼ばれる");
});

テストを強制失敗させる関数は用意されてない。

テストを強制失敗させるために、JUnitではfailメソッドが用意されています。
Jestでは同様の関数は存在しません。
テストを強制失敗させるには、Errorをthrowするしかないです。

TypeScriptのクラスをモックする

特定のクラスを呼び出している関数の試験をする際に、クラスのコンストラクタで色々と処理をしている場合があります。
コンストラクタを通すために色々と手間がかかるようであれば、モックしてしまうのも手の一つ。
ここでは、TodoItemクラスとTodoListクラスをモックします。
テスト対象のソースは以下のような実装です。

import {TodoItem} from "./todoitem";
import {TodoList} from "./todolist";

export function addTodo() {
    try {
        const todo:TodoItem = new TodoItem();
        const todolist:TodoList = new TodoList();
        todolist.pushTodo(todo.getTodo());
    } catch (e) {
        console.log(e);
    }
}

Jestの公式ページを見たが、TypeScriptのクラスのモック方法がなかった。
qiitaにTypeScriptのクラスのモック方法が紹介されていたので、参考にさせてもらいました。

qiita.com

import {TodoItem} from "../src/todoitem";
import {TodoList} from "../src/todolist";
import { addTodo } from "../src/actionevent";

jest.mock("../src/todoitem");
jest.mock("../src/todolist");
const TodoItemMock = TodoItem as jest.Mock;
const TodoListMock = TodoList as jest.Mock;

beforeEach(() => {
    TodoItemMock.mockClear();
    TodoListMock.mockClear();
});

test("test addTodo", () => {
    TodoItemMock.mockImplementation(() => {
        return {
            getTodo: (): Text => {
                return document.createTextNode("test");
            }
        }
    });

    TodoListMock.mockImplementation(() => {
        return {
            pushTodo:(text:Text): void => {
                return;
            }
        }
    });
    expect(addTodo());
});

test("test addTodo error", () => {
    TodoItemMock.mockImplementation(() => {
        throw new Error("constructor error.")
    });

    TodoListMock.mockImplementation(() => {
        pushTodo:(text:Text): void => {
            return;
        }
    });
    expect(addTodo());
});

TypeScriptのクラスをモックするポイントとしては、TypeScriptのクラスをjest.Mockで型変換する必要がある。

コンストラクタ内でErrorをスローしたい

コンストラクタでErrorをスローする場合は、mockImplementation内でthrowを定義すれば良い。

TodoItemMock.mockImplementation(() => {
    throw new Error("constructor error.")
});

stackoverflow.com

まとめ

簡単なソースコード単体テストをやってみたが、inputタグといったユーザが入力するコンポーネントを処理するソースは単体テストをするのは少し難しさを感じた。
簡単なソースで手間取ったので、もっと入力情報が多いものや複雑なものは単体テストが大変だと思った。
入力情報が多いものはコンポーネントを小さくしてテストしやすい作りにするのがベストなのかと思うが、フロントエンドの設計として何がベストプラクティスなのか分からないので、いろんな人の意見を見たり聞いたりしてテストしやすい設計ができるように心がけていきたい。

フロントエンドのテストは難しいなと思っていたら、Testing JavaScriptというテストに関する教材があるみたい。
英語で有料だけれども、静的解析からEndtoEndのテストに関してテスト全般を取り扱ってる。
記事を読んだだけで、ワクワクするような内容だった。

blog.engineer.adways.net

やっぱりテストって奥が深いな。