フロントエンドのお勉強のためにTODOアプリを作っております。
kotapontan.hatenablog.com github.com
今回、step1.0でコーディングしたTypeScriptの単体テストをJestで実施しました。
Jestとは
シンプルさを重視したJavaScriptテスティングフレームワーク。
- 高速で安全
- コードカバレッジが取得出来る
- モッキングが容易
- 分かりやすいエラーメッセージ
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に必要なパッケージをインストールする
このサイトを参考に準備をしました。
必要なパッケージをインストールします。
$ 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のクラスのモック方法が紹介されていたので、参考にさせてもらいました。
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.") });
まとめ
簡単なソースコードの単体テストをやってみたが、inputタグといったユーザが入力するコンポーネントを処理するソースは単体テストをするのは少し難しさを感じた。
簡単なソースで手間取ったので、もっと入力情報が多いものや複雑なものは単体テストが大変だと思った。
入力情報が多いものはコンポーネントを小さくしてテストしやすい作りにするのがベストなのかと思うが、フロントエンドの設計として何がベストプラクティスなのか分からないので、いろんな人の意見を見たり聞いたりしてテストしやすい設計ができるように心がけていきたい。
フロントエンドのテストは難しいなと思っていたら、Testing JavaScriptというテストに関する教材があるみたい。
英語で有料だけれども、静的解析からEndtoEndのテストに関してテスト全般を取り扱ってる。
記事を読んだだけで、ワクワクするような内容だった。
やっぱりテストって奥が深いな。