ユニットテストって何?って人向けのmochaとchaiの使い方


自動テストって意識高そうで恰好良いですよね! 普段ブラウザ用のJavaScriptしか書かない人なのですが、テストについて調べても、難しい話から始まる記事ばかりで「アサーションって何?」とか「何で通常のファイルとは別のjsが必要なの?」みたいな気持でした。この記事では簡単な前提条件から、mocha/chaiの使い方の概要が分かる部分までを説明したいと思います。

そもそもテストとは何?

ソフトウェア開発におけるテストとは、ソフトウェアの振舞いが想定した通りであるかを検証することです。ソフトウェアのサブルーチンを取り出してテストする「単体テスト/ユニットテスト」と、ソフトウェア全体の挙動をテストする「結合テスト」があります。mocha・chaiはこのうちの「単体テスト」を対象としたツールです。

じゃあ単体テストって何をすることなの?

単体テストとは、ある特定の関数を対象に、入力値と出力値が仕様通りであるかを検査することを指します。

関数の仕様って何?

「あれがきたらこれが帰ってくる」という決まりで、その仕様によって色々と検証の仕方が考えられます。単純なものでは以下のような例が挙げられます。

  • 引数がaとbだったら、返り値はa+bになる
  • 引数がString型じゃなかったらErrorが帰ってくる
  • 引数がxからyの範囲だったら、返り値はmからnの範囲になる
  • 引数が有効な文字列だったらjQueryオブジェクトが帰ってくる

単体テストをするとどんなメリットがあるの?

単体テストをすると信頼性の高いコードの変更・修正ができるようになります。

コードを変更してもテストに合格しているということは、ソフトウェアの入出力の挙動が変化していない=デグレが起こっていないことを保証できます。これはリファクタリング(=振舞いを変えることなく、コードを整理して保守性を高めること)や新しい機能の追加を行う際に大きな恩恵があります。

テストコードの書き方

ユニットテストでは、特定の関数を対象としたテストを作成し、関数の入力値と・返り値が、期待する挙動になっているかを1つ1つ検証していきます───


ちょっとまって! テストしたい関数に入力値も返り値もないんだけど!?

…という場合、その関数のテストコードは書けないということになります。
つまり、単体テストをやりたかったら、関数が入力と出力を持つ設計をしないといけないということになります。面倒くさいですね! (テストダブルを使えば厳密には可能ですが、面倒なことに変わりはありません。)

このように、テストを意識することで、コードの設計にも気を使う必要がでてきます。しかし、関数の入出力を意識することで、基本的に副作用が少なくコンパクトな関数の設計になっていくはずです。テストしやすいコードを普段から意識することで、全体的なコーディング品質を高めることができます。


───たとえばJavaScriptの Math.round(3.5) で期待される結果は 4 なので、次のようなコードで検証ができます。

// 組込みのメソッドをテストする意味はあんまりない
if(Math.round(3.5) === 4) {
  console.log("Test Success!");
} else {
  throw new Error("Error!");
}

上の例では「そんなのテストするまでもなく当たり前じゃん!」という感じですが、Math.roundはともかく、自分で作った関数だったりすると、本当に常に絶対、設計意図通りの戻り値が帰ってくるかは分からないですよね? これならテストする意味があります。

// これならテストの意味がある
var myInstance = new MyInstance();
if(myInstance.magicMethod(user_input) === true) {
  console.log("Test Success!");
} else {
  throw new Error("Error!");
}

Mocha/Chaiのようなライブラリを使うと、上記のようなテストコードをもっと便利に記述・実行することができるようになります。

// ChaiのAPIを使用
var assert = chai.assert;

// Mochaの規則に従ってテストを実装
describe('マジックメソッド機能', function(){
    var myInstance = new MyInstance();

    it('ユーザーの選択がOKの時は有効', function() {
      assert.strictEqual(myInstance.magicMethod(true), true);
    });

    it('ユーザーの選択がNGの時は無効', function() {
      assert.strictEqual(myInstance.magicMethod(false), false);
    });

    it('ユーザーの選択がOKでもNGでない時はエラー', function() {
      assert.throws(function() {
        myInstance.magicMethod('string');
      }, Error);
    });
});

上の例にあるdescribe()it()といった関数はMochaの提供する機能。
見慣れないassertオブジェクトはChaiの提供する機能です。

describe/it/assertの意味

describe()it()の内側には、文字列とコールバック関数が入っています。この文字列はある種のコメントのようなものです。it()のコールバック関数の内側が1つのテストであることを意味しています。第一引数の文字列は、そのテスト項目の期待する結果を自然言語で説明した文言が入ります。

it()の中では、特別な「アサーション」と呼ばれる関数(この例ではassertにぶら下がるいくつかのメソッド)を使い、テストを実施します。アサーションはテスト結果が期待値と同じであるのかの真偽判定を行い、そのテスト項目の成否をフレームワークに伝えます。フレームワークはテスト結果が意図したものであるかどうかをチェックし、もし意図と異なる値が検出された場合はユーザーに通知します。

describe()関数は、複数のテストケースをフォルダのような階層関係でまとめるための機能で、文字列の部分はフォルダの名前のような感覚で使います。

ビヘイビア駆動開発

日本語だと分かりにくいですが、本来describeやitといったネーミングは、ソフトウェアの振る舞い(Behavior)を記述した自然な文章として読めることを意図されています。

describe('reminder function', function() {
  it('adds a reminder date when an invoice is created', function() {
     assert.equal(...);
  });
});

このようにな記法を「BDD(ビヘイビア駆動開発/振る舞い駆動開発)」と呼びます。

BDDとTDD

Moahaでは、BDDの他に「TDD(テスト駆動開発)」という記法も利用できます。TDDとBDDはテストフレームワークとしての機能としては同じですが、その運用方法には大きな思想の違いがあります。興味のある方は「TDD BDD 違い」などでググってみてください。個人的には、初心者にはBDDをおすすめします。

いまさら聞けないTDD/BDD超入門(1):テスト駆動開発/振る舞い駆動開発を始めるための基礎知識 (1/3) - @IT

このTDD/BDDといった考え方は、他の言語のテストフレームワークでも共通して利用されています。


補足: ChaiのBDD記法とTDD記法

実は、ChaiでもMochaと同じようにTDD/BDDの記法が選択できるようになっています。今回のサンプルではTDDの記法である「assert」を使っています。

本来であればMocha,Chaiの両方ともBDD記法を使うのが整合しているのですが、ChaiのBDD記法を生かすのであれば、他も全部英語で書かないと逆に不自然になってしまいます。

Expect / Should - Chai

そのため、今回の例ではシンプルに関数として見えるassertの記法を使用しています。


MochaとChaiの機能とメリット

このように、MochaはBDD/TDDをするための枠組みを提供し、chaiはテストを実地する為の便利なメソッド(アサーション)を提供してくれます。JavaScriptの型の比較や判定は面倒なので、信頼できる専用のアサーションライブラリがあると便利なのです。

  • Mochaの利点
    • BDD/TDDの書き方でテストケースを記述できる
    • テスト結果をUIやJSONに出力してくれる
    • コマンドラインからも実行できる (Node.js, PhantomJSが必要)
    • 非同期処理に対応
    • テスト前後の定型処理を after() や beforeEach() などのhookにまとめられる
  • Chaiの利点
    • 様々なテストの為のアサーション機能を提供してくれる(以下参照)
// 2つの引数の内容が末端まで完全に一致していたら成功
assert.deepEqual({hoge: 'hage'}, {hoge: 'fuge'});
// この例ではテスト失敗

// 2つの引数の内容が末端まで同一でなければ成功
assert.notDeepEqual({hoge: 'hage'}, {hoge: 'hage'});
// この例ではテスト失敗

// 最初と第3引数を、第2引数の演算子で比較し、trueならば成功
assert.operator(-1, '<', 3);
// この例ではテスト成功

// 第1引数が、第2引数を基準とした第3引数の絶対値内に収まっていれば成功
assert.closeTo(2.5, 3, 0.5);
// この例ではテスト成功

アサーションと例外処理の違い

アサーションと例外処理の違いは自分もよく分からなかったのですが、こちらの質問を見るとニュアンスは分かるのではないかと思います。

アサーションと例外処理の違いについて - Java | 【OKWave】

自分の場合は次のように解釈しています。

  • ソフトウェアのエラーを補完するための条件判定が例外処理
  • テストのための検算的な条件判定がアサーション

テスト用のHTMLファイルを作成し、ブラウザで実行する

もし上のようなテストコードをそのままプロダクトコード内に書いてリリースをしてしまうと、ユーザーにとってはメリットのない判定処理がたくさん実行されてしまい、パフォーマンスが低下してしまいます。その為、JSではテスト用のコードを、実際にリリースするコードとは分けた別の場所に用意します。

リリースされるファイルとは関係ない場所にHTMLを作成し

  • 実際にリリースされるメインのJSファイル
  • 実際に使用するCSSファイル
  • テストコードだけが書かれたJSファイル
<!DOCTYPE html>

<html>
<head>
  <meta charset="utf-8">
  <title>JavaScript Test</title>

  <link rel="stylesheet" href="(ここにサイトのCSSを挿入)">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.min.css">

</head>
<body>

  <!-- テストにHTML要素が必要な場合はここに入力 -->
  <div id="hoge">
    <p>Hoge Hoge Fuga Fuga</p>
  </div>

  <!-- このサイトで使用するJSライブラリ(jQuery等) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
  <!-- メインのJavaScript -->
  <script src="assets/js/script.js"></script>

  <!-- mocha -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.min.js"></script>
  <!-- chai -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.0.0/chai.min.js"></script>

  <!-- mocha初期化 -->
  <script>mocha.setup('bdd');</script>

  <!-- ここにテストコードを書く -->
  <script src="test.js"></script>

  <!-- mochaのUI -->
  <div id="mocha"></div>

  <!-- mochaを実行 -->
  <script>
    mocha.checkLeaks();
    // 必要なら他のグローバルオブジェクトを配列に追記する
    mocha.globals(['jQuery']);
    mocha.run();
  </script>
</body>
</html>

HTMLを使う機能のテストをしたい場合は、HTML内に適当な要素を記載しておき、それを対象にテストコードを実装すればOKです。


ちなみに、実際の開発ではGulp+Karma+Mocha+Chai+PhantomJSなどのテスト環境を作り、コマンドライン上でテストを自動で実地するところまでいけると本当にユニットテストの自動化ができて便利です。

関連するスライド資料

この記事に比べるとやや難易度の高い内容ですが、拙作のスライド「Web制作者視点で理解するソフトウェアテスト」で、Web制作者向けにJavaScriptのユニットテストとシステムテストの手法を紹介しています。

こちらもどうぞ!

参考リンク