この記事はモバイルファクトリー Advent Calendar 2020 7日目の記事です。
こんにちは、ブロックチェーンチームのソフトウェアエンジニア id:odan3240 です。湯船に浸かるのが楽しい季節になってきました。
以前テストに関するこの記事が話題になっていて、読んだときに最後の部分が目に留まりました。
テストを先に書いてから実装を書くか、先に書いた実装のテストをあとから書いているか、という場合でも違いが出てきそう。
以前までの自分は先に実装を書いてからテストを書くことがほとんどでした。理由としては、性格的にコードを書くのが好きで、頭の中にあるコードを急いで書き出したくなるため、作業に入ると先に実装を書いていました。
しかし、開発時に実装より先にテストケースから書き始めるとうまく実装が進むことに気付いたので、共有します。
割り算を行う関数 div
を例にすると次のような感じです。
export function div(a: number, b: number) { if (b === 0) throw new Error("divide zero"); return a / b; }
これに対して実装後に正常系と異常系で二分するようなテストを書いていました。
describe("div について", () => { describe("正常系", () => { it("動作が正しいこと", () => { expect.assertions(1); expect(div(11, 2)).toBe(5.5); }); }); describe("異常系", () => { it("例外を投げること", () => { expect.assertions(1); expect(() => div(11, 0)).toThrow(Error); }); }); });
今の見るとこのテストには次の問題があると考えています。
- 「正常系」「異常系」では、どういう条件で正常系/異常系の挙動になるのかがわからない
- 「動作が正しいこと」では、具体的にどういう挙動を求めているのかがわからない
どちらもテストコードを見なければその関数の仕様となる求めている条件や挙動がわかりません。
これの原因を考えてみると、先に書いた実装に合わせてテストケースを書いているからだと思いました。
この問題の解消のために、テスト対象の関数の仕様の列挙を目的にまずテストケースを書き始めるようになりました。先ほどの div
関数の例だと次のようなテストケースになります1。
describe("div について", () => { describe("割る数が0以外の場合", () => { it.todo("割り算が計算される"); }); describe("割る数が0の場合", () => { it.todo("例外を throw する"); }); });
ポイントは、挙動を確かめるために実装に沿ったテストケースを書くのではなく、テストの対象となる関数がどういう場合にどういう挙動になってほしいのかを表すテストケースを書くことです2。
仕様として先にテストケースから書くことによって次の利点があると考えています。
- テストケースの数によってそのメソッドの責務が過剰になっていないか事前に気付ける
- テストケースを書いた時点で実装する仕様に対して抜け漏れがないかチームメンバーにレビューを依頼できる
- テストケースが仕様として機能するため後からその関数がどういう仕様なのか楽に追える
まとめ
最近実践している、実装の前にテストケースを書くことで、仕様を整理してから実装に取り掛かることについて紹介しました。
明日の記事は id:mp0liiu さんです!