『AngularJS: Up and Running』読書メモ 第3章 AngularJSでのユニットテスト

AngularJS: Up and Running

コードが意図した通りに動いているか確認するのは書く人間の義務である。ユニットテストによって、コードがどのように振る舞うべきか定義することができる。また、ユニットテストを自動的に実行することで、振る舞いが意図せず変わっていないか確認することができる。

ユニットテストとは何か、何故必要なのか

ユニットテストとは、1つの関数や、コードの1部分を取り上げて、それが意図したとおりに動作しているか確認するテストを書く、というコンセプトである。

以下に、JavaScriptベースのクライアントサイドアプリケーションでユニットテストを書くべき理由を5つ挙げる。

(1) 正しさの証明

ユニットテストが無いと、手動テストを盲目的に信頼するしかない。ユニットテストは、意図したとおりに動作することの証明になる。

(2) コンパイラが無い

JavaScriptにはコンパイラが無いため、エラーは実行時にしかわからない。ブラウザで実行する前にユニットテストを走らせれば、エラーを検知できる。

(3) エラーを早く見つけられる

ユニットテストによって、実際にアプリケーションを実行するよりも早く以上に気付ける。

(4) エンバグを防ぐ

ユニットテストを書くことで、将来的に既存機能を壊してバグを作りこんでしまう可能性を低下させることができる。

(5) 仕様になる

コメントは実装と対応していない可能性があるが、ユニットテストは実装と対応しており、アプリケーションの仕様ともいえる。

初めてのTDD

テスト駆動開発(Test Driven Development, TDD)とは、アジャイルの方法論で、開発のスタイルを反転させる。TDDでは、コードを実装する前に、テストを書く。

TDDの信条は以下のとおり。

  • 失敗しているテストがコードを要求している場合にのみ、コードは書かれる
  • テストをパスするのに必要最小限のコードが書かれる
  • 重複は全てのステップで取り除かれる
  • 全てのテストが通ったら、次に必要な機能のための失敗するテストが追加される

これらのシンプルなルールによって、以下のことが保証される。

  • コードは有機的に成長し、コードの各行には目的がある
  • コードは高度にモジュール化され、凝集性があり、再利用可能である(テストを書くためにはこれらの要素が必要である)
  • 将来的なコードの毀損やバグを防ぐ包括的なテストが提供される
  • テストは仕様やドキュメンテーションとしても機能する

Karma入門

Karmaは高速なテストランナーである。NodeJSとSocketIOを利用して、複数のブラウザで高速にテストを実行している。

Karmaはテストを可能な限りシンプルで高速にすることを目指している。

以下がKarmaのインストール方法である。

  1. NodeJSをインストールする
  2. Karma CLIをインストールする(sudo npm install -g karma-cli)
  3. Karmaをテストを行うフォルダにインストールする(npm install karma)
  4. 使用するKarmaプラグインをインストールする(npm install karma-jasmine 等)

Karmaはプラグインシステムを採用しており、使用するテスティングフレームワークや各ブラウザ向けのテストランナーはプラグインでインストールする。以下のコマンドを実行すると、Jasmineと、Chromeのランチャー(テスト実行時にブラウザを自動で起動するスクリプト)がインストールされる。

npm install karma-jasmine karma-chrome-launcher

Karma及びプラグインのインストールは、テスト対象のプロジェクト内で実行する必要がある点に注意が必要。

テストランナーとテスティングフレームワーク

テストランナーとテスティングフレームワークは混同されがちである。これは、同じライブラリが両方の仕事をすることが多いからと思われる。

JavaScript(そしてAngularJS)では、それぞれの目的に2つの分離されたツールを利用する。Karmaはテストランナーで、全てのユニットテストを探してきて、ブラウザを開き、テストを実行して、結果を取得することに責任を持つ。テストの書き方には関知しない。

Jasmineはテスティングフレームワークである。テストを書くための文法、API、アサーションの書き方を定義する。テスティングフレームワークにはJasmine以外の選択肢(mocha、qunit等)もある。

Karmaプラグイン

Karmaプラグインは、以下の種類に分けられる。

  • ブラウザランチャー:テスト実行時にブラウザを自動的に起動する
  • テスティングフレームワーク:テストを書く際に使用するフレームワーク(Jasmine, mocha, qunit等)
  • レポーター:Karmaの出力を制御する
  • インテグレーション:既存のJavaScriptライブラリ・ツールとの統合(Closure, RuquireJS等)

Karmaの設定の解説

Karmaを使用するには、設定ファイル(karma.conf.js)が必要である。以下は設定ファイルの例である。

 // File:  chapter3/karma.conf.js
// Karma configuration
module.exports = function(config) {
    config.set({
        // base path that will be used to resolve files and exclude
        basePath: '',

        // testing framework to use (jasmine/mocha/qunit/...)
        frameworks: ['jasmine'],

        // list of files / patterns to load in the browser
        files: [
            'angular.min.js',
            'angular-mocks.js',
            'controller.js',
            'simpleSpec.js',
            'controllerSpec.js'
         ],

        // list of files / patterns to exclude
        exclude: [],

        // web server port
        port: 8080,

        // level of logging
        // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
        logLevel: config.LOG_INFO,

        // enable / disable watching file and executing tests whenever any file changes
        autoWatch: true,

        // Start these browsers, currently available:
        // - Chrome
        // - ChromeCanary
        // - Firefox
        // - Opera
        // - Safari (only Mac) // - PhantomJS
        // - IE (only Windows)
        browsers: ['Chrome'],

        // Continuous Integration mode
        // if true, it captures browsers, runs tests, and exits
        singleRun: false
    });
};

以下、設定ファイルの解説。

  • basePath: テスト対象の全てのファイルと、テストが必要とするファイルの読み込みのための起点となるパス。Karmaの設定ファイルからの相対パス。
  • frameworks: 読み込むフレームワークのリスト。jasmine, mocha, qunit等。
  • files: 読み込むファイルのリスト。AngularJSの場合、angular.jsを最初に読み込んで、その後テストヘルパーを含んだangular-mock.jsを読み込む。その後、アプリケーションのソースコードを読み込み、最後にユニットテストのファイルを読み込む。
  • exclude: 除外するファイルのリスト
  • port: Karmaのサーバがどのポートを使うか。デフォルトは8080。
  • logLevel: Karmaがブラウザから取得するログのレベル(全出力・エラーのみ出力等)
  • autoWatch: trueにすると、files設定に含まれる全てのファイルを監視して、変更があったら際に自動的に全てのテストが実行される
  • browsers: Karma起動時に開くブラウザ。実際の起動には、それぞれのブラウザに対応したプラグインが必要。
  • singleRun: 1つのユニットテストの終了後に、サーバーを停止するか否か。CI(Continuous Integration, Jenkins等)環境ではtrueに、それ以外ではfalseにすべき。

Karma設定の詳細はhttp://karma-runner.github.io/0.13/config/configuration-file.htmlを参照。

Karma設定の生成

以下のコマンドを実行することで、設定ファイルを生成できる。

karma init

これによって、対話シェルが起動する。一連の質問に応えることで、ニーズに応じたkarma.conf.jsファイルが生成される。

Jasmine: テストスペックのスタイル

Jasmineは振る舞い駆動のテストの書式を提供する。

振る舞い駆動のテストでは、大量のアサーションを書く代わりに、振る舞いと期待する状態を記述する。

Jasmineの文法

// File: chapter3/simpleSpec.js
// A test suite in Jasmine
describe('My Function', function() {

    var t;
    // Similar to setup
    beforeEach(function() {
        t = true;
    });

    afterEach(function() {
        t = null;
    });

    it('should perform action 1', function() {
        expect(t).toBeTruthy();
    });
    it('should perform action 2', function() {
        var expectedValue = true;
        expect(t).toEqual(expectedValue);
    });
});
  • describe: 一連のテスト(テストツイート)を作成する。これは複数のユニットテストのコンテナである。describeの対象はコントローラーやサービスである。また、describeのネストも可能である。
  • beforeEach: それぞれのテストの実行前に実行される処理を記述する
  • afterEach: それぞれのテストの実行後に実行される処理を記述する
  • it: ユニットテストの単位である。itブロックは、他のitブロックから独立しているべきである。状態のセットアップと、機能の実行、戻り値のチェックが含まれる。
  • expect: それぞれのexpectは値を取り、その後にマッチャーで期待する値を記述する。

Jasmineのマッチャー

全てのマッチャーは、expect(value)の後に続けて使用する。以下は代表的なマッチャーの例。

  • toEqual: 2つのオブジェクトの同一性を比較する
  • toBe: 参照をチェックする。2つの変数が同じオブジェクトを参照している場合はtrue。
  • toBeTruthy: JavaScriptでtrueとして評価される値か調べる
  • toBeFalsy: JavaScriptでfalseとして評価される値か調べる
  • toBeDefined: expectに渡されたリファレンスが定義済みか調べる
  • toBeUndefined: expectに渡されたリファレンスが未定義か調べる
  • toBeNull: expectに渡されたリファレンスがnullか調べる
  • toContain: expectに渡された配列がマッチャーに指定した要素を含むか調べる
  • toMatch: 正規表現によって渡された文字列にマッチするか調べる

※JavaScriptでfalseとして評価される値
– nullのオブジェクト
– 空の文字列
– 数値の0
– Booleanのfalse

コントローラーのユニットテストの例

以下のようなコントローラに対するコードを書く(書籍のサンプルコードを一部割愛)

angular.module('notesApp', [])
    .controller('ListCtrl', [function() {
        var self = this;
        self.items = [
            {id: 1, label: 'First', done: true},
            {id: 2, label: 'Second', done: false}
        ];

テストは以下のようになる。

describe('Controller: ListCtrl', function() {
    // Instantiate a new version of my module before each test
  beforeEach(module('notesApp'));

    var ctrl;

    // Before each unit test, instantiate a new instance
  // of the controller
  beforeEach(inject(function($controller) {
        ctrl = $controller('ListCtrl');
    }));

    it('should have items available on load', function() {
        expect(ctrl.items).toEqual([
            {id: 1, label: 'First', done: true},
            {id: 2, label: 'Second', done: false}
        ]);
    });
});

以下は、angular.mock.jsで定義されている、テスト用ヘルパーメソッド。

  • module(): 指定したモジュールを初期化する
  • inject(): AngularJSのサービスを関数に注入する

$controllerはテスト専用ではなく、AngularJSのサービスの1つ。

ユニットテストの実行

ユニットテストの実行には、以下のコマンドを実行する。

karma start

このコマンドは現在のディレクトリから自動的にkarma.conf.jsを見つけ出す。Karmaの設定ファイルが別の名前だったり、別の場所にある場合には、パスを指定する必要がある。

karma start tests/my_karama.conf.js

karmaコマンドはテスト用のサーバを立ち上げる。設定で記述したそれぞれのブラウザが自動的に起動する。自動的に起動しなかったブラウザも、手動で起動してページにアクセスすることでテストを実行できる。

テストの実行結果はコンソールに表示される。ブラウザには何も表示されない。

autoWatchがtrueになっている場合、karmaのサーバが起動した状態で、監視対象のファイルを書き換えて保存すると、テストが自動的に再実行される。

おわりに

AngularJSのユニットテストで注目すべき点の1つは、DI(Dependency Injection, 依存性の注入)が効果的に使われていることである。ユニットテストにおいては、テスト対象のモジュールが何に依存しているかを気にする必要がない。オブジェクトの初期化は全てAngularJSがやってくれる。

コメントをどうぞ

コメントを残す