12周年記念パーティ開催! 2024/5/10(金) 19:00

JavaScript開発のためのテスト入門 第1回 テストとはなにか

シリーズ第1回目は、JavaScript開発におけるテストを解説しつつ、実際にQUnitというテスト用のライブラリを使用して、テストを行います。簡単なサンプルを通して、テストのメリットがわかります。

発行

著者 外村 和仁 フロントエンド・エンジニア
JavaScript開発のためのテスト入門 シリーズの記事一覧

テストとはなにか

テストとはなんでしょうか? 「テスト」は日本語でいうと「試験」や「検査」という意味ですが、本シリーズで扱うのはソフトウェアのテストで、開発したプログラムが正しく動作するかを検証することです。

このシリーズでは、JavaScriptの開発におけるテストを扱いますので、普段、JavaScriptをある程度書いている方を対象としています。

例えば、次のようなJavaScriptの関数があったとします。

function parseName(str) {
  var name = str.split(/\s+/);

  return {
    firstName: name[0],
    lastName: name[1]
  };
}

名前を引数として呼び出すと、文字列をパースしてfirstNamelastNameというプロパティを持つオブジェクトとして返してくれるparseName関数です。

このような関数を作成したとき、期待通りに動いているかどうかを確認する方法は、いろいろあると思いますが、一番簡単なのはconsole.logなどを使って、ブラウザのデバッグツールで確認するという方法でしょう。

console.logを実行すると次のようになります。

console.log( parseName('Kazuhito Hokamura') );
//=> { firstName: 'Kazuhito', lastName: 'Hokamura' }

パースができて、期待している動作であることがわかります。次にこの関数でミドルネームがある場合も対応したいという変更が入ったとします。コードを次のように変更しました。

function parseName(str) {
  var name = str.split(/\s+/);

  return {
    firstName: name[0],
    middleName: name[1],
    lastName: name[2],
  };
}

確認してみると正常に動いているように見えます。

parseName('first middle last');
//=> {firstName: "first", middleName: "middle", lastName: "last"}

しかし、修正する前まで動いていた、先ほどのコードを実行すると次のようになります。

parseName('Kazuhito Hokamura');
//=> {firstName: "Kazuhito", middleName: "Hokamura", lastName: undefined}

middleNameHokamuraになっていて、期待した動作ではありません。

これは極端な例ですが、コードを修正したら、これまで動いていたコードが正常に動かなくなってしまうということは、ソフトウェアの開発においてよくあることです。

機能を修正するたびに、これまで動いてた動作を毎回手動で行い、目視ですべての結果を確認するのはとても手間がかかりますし、効率的ではありません*。そこで期待する動作をプログラムのコードで書いておいて、何度でも自動で動作の確認を実行しようというのがソフトウェア開発におけるテストです。

*注:手動や目視での確認はテストではない?

console.logなどを使って手動や目視で確認するのも、プログラムの動作確認ですので、広義ではテストと言えます。しかし、一般的にソフトウェアの開発のコンテキストで「テスト」と言うと、プログラムを使って行うテストのことを指すことが多いです。本シリーズでも単にテストといった場合はプログラムを使ったテストを指すことにします。

例えばparseNameの動作を検証するために、次のようなプログラムを書きました。

function assert(value, message) {
  if (!value) alert('AssertionError: ' + message);
}

var result1 = parseName('Kazuhito Hokamura');
assert(result1.firstName === 'Kazuhito', 'firstName is Kazuhito');
assert(result1.lastName === 'Hokamura', 'lastName is Hokamura');

var result2 = parseName('first middle last');
assert(result2.firstName === 'first', 'firstName is first');
assert(result2.middleName === 'middle', 'middleName is middle');
assert(result2.lastName === 'last', 'lastName is last');

assert関数は第一引数で受け取った値が偽であれば、テストが失敗したとみなしてメッセージをアラートで表示するというとても単純な関数です。この関数にparseNameを実行した結果の期待値を指定します。

次のデモは、parseName関数を読み込んだ後、assert関数を実行してテストします。

これを実行するとAssertionError: lastName is Hokamuraというアラートが表示され、テストが失敗することがわかります*。後はこのテストが成功するようにparseName関数を修正すれば、いちいちconsole.logの結果を目視で確認しなくても期待する動作になったのかどうかを確認できます。

*注:アラートの意味

アラートは「lastName is Hokamuraがエラーだ」と表示しているわけではなく、「lastName is Hokamuraとなるべきところが、その値になっていない(偽である)」という表示をしています。しかし、どのような値であったため、偽と判定されたのかまでは表示されません。

このように、プログラムの動作検証をプログラムで行うのがソフトウェア開発におけるテストです。

テストのためのライブラリ(QUnit)

先ほどのテストコードではassertという簡単な関数を使ってテストコードを書きましたが、なぜテストが失敗したかわからなかったり、結果がalertでしか表示されないなど、あまり使いやすいものではありませんでした。

そこで、テストを便利に行うためのライブラリがいろいろと存在していますから、そのようなライブラリを使ってテストを書くのがおすすめです。ここではQUnitというライブラリを紹介します。

JavaScriptで開発されたプログラムをテストするためのライブラリのひとつ。

上記サイトでQUnitのJavaScriptファイルとCSSファイルをダウンロードしてhtmlファイルに読み込み、続けて、次のようなテストを書きます。

test('parseName', function() {
  var result1 = parseName('Kazuhito Hokamura');
  equal(result1.firstName, 'Kazuhito', 'firstName is Kazuhito');
  equal(result1.lastName, 'Hokamura', 'lastName is Hokamura');

  var result2 = parseName('first middle last');
  equal(result2.firstName, 'first', 'firstName is first');
  equal(result2.middleName, 'middle', 'middleName is middle');
  equal(result2.lastName, 'last', 'lastName is last');
});

test関数でテストの対象を明示し、equal関数で値が正しいかどうかをチェックします。これを実行すると次のようになります。テストしている内容は先ほどの例とまったく同じです。

緑になっている部分が成功したテスト、赤になっているのが失敗したテストです*。失敗したテストの箇所にはHokamuraという期待値だが、結果はundefinedだったので失敗した、という表示がされ、どのようにテストが失敗したかがわかりやすくなっています。

*注:テスト結果の表示色

一般的にテストは緑が成功を意味し、赤が失敗を意味します。

このテストに通るように、parseName関数を次のように修正します。

function parseName(str) {
  var name = str.split(/\s+/);

  if (name.length === 2) {
    return {
      firstName: name[0],
      lastName: name[1]
    };
  }
  else if (name.length === 3) {
    return {
      firstName: name[0],
      middleName: name[1],
      lastName: name[2]
    };
  }
}

この書き直したparseName関数に対して、先ほどのテストを実行した結果は、次のようになります。テストに通るようにpraseName関数を修正し、再度テストを行いました。

このように、赤くなっているところがなくなり、すべてのテストが通ることが確認できました。

テストのメリット

ここまでの例示でテストのメリットが、なんとなくわかったと思います。ここで改めてテストをする意味を考えてみましょう。

品質の保持

先ほどのデモで、テストをパスしたparseName関数ですが、コードを見るとreturnのところの記述が重複していたり、空白文字が1つか2つの文字列しか想定していないなど、まだまだ改良の余地があります。

一行コードを修正するたびに、目視でいくつもの項目を確認しなければいけない場合、だんだんコードを修正するのが億劫になってしまいます。もしそれが、他人が書いたコードの修正であればなおさらです。

そうなってしまうと極力既存のコードは触らず、似たような関数を作るなどして対応するのが、その場しのぎの方法としてあります。ですが、これを続けると、あっという間にメンテナンスできないソフトウェアになるでしょう。

しかし、テストがあれば安心して改良に取り組むことができます。ここで書いたテストはすぐに実行できるのですから、コードを修正してからの確認は一瞬で済みます。テストを書いておけば、コードを修正するための手間を大きく改善することができます。

このように、テストはソフトウェアの品質を保つためのとても強力な手段の一つなのです。

品質の向上

また、テストを書くことはプログラムの品質を上げることにも一役買ってくれます。例えば次のコードを見てください。

$(function() {
  // フォームがサブミットされたら
  $('.form').submit(function() {
    // 名前のフィールドを探して
    var $name = $(this).find('input.name');

    // 名前の値をパースして
    var _name = $name.val().split(/\s+/);
    var name = {
        firstName: _name[0],
        lastName: _name[1]
    };

    // Ajaxで送信する
    $.post(url, { name: name }).done(function() {
      alert('成功しました');
    });
  });
});

フォームをサブミットしたら名前のフィールドからテキストを取得し、パースしてAjaxでPOSTするというプログラムです。しかし、この一連の処理をテストするのは少々大変です。テストするときに実際にform要素を作ったり、Ajax用のサーバーを用意する必要があるかもしれません。

そこで、まずは名前のパース部分だけをparseName関数として切り出しました。

$(function() {
  // フォームがサブミットされたら
  $('.form').submit(function() {
    // 名前のフィールドを探して
    var $name = $(this).find('input.name');

    // 名前の値をパースして
    var name = parseName($name.val());

    // Ajaxで送信する
    $.post({ name: name }).done(function() {
      alert('成功しました');
    });
  });
});

そうすると、少なくとも名前をパースするロジックのテストは簡単に行うことができます。もちろん、フォームのサブミット処理やAjaxの処理も必要に応じてテストが必要です。しかし、それらはHTMLに依存していたり事前準備が大変だったりと、テストを行うのに比較的手間がかかってしまいます。

そこでparseNameなど、外部の環境に影響を受けない*ロジックを重点的にテストするというのは一つの手法です。外部環境に影響を受けないということは、別の環境でも使える可能性があり、再利用性が高いということにもつながります。

parseName関数は引数で受け取る文字列にしか依存しないので、他のところでも名前のパースが必要になったときに簡単に使いまわすことができます。

*注:外部の環境に影響を受けない

この例でいうとフォームやAjaxと関係なく動くという意味です。

また、このような処理を担当する部分はMVCでいえば、主にModelにあたります。Modelにロジックを集めることはテストのしやすさを高めるとともに、プログラムの役割分担がきちんと行われた、メンテナンス性の高い設計につながります。

このように、テストがしやすいプログラムにしようとすると、自然とプログラムの再利用性やメンテナンス性が高まるという利点があります。

何のためのテスト?

テストを書くことはソフトウェアの品質を保つためにとても重要なことですが、テストを書いたからといってソフトウェアのバグがまったくなくなる、というわけではないことに注意してください(もしそうなら、世の中のソフトウェアのバグが完全になくなるということです。そうではないのは、一目瞭然ですね)。

以下はWikipediaにある「ソフトウェアテスト」からの引用です。

ソフトウェアテストは、プログラム中の欠陥(バグ)をできる限り多く発見することを目標として行われる。ソフトウェアテストに成功するとは、欠陥を発見することである。ソフトウェアテストでは、欠陥が存在することを示すことはできるが、欠陥が存在しないことは証明できない。

この文章が示すように、テストを書くことによってバグを発見することはできますが、バグがないということを証明することはできません。

例えば先ほどのparseName関数のテストでは、Kazuhito Hokamurafirst middle lastという入力の動作は保証されますが、それ以外の入力があった場合のテストケースはありません。

ですので、それ以外の入力値があった場合に正常に動作するという証明にはなりません。あくまでテストは、記述したテストケースが間違いなく動作することを保証するものなのです。

したがって、どのようなテストケースを書けばよいかというのは非常に重要になるのですが、これは同時に難しい問題でもあります。ケースにもよりますし、一概にこれだけ書いておけば間違いないという明確な答えがあるわけではありません。

最初はどのようなテストを書いていいかわからないと思いますが、プログラム自体の習得と同様、経験を積むことで習得できるスキルなので、わからないから書けないと悩むよりは、まず下手でもいいから書いてみることをおすすめします。経験を積むうちに、こういうテストを書いておけば安心できるな、とか、こういうふうにコードを整理するとテストも書きやすくなるなというのがわかってくるはずです。

いつテストを書くか

テストを書くタイミングはいくつか存在します。

機能を実装した直後

まずは機能を実装した後に、期待する動作のテストケースを書くというタイミングがあります。すでに実装があって、どのような動作をするかがはっきりわかるので、テストも書きやすいです。

プログラムの動作検証のためのテストだから、機能を実装した後に書くのが普通と思うかもしれませんが、実はそれ以外にもテストを書くタイミングはいくつか存在します。

機能を実装する前

プログラムを実装する前にテストを書くという開発手法があります。この手法はテスト駆動開発、またはTDD(Test Driven Development)と呼ばれています。

テストは最初に書き、そのテストを満たすような最低限の実装をし、テストが通ったらリファクタリングを行うということを繰り返しながら開発を進めることにより、動くコードとクリーンなコードの両立を目指すという開発手法です。

しかし、テストを書き慣れていない段階ではTDDは難易度が高いので、テストに慣れてきて、興味があれば学んでみるとよいでしょう。

バグを発見したとき

バグを発見したタイミングでテストを書くのは非常に重要です。まずバグを発見したらそのバグが再現するかどうかのテストを書きます。あとはそのテストが通るようにコードを修正すれば、テストがある限りそのバグが再発することはありません。

また、複数人で開発している場合や、オープンソースのソフトウェアなどで、他人が書いたコードにバグを見つけたとしましょう。そのような場合「このテストケースが通っていないよ」という具合に、バグの存在をテストのコードで知らせることができれば、バグを修正する側は、とても作業しやすくなります。

バグを再現するテスト書くことはとても重要ですが、テストをまったく書いていない場合、テストを実行する環境作りから始める必要があります。ですので、少しでもいいのでテストを書いておいて、いつでもテストを追加できるような環境を作っておくことは重要です。

まとめ

今回はソフトウェアにおけるテストの概要やメリットについて解説しました。

テストを楽に行うライブラリは、今回紹介したQUnit以外にもたくさんあり、それぞれ特徴や機能に違いがあります。次回はそのようなライブラリの紹介と比較を行う予定です。お楽しみに!