今日からテストを実践するための知識
ソフトウェアの自動テストには長い歴史の積み重ねがあります。ここからは、その積み重ねの中で培われてきたベストプラクティスをいくつか紹介します。
テストケース名は日本語で書く
まず、テストケース名は日本語で書くというプラクティスです。これは先ほどの例ですでに実践していました。
Deno.test("1 + 2 は 3 である", () => { // テストケース名は日本語で書いている const x = add(1, 2); assertEquals(x, 3); });
もちろん世界中の誰もがコードを読むことができるOSSではテストケース名は英語です。しかし、開発者が日本人だけであれば、テストケース名を日本語にする方が良いでしょう。
今回のようなシンプルな例では英語でもすぐに理解できます。ただ、現場で書くテストコードはソフトウェアが扱う領域特有の用語が頻繁に出てきます。
そこで、架空のシステムの架空のテストケースを考えてみましょう。あるブログサービスがあり、無料ユーザーは個人プランのみ利用可能、チームプランは有料であり、月額契約も年間契約もできるとしましょう。このとき、例えば、「無料ユーザーは、所属チームに対して記事のレビュー依頼ができない」や「年間契約者は契約終了日を越えた日時に記事の公開予約ができない」といったテストケースがあれば、「所属チーム」や「契約終了日」「公開予約」の英語を考える前に、日本語で書いた方がスムーズです。また、後からコードを読む人にとっても、日本語の方がテストの意図が明瞭に伝わるでしょう。
テストケース名は「Xのとき、Yを返す(Yである)」という形式で書くことが一般的です。このため、先ほどのテストケース名「1 + 2 は 3 である」は、「1と2を渡したとき、3を返す」と書き換えても良いでしょう。
コードは自分が書く時間より人に読まれる時間の方が長いため、テストケース名一つにも気を配ることは開発者として良い姿勢です。
テストはArrange/Act/Assertのパターン(3Aパターン)で書く
今回紹介したテストはシンプルなものでしたが、中にはテストを実行するために事前準備が必要なテスト対象もあります。その場合、何が準備で何がテスト対象でどんな結果を期待しているかを分けて書くと良いテストコードになります。この考え方をパターン化したものが有名なArrange/Act/Assertの3Aパターンです。
Arrangeは前準備、Actは実行、 Assertは検証という意味です。3Aとはこれらの頭文字を合わせた表現です。ユニットテストに限らず多くのテストはこのパターンで表現するとシンプルに記述できます。
簡単なコードで3Aパターンのサンプルを紹介します。unit.ts
にUserManager
クラスを追加し、unit.test.ts
でUserManager
クラスのaddUser
メソッドをテストしてみます。
// ... type User = { id: number; name: string; email: string; }; export class UserManager { private users: User[] = []; private nextId: number = 1; addUser(name: string, email: string) { const newUser = { id: this.nextId++, name, email }; this.users.push(newUser); return newUser; } }
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"; import { add, UserManager } from "./unit.ts"; // UserManager も import する // ... Deno.test('ユーザーを1人追加できる', () => { // Arrange const userManager = new UserManager(); const name = 'John Doe'; const email = 'john.doe@example.com'; // Act const newUser = userManager.addUser(name, email); // Assert assertEquals(newUser.name, name); assertEquals(newUser.email, email); });
テスト対象であるaddUserというメソッドを実行(Act)する前にユーザーのデータを準備しなければなりません。これがArrangeです。Actの後には検証(Assert)が来ており、新しく追加されたユーザーの名前とメールアドレスが期待している値と一致しているかをしています。
なお、テストを書くときは「1テストケースで1アクションのみ」を書くように意識しましょう。1テストケースで複数のアクションを書いてしまうと何がテスト対象なのか分かりづらくなるからです。
さまざまなアサーションを知る
さまざまなアサーションを知るとシンプルで表現力豊かなテストコードを書くことができます。以下はDenoでよく使われるアサーションです。値がtrueであるか、2つ渡した値が同じなのか、値が存在するのか、値が配列に含まれているのかを検証しています。
-
assert(expr)
- 式が真(true)であることを確認します
-
例:
assert(add(1, 2) === 3)
-
assertEquals(actual, expected)
- 実際の値が期待値と等しいことを確認します。== 相当です。
-
例:
assertEquals(add(1, 2), 3)
-
assertStrictEquals(actual, expected)
- 実際の値と期待値の厳密な比較に使用します。=== 相当です。
-
例:
assertStrictEquals(add(1, 2), 3)
-
assertExists(actual)
- 値がundefinedまたはnullでないことを確認します。
-
例:
assertExists(add(1, 2))
-
assertArrayIncludes(actual, expected)
- 配列が指定された要素を含むことを確認します。
-
例:
assertArrayIncludes([1, 2, 3], [3])
これらはDeno特有のアサーションです。ただし、アサーションはテストに共通の考え方であり、他のテストフレームワークでも同じようなアサーションを備えているので基本的なアサーションを覚えておけば他でも応用ができます。
実際の現場で使っているテストフレームワークでは、今回紹介したアサーションがどのように書けるかぜひチェックしてみてください。
まとめ
本記事では、自動テストの最小単位であるユニットテストを説明し、JavaScript/TypeScriptを実行できるDenoを用いてコード例を紹介しました。
テストが通ること(Pass)とテストが失敗すること(Fail)は、開発者のみならずユーザーにとっても重要です。 テストが通ることについてユーザーの観点から着目すると、テストが通っているソフトウェアはバグを出さないことが期待でき、安心してサービスを利用できます。
開発者の観点からは、テストが通っていることはもちろん、テストの失敗それ自体も重要です。なぜなら、テストが失敗するときは自分が書いたコードが間違っているからです。この間違いを修正してテストが通るようになれば、自分が書いたコードにバグがないということがわかり、コードの品質も向上します。バグの有無が即座にわかること、つまり開発中の即時的なフィードバックが開発者にとって重要です。
ソフトウェア開発にバグはつきものですが、バグがあること自体が悪いのではなく、自分でバグに気付けずリリースしてしまうのが良くないことです。ユーザーからバグ報告を受ける前に社内でバグに気づけるのが理想です。自動テストは、開発者がすぐにバグを見つけるための手段であり、ひいてはユーザーにバグを出さないという理想に近づく手段でもあります。
さらに、自分が書いたコードのテストが通るという経験を積み重ねることは、自分が書いたコードに対する自信につながります。テストが通っているコードは安心してリリースできるため、ユニットテストは開発者の心の安全のためにも役立ちます。
本記事では自動テストの意義とユニットテストの書き方を紹介しました。次回は、企画・開発・リリース・リリース以降といったソフトウェア開発の大きな流れの中でユニットテストを位置付けることで、その意義と役割を深掘りします。
FizzBuzz の実装とテスト
以下にFizzBuzzの実装とテストを掲載します。よくある言い回しに「テストコードは仕様書である」というものがあります。シンプルな例ですがその意味がわかるかと思います。
// fizzbuzz.ts export function fizzBuzz(num: number) { const canDivideByThree = num % 3 === 0; const canDivideByFive = num % 5 === 0; if (canDivideByThree && canDivideByFive) { return "FizzBuzz"; } else if (canDivideByThree) { return "Fizz"; } else if (canDivideByFive) { return "Buzz"; } return num.toString(); }
// fizzbuzz.test.ts import { fizzBuzz } from './fizzbuzz.ts'; import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"; Deno.test('数値を文字列に変換する', () => { assertEquals(fizzBuzz(1), '1'); assertEquals(fizzBuzz(2), '2'); }); Deno.test('3の倍数を渡すと「Fizz」を返す', () => { assertEquals(fizzBuzz(3), 'Fizz'); assertEquals(fizzBuzz(6), 'Fizz'); assertEquals(fizzBuzz(9), 'Fizz'); }); Deno.test('5の倍数を渡すと「Buzz」を返す', () => { assertEquals(fizzBuzz(5), 'Buzz'); assertEquals(fizzBuzz(10), 'Buzz'); assertEquals(fizzBuzz(20), 'Buzz'); }); Deno.test('3と5両方の倍数を渡すと「FizzBuzz」を返す', () => { assertEquals(fizzBuzz(15), 'FizzBuzz'); assertEquals(fizzBuzz(30), 'FizzBuzz'); assertEquals(fizzBuzz(45), 'FizzBuzz'); });