作ったDiscordボットが正しく動くことは誰が保証するのでしょうか?それはもちろんボットを作ったあなたです!
正常ケースだけ動くことを確認できたら、とりあえずリリースしてみてユーザーをデバッグ担当にすることもできますが、機能追加や改修の度になにかしらの機能が壊れてユーザーからの報告を受けるのは、ボットの評判の失墜やサービスレベルの低下に繋がります。
しかし、毎回自分で機能を網羅的に確認するのも限界があります。そこでこの面倒な作業をある程度自動化したいわけですが、それが単体テストと呼ばれるものです。
単体テストとは
単体テストは関数やクラス、モジュールごとにテストコードを書き、そのロジックが意図した通りに実装されているかを確かめるものです。
しかしテストは誰かが勝手にいい感じに作ってくれるものではありません。最近は生成AIによるコーディング支援もありますが、最終的な正しさを担保するのは人間です。
テスト用のライブラリもありますが、それほど複雑でもないので、自分でテスト用の仕組みを作ってしまってもいいでしょう。基本的には次のような要素を含むコードを書きます。
- 切替え可能なテストフラグ。本番の実行時にテストコードが実行されると性能低下を引き起こす可能性があるので、テストフラグで切り替えられるようにしておきます。
- テスト用のメソッドを使った検証。JavaScriptには
console.assert()があり、与えられた式の結果が真でない場合に、エラーメッセージを表示します。ただし、例外とは異なり、コードの実行は打ち切られずに続行します。
// テストフラグが有効なら…
if (isTest) {
// 1d6ダイスを作ったら…
const dice = new Dice(1, 6)
// 個数は1個で、面数は6のはず
console.assert(dice.rolls == 1, '個数のテストに失敗しました')
console.assert(dice.sides == 6, '面数のテストに失敗しました')
// ロールしたときは1~6の範囲に収まるはず
// ...省略...
}単体テストの勘所
単体テストでは関数やクラスのメソッドひとつずつに対してテストコードを書いていきます。通常ひとつの関数にひとつのテストだけでなく、いくつものテストを書きます。正常ケースだけでなく、異常ケースの判定も入念に行いましょう。
以下のコードは入力された値の階乗を求める関数で、 であれば正常に動作しますが、負数に対しては無限ループに陥ってしまいます。もし負数が入力されたときのことを想定したテストコードを書くことができれば、リリース前に想定ケースの抜け漏れに気づけるでしょう。
function factorial(n) {
if (n == 0) {
return 1
}
return n * factorial(n - 1)
}
// 正常ケース
console.assert(factorial(0) == 1)
console.assert(factorial(1) == 1)
console.assert(factorial(2) == 2)
console.assert(factorial(3) == 6)
console.assert(factorial(4) == 24)
console.assert(factorial(5) == 120)
// 異常ケースのテストが足りていない!
// ...省略...特にコーナーケースやエッジケースは頭の片隅に入れておきましょう。0 や -1, とてつもなく大きな値などが代表的な値と言えます。
JavaScriptは動的型付け言語であり、変数の値や関数の引数の型を制限できないので、型を検査するコードを書きたくなりますが、それだとキリがなくなってしまうので、呼び出される過程で論理的に起こり得ない型の検査までは通常不要です。
例外をテストする
JavaScriptでは数値計算に失敗したときにはNaNを返しますし、オブジェクトに対して存在しないプロパティにアクセスしたときにはundefinedを返すので、例外が発生することはあまりありませんが、一部の関数やユーザー定義のコードでは例外が使われることがあります。
しかし、console.assert() には例外をテストする機能はないので、そのための機構は自分で作成しなければなりません。
function assertException(action, exception, message) {
try {
action()
// 正常に成功したらアサート失敗
console.assert(false, message)
} catch (e) {
// 例外が発生したが、意図した例外であるか判定する
console.assert(e instanceof exception, message)
}
}
assertException(
() => { throw new Error() },
Error,
'例外のテストに失敗しました'
)