MochaとAssertを使用してNode.jsモジュールをテストする方法

提供:Dev Guides
移動先:案内検索

著者は、 Open Internet / Free Speech Fund を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。

序章

テストはソフトウェア開発の不可欠な部分です。 プログラマーは、アプリケーションが希望どおりに動作していることを確認するために、変更を加えたときにアプリケーションをテストするコードを実行するのが一般的です。 適切なテスト設定を使用すると、このプロセスを自動化することもでき、時間を大幅に節約できます。 新しいコードを記述した後に一貫してテストを実行することで、新しい変更によって既存の機能が損なわれないことが保証されます。 これにより、開発者はコードベースに自信を持てるようになります。特に、ユーザーがコードベースを操作できるように、コードベースを本番環境にデプロイする場合はそうです。

テストフレームワークは、テストケースを作成する方法を構成します。 Mocha は、テストケースを整理して実行する人気のあるJavaScriptテストフレームワークです。 ただし、Mochaはコードの動作を検証しません。 テストの値を比較するには、Node.js assertモジュールを使用できます。

この記事では、 Node.jsTODOリストモジュールのテストを作成します。 Mochaテストフレームワークを設定して使用し、テストを構成します。 次に、Node.js assertモジュールを使用して、テスト自体を作成します。 この意味で、Mochaを計画ビルダーとして使用し、assertを使用して計画を実装します。

前提条件

ステップ1—ノードモジュールの作成

テストしたいNode.jsモジュールを書くことからこの記事を始めましょう。 このモジュールは、TODOアイテムのリストを管理します。 このモジュールを使用すると、追跡しているすべてのTODOを一覧表示し、新しいアイテムを追加し、一部を完了としてマークすることができます。 さらに、TODOアイテムのリストをCSVファイルにエクスポートできるようになります。 Node.jsモジュールの作成について復習したい場合は、Node.jsモジュールの作成方法に関する記事を読むことができます。

まず、コーディング環境を設定する必要があります。 ターミナルにプロジェクトの名前でフォルダを作成します。 このチュートリアルでは、todosという名前を使用します。

mkdir todos

次に、そのフォルダに入ります。

cd todos

ここで、 npm を初期化します。これは、CLI機能を使用して後でテストを実行するためです。

npm init -y

依存関係はMochaだけで、これを使用してテストを整理および実行します。 Mochaをダウンロードしてインストールするには、次を使用します。

npm i request --save-dev mocha

Mochaは、dev依存関係としてインストールします。これは、実稼働環境のモジュールでは必要ないためです。 Node.jsパッケージまたはnpmの詳細については、npmおよびpackage.jsonでNode.jsモジュールを使用する方法に関するガイドをご覧ください。

最後に、モジュールのコードを含むファイルを作成しましょう。

touch index.js

これで、モジュールを作成する準備が整いました。 nanoのようなテキストエディタでindex.jsを開きます。

nano index.js

Todosクラスを定義することから始めましょう。 このクラスには、TODOリストを管理するために必要なすべての関数が含まれています。 次のコード行をindex.jsに追加します。

todos / index.js

class Todos {
    constructor() {
        this.todos = [];
    }
}

module.exports = Todos;

Todosクラスを作成することからファイルを開始します。 そのconstructor()関数は引数をとらないため、このクラスのオブジェクトをインスタンス化するために値を指定する必要はありません。 Todosオブジェクトを初期化するときに行うのは、空の配列であるtodosプロパティを作成することだけです。

modules行を使用すると、他のNode.jsモジュールでTodosクラスを要求できます。 クラスを明示的にエクスポートしないと、後で作成するテストファイルで使用できなくなります。

保存したtodosの配列を返す関数を追加しましょう。 次の強調表示された行に書き込みます。

todos / index.js

class Todos {
    constructor() {
        this.todos = [];
    }
    
    list() {
        return [...this.todos];
    }
}

module.exports = Todos;

list()関数は、クラスで使用されている配列のコピーを返します。 JavaScriptのdestructuring構文を使用して配列のコピーを作成します。 list()によって返される配列に対してユーザーが行った変更が、Todosオブジェクトによって使用される配列に影響を与えないように、配列のコピーを作成します。

注:JavaScript配列は参照型です。 これは、配列への variable 割り当て、または配列をパラメーターとして使用する関数呼び出しの場合、JavaScriptは作成された元の配列を参照することを意味します。 たとえば、xという3つのアイテムを含む配列があり、y = xy、[のような新しい変数yを作成するとします。 X143X]両方とも同じものを指します。 yを使用してアレイに加えた変更は、変数xに影響し、その逆も同様です。


次に、新しいTODOアイテムを追加するadd()関数を作成しましょう。

todos / index.js

class Todos {
    constructor() {
        this.todos = [];
    }
    
    list() {
        return [...this.todos];
    }
    
    add(title) {
        let todo = {
            title: title,
            completed: false,
        }
        
        this.todos.push(todo);
    }
}

module.exports = Todos;

add()関数は文字列を受け取り、それを新しいJavaScriptオブジェクトtitleプロパティに配置します。 新しいオブジェクトにはcompletedプロパティもあり、デフォルトではfalseに設定されています。 次に、この新しいオブジェクトをTODOの配列に追加します。

TODOマネージャーの重要な機能は、アイテムを完了としてマークすることです。 この実装では、todos配列をループして、ユーザーが検索しているTODOアイテムを見つけます。 見つかった場合は、完了のマークを付けます。 何も見つからない場合は、エラーをスローします。

次のようなcomplete()関数を追加します。

todos / index.js

class Todos {
    constructor() {
        this.todos = [];
    }
    
    list() {
        return [...this.todos];
    }
    
    add(title) {
        let todo = {
            title: title,
            completed: false,
        }
        
        this.todos.push(todo);
    }
    
    complete(title) {
        let todoFound = false;
        this.todos.forEach((todo) => {
            if (todo.title === title) {
                todo.completed = true;
                todoFound = true;
                return;
            }
        });

        if (!todoFound) {
            throw new Error(`No TODO was found with the title: "${title}"`);
        }
    }
}

module.exports = Todos;

ファイルを保存して、テキストエディタを終了します。

これで、実験できる基本的なTODOマネージャーができました。 次に、コードを手動でテストして、アプリケーションが機能しているかどうかを確認しましょう。

ステップ2—コードを手動でテストする

このステップでは、コードの関数を実行し、出力を観察して、期待どおりであることを確認します。 これは手動テストと呼ばれます。 これは、プログラマーが適用する最も一般的なテスト方法である可能性があります。 後でMochaを使用してテストを自動化しますが、最初に手動でコードをテストして、手動テストがテストフレームワークとどのように異なるかをよりよく理解できるようにします。

2つのTODOアイテムをアプリに追加し、1つを完了としてマークしましょう。 index.jsファイルと同じフォルダーでNode.jsREPLを起動します。

node

REPLに>プロンプトが表示され、JavaScriptコードを入力できることが示されます。 プロンプトで次のように入力します。

const Todos = require('./index');

require()を使用して、TODOモジュールをTodos変数にロードします。 モジュールがデフォルトでTodosクラスを返すことを思い出してください。

それでは、そのクラスのオブジェクトをインスタンス化しましょう。 REPLで、次のコード行を追加します。

const todos = new Todos();

todosオブジェクトを使用して、実装が機能することを確認できます。 最初のTODOアイテムを追加しましょう:

todos.add("run code");

これまでのところ、端末に出力は表示されていません。 すべてのTODOのリストを取得して、"run code"TODOアイテムが保存されていることを確認しましょう。

todos.list();

この出力はREPLに表示されます。

Output[ { title: 'run code', completed: false } ]

これは期待される結果です。TODOの配列に1つのTODOアイテムがあり、デフォルトでは完了していません。

別のTODOアイテムを追加しましょう:

todos.add("test everything");

最初のTODOアイテムを完了としてマークします。

todos.complete("run code");

todosオブジェクトは、"run code""test everything"の2つのアイテムを管理するようになります。 "run code"TODOも完成します。 もう一度list()を呼び出して、これを確認しましょう。

todos.list();

REPLは以下を出力します:

Output[
  { title: 'run code', completed: true },
  { title: 'test everything', completed: false }
]

ここで、次のようにしてREPLを終了します。

.exit

モジュールが期待どおりに動作することを確認しました。 コードをテストファイルに入れたり、テストライブラリを使用したりはしませんでしたが、コードを手動でテストしました。 残念ながら、この形式のテストは、変更を加えるたびに実行するのに時間がかかります。 次に、Node.jsで自動テストを使用して、Mochaテストフレームワークでこの問題を解決できるかどうかを確認しましょう。

ステップ3—MochaとAssertを使用して最初のテストを作成する

最後のステップでは、アプリケーションを手動でテストしました。 これは個々のユースケースで機能しますが、モジュールが拡張されるにつれて、この方法は実行可能性が低くなります。 新しい機能をテストするときは、追加された機能によって古い機能に問題が発生していないことを確認する必要があります。 コードを変更するたびに各機能をもう一度テストしたいと思いますが、これを手動で行うと、多大な労力を要し、エラーが発生しやすくなります。

より効率的な方法は、自動テストを設定することです。 これらは、他のコードブロックと同じように記述されたスクリプトテストです。 定義された入力を使用して関数を実行し、それらの効果を検査して、期待どおりに動作することを確認します。 コードベースが大きくなると、自動テストも大きくなります。 機能と一緒に新しいテストを作成すると、モジュール全体が引き続き機能することを確認できます。すべて、毎回各関数の使用方法を覚えておく必要はありません。

このチュートリアルでは、Node.jsassertモジュールでMochaテストフレームワークを使用しています。 それらがどのように連携するかを実際に体験してみましょう。

まず、テストコードを保存する新しいファイルを作成します。

touch index.test.js

次に、お好みのテキストエディタを使用してテストファイルを開きます。 以前のようにnanoを使用できます。

nano index.test.js

テキストファイルの最初の行で、Node.jsシェルで行ったようにTODOsモジュールをロードします。 次に、テストを作成するときに使用するassertモジュールをロードします。 次の行を追加します。

todos / index.test.js

const Todos = require('./index');
const assert = require('assert').strict;

assertモジュールのstrictプロパティを使用すると、Node.jsで推奨され、より多くのユースケースを考慮できるため、将来の保証に適した特別な同等性テストを使用できます。

テストの作成に入る前に、Mochaがコードをどのように編成するかについて説明しましょう。 Mochaで構造化されたテストは通常、次のテンプレートに従います。

describe([String with Test Group Name], function() {
    it([String with Test Name], function() {
        [Test Code]
    });
});

describe()it()の2つの主要な機能に注意してください。 describe()関数は、同様のテストをグループ化するために使用されます。 Mochaがテストを実行する必要はありませんが、テストをグループ化すると、テストコードの保守が容易になります。 同様のテストを簡単に更新できるように、テストをグループ化することをお勧めします。

it()には、テストコードが含まれています。 ここで、モジュールの関数を操作し、assertライブラリを使用します。 多くのit()関数は、describe()関数で定義できます。

このセクションの目標は、Mochaとassertを使用して手動テストを自動化することです。 これは、describeブロックから始めて段階的に実行します。 モジュール行の後にファイルに以下を追加します。

todos / index.test.js

...
describe("integration test", function() {
});

このコードブロックを使用して、統合テストのグループ化を作成しました。 ユニットテストは、一度に1つの機能をテストします。 統合テストは、モジュール内またはモジュール間で機能がどの程度連携して機能するかを検証します。 Mochaがテストを実行すると、そのdescribeブロック内のすべてのテストが"integration test"グループで実行されます。

モジュールのコードのテストを開始できるように、it()関数を追加しましょう。

todos / index.test.js

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
    });
});

テストの名前をわかりやすく説明していることに注目してください。 誰かが私たちのテストを実行すると、何が合格か不合格かがすぐに明らかになります。 十分にテストされたアプリケーションは通常、十分に文書化されたアプリケーションであり、テストは効果的な種類の文書化になる場合があります。

最初のテストでは、新しいTodosオブジェクトを作成し、アイテムが含まれていないことを確認します。

todos / index.test.js

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.notStrictEqual(todos.list().length, 1);
    });
});

コードの最初の新しい行は、Node.js REPLまたは別のモジュールで行うように、新しいTodosオブジェクトをインスタンス化しました。 2番目の新しい行では、assertモジュールを使用します。

assertモジュールから、notStrictEqual()メソッドを使用します。 この関数は、テストする値(actual値と呼ばれる)と取得する予定の値(expected値と呼ばれる)の2つのパラメーターを取ります。 両方の引数が同じである場合、notStrictEqual()はエラーをスローしてテストに失敗します。

保存してindex.test.jsを終了します。

長さは0である必要があり、1ではないため、基本ケースは真になります。 Mochaを実行してこれを確認しましょう。 これを行うには、package.jsonファイルを変更する必要があります。 package.jsonファイルをテキストエディタで開きます。

nano package.json

次に、scriptsプロパティで、次のように変更します。

todos / package.json

...
"scripts": {
    "test": "mocha index.test.js"
},
...

npmのCLItestコマンドの動作を変更しました。 npm testを実行すると、npmはpackage.jsonに入力したコマンドを確認します。 node_modulesフォルダーでMochaライブラリを検索し、テストファイルを使用してmochaコマンドを実行します。

package.jsonを保存して終了します。

テストを実行するとどうなるか見てみましょう。 ターミナルで、次のように入力します。

npm test

このコマンドは、次の出力を生成します。

Output> [email protected] test your_file_path/todos
> mocha index.test.js



integrated test
    ✓ should be able to add and complete TODOs


  1 passing (16ms)

この出力は、最初に、実行しようとしているテストのグループを示します。 グループ内の個々のテストごとに、テストケースはインデントされます。 it()関数で説明したように、テスト名が表示されます。 テストケースの左側にあるチェックマークは、テストに合格したことを示します。

下部に、すべてのテストの要約が表示されます。 私たちの場合、1つのテストに合格し、16ミリ秒で完了しました(時間はコンピューターによって異なります)。

私たちのテストは成功から始まりました。 ただし、この現在のテストケースでは、誤検知が発生する可能性があります。 false-positive は、失敗するはずのときに合格するテストケースです。

現在、配列の長さが1と等しくないことを確認しています。 この条件が当てはまらないときに当てはまるように、テストを変更してみましょう。 index.test.jsに次の行を追加します。

todos / index.test.js

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        todos.add("get up from bed");
        todos.add("make up bed");
        assert.notStrictEqual(todos.list().length, 1);
    });
});

ファイルを保存して終了します。

TODOアイテムを2つ追加しました。 テストを実行して、何が起こるかを確認しましょう。

npm test

これにより、次のようになります。

Output...
integrated test
    ✓ should be able to add and complete TODOs


  1 passing (8ms)

長さが1より大きいため、これは期待どおりに通過します。 ただし、最初のテストを行うという本来の目的は無効になります。 最初のテストは、空白の状態から開始することを確認するためのものです。 より良いテストは、すべての場合にそれを確認します。

TODOがまったくない場合にのみ合格するように、テストを変更しましょう。 index.test.jsに次の変更を加えます。

todos / index.test.js

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        todos.add("get up from bed");
        todos.add("make up bed");
        assert.strictEqual(todos.list().length, 0);
    });
});

notStrictEqual()strictEqual()に変更しました。これは、実際の引数と期待される引数が等しいかどうかをチェックする関数です。 引数が完全に同じでない場合、厳密な等しいは失敗します。

保存して終了し、テストを実行して、何が起こるかを確認します。

npm test

今回は、出力にエラーが表示されます。

Output...
  integration test
    1) should be able to add and complete TODOs


  0 passing (16ms)
  1 failing

  1) integration test
       should be able to add and complete TODOs:

      AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
+ expected - actual

- 2
+ 0
      + expected - actual

      -2
      +0

      at Context.<anonymous> (index.test.js:9:10)



npm ERR! Test failed.  See above for more details.

このテキストは、テストが失敗した理由をデバッグするのに役立ちます。 テストが失敗したため、テストケースの開始時にティックがなかったことに注意してください。

テストの概要は出力の下部には表示されなくなりましたが、テストケースのリストが表示された直後に表示されます。

...
0 passing (29ms)
  1 failing
...

残りの出力は、失敗したテストに関するデータを提供します。 まず、失敗したテストケースを確認します。

...
1) integrated test
       should be able to add and complete TODOs:
...

次に、テストが失敗した理由を確認します。

...
      AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
+ expected - actual

- 2
+ 0
      + expected - actual

      -2
      +0

      at Context.<anonymous> (index.test.js:9:10)
...

strictEqual()が失敗すると、AssertionErrorがスローされます。 expectedの値0は、actualの値2とは異なることがわかります。

次に、テストファイルにコードが失敗した行が表示されます。 この場合、10行目です。

これで、誤った値が予想される場合、テストが失敗することがわかりました。 テストケースを正しい値に戻しましょう。 まず、ファイルを開きます。

nano index.test.js 

次に、todos.add行を取り出して、コードが次のようになるようにします。

todos / index.test.js

...
describe("integration test", function () {
    it("should be able to add and complete TODOs", function () {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);
    });
});

ファイルを保存して終了します。

もう一度実行して、誤検知の可能性がないことを確認します。

npm test

出力は次のようになります。

Output...
integration test
    ✓ should be able to add and complete TODOs


  1 passing (15ms)

テストの復元力が大幅に向上しました。 統合テストを進めましょう。 次のステップは、新しいTODOアイテムをindex.test.jsに追加することです。

todos / index.test.js

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);
        
        todos.add("run code");
        assert.strictEqual(todos.list().length, 1);
        assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
    });
});

add()関数を使用した後、strictEqual()を使用してtodosオブジェクトによって管理されているTODOが1つあることを確認します。 次のテストでは、todosのデータをdeepStrictEqual()で確認します。 deepStrictEqual()関数は、期待されるオブジェクトと実際のオブジェクトが同じプロパティを持っていることを再帰的にテストします。 この場合、両方の配列にJavaScriptオブジェクトが含まれていることをテストします。 次に、JavaScriptオブジェクトが同じプロパティを持っていること、つまり、両方のtitleプロパティが"run code"であり、両方のcompletedプロパティがfalseであることを確認します。

次に、次の強調表示された行を追加して、必要に応じてこれら2つの同等性チェックを使用して残りのテストを完了します。

todos / index.test.js

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);
        
        todos.add("run code");
        assert.strictEqual(todos.list().length, 1);
        assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
    
        todos.add("test everything");
        assert.strictEqual(todos.list().length, 2);
        assert.deepStrictEqual(todos.list(),
            [
                { title: "run code", completed: false },
                { title: "test everything", completed: false }
            ]
        );
        
        todos.complete("run code");
        assert.deepStrictEqual(todos.list(),
            [
                { title: "run code", completed: true },
                { title: "test everything", completed: false }
            ]
    );
  });
});

ファイルを保存して終了します。

これで、テストは手動テストを模倣します。 これらのプログラムによるテストでは、実行時にテストに合格した場合、出力を継続的にチェックする必要はありません。 通常、使用のあらゆる側面をテストして、コードが適切にテストされていることを確認します。

npm testでもう一度テストを実行して、このおなじみの出力を取得しましょう。

Output...
integrated test
    ✓ should be able to add and complete TODOs


  1 passing (9ms)

これで、Mochaフレームワークとassertライブラリを使用した統合テストが設定されました。

モジュールを他の開発者と共有し、彼らがフィードバックを提供している状況を考えてみましょう。 ユーザーの大部分は、complete()関数が、まだTODOが追加されていない場合にエラーを返すことを望んでいます。 この機能をcomplete()関数に追加しましょう。

テキストエディタでindex.jsを開きます。

nano index.js

関数に以下を追加します。

todos / index.js

...
complete(title) {
    if (this.todos.length === 0) {
        throw new Error("You have no TODOs stored. Why don't you add one first?");
    }

    let todoFound = false
    this.todos.forEach((todo) => {
        if (todo.title === title) {
            todo.completed = true;
            todoFound = true;
            return;
        }
    });

    if (!todoFound) {
        throw new Error(`No TODO was found with the title: "${title}"`);
    }
}
...

ファイルを保存して終了します。

次に、この新機能の新しいテストを追加しましょう。 アイテムのないTodosオブジェクトでcompleteを呼び出すと、特別なエラーが返されることを確認したいと思います。

index.test.jsに戻ります:

nano index.test.js

ファイルの最後に、次のコードを追加します。

todos / index.test.js

...
describe("complete()", function() {
    it("should fail if there are no TODOs", function() {
        let todos = new Todos();
        const expectedError = new Error("You have no TODOs stored. Why don't you add one first?");
    
        assert.throws(() => {
            todos.complete("doesn't exist");
        }, expectedError);
    });
});

以前と同じようにdescribe()it()を使用します。 テストは、新しいtodosオブジェクトを作成することから始まります。 次に、complete()関数を呼び出すときに受け取ると予想されるエラーを定義します。

次に、assertモジュールのthrows()機能を使用します。 この関数は、コードでスローされたエラーを確認できるように作成されました。 その最初の引数は、エラーをスローするコードを含む関数です。 2番目の引数は、受け取ると予想されるエラーです。

ターミナルで、npm testを使用してテストをもう一度実行すると、次の出力が表示されます。

Output...
integrated test
    ✓ should be able to add and complete TODOs

  complete()
    ✓ should fail if there are no TODOs


  2 passing (25ms)

この出力は、Mochaとassertを使用して自動テストを行う理由の利点を強調しています。 テストはスクリプト化されているため、npm testを実行するたびに、すべてのテストに合格していることを確認します。 他のコードがまだ機能しているかどうかを手動で確認する必要はありませんでした。 それは私たちがまだ合格したテストのためだということを私たちは知っています。

これまでのところ、私たちのテストは同期コードの結果を検証しました。 非同期コードで動作するように、新しく見つかったテストの習慣をどのように適応させる必要があるかを見てみましょう。

ステップ4—非同期コードのテスト

TODOモジュールに必要な機能の1つは、CSVエクスポート機能です。 これにより、保存されているすべてのTODOが、完了ステータスとともにファイルに出力されます。 これには、fsモジュール(ファイルシステムを操作するための組み込みのNode.jsモジュール)を使用する必要があります。

ファイルへの書き込みは非同期操作です。 Node.jsのファイルに書き込む方法はたくさんあります。 コールバック、Promises、またはasync /awaitキーワードを使用できます。 このセクションでは、これらのさまざまなメソッドのテストを作成する方法を見ていきます。

コールバック

callback 関数は、非同期関数の引数として使用される関数です。 非同期操作が完了すると呼び出されます。

TodosクラスにsaveToFile()という関数を追加しましょう。 この関数は、すべてのTODOアイテムをループし、その文字列をファイルに書き込むことによって文字列を作成します。

index.jsファイルを開きます。

nano index.js

このファイルに、次の強調表示されたコードを追加します。

todos / index.js

const fs = require('fs');

class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }
    
    add(title) {
        let todo = {
            title: title,
            completed: false,
        }
        this.todos.push(todo);
    }

    complete(title) {
        if (this.todos.length === 0) {
            throw new Error("You have no TODOs stored. Why don't you add one first?");
        }

        let todoFound = false
        this.todos.forEach((todo) => {
            if (todo.title === title) {
                todo.completed = true;
                todoFound = true;
                return;
            }
        });

        if (!todoFound) {
            throw new Error(`No TODO was found with the title: "${title}"`);
        }
    }

    saveToFile(callback) {
        let fileContents = 'Title,Completed\n';
        this.todos.forEach((todo) => {
            fileContents += `${todo.title},${todo.completed}\n`
        });

        fs.writeFile('todos.csv', fileContents, callback);
    }
}

module.exports = Todos;

まず、fsモジュールをファイルにインポートする必要があります。 次に、新しいsaveToFile()関数を追加しました。 この関数は、ファイルの書き込み操作が完了すると使用されるコールバック関数を受け取ります。 その関数では、ファイルとして保存する文字列全体を格納するfileContents変数を作成します。 CSVのヘッダーで初期化されます。 次に、内部配列のforEach()メソッドを使用して各TODOアイテムをループします。 繰り返しながら、個々のtodosオブジェクトのtitleおよびcompletedプロパティを追加します。

最後に、fsモジュールを使用して、writeFile()関数でファイルを書き込みます。 最初の引数はファイル名todos.csvです。 2つ目はファイルの内容で、この場合はfileContents変数です。 最後の引数は、ファイル書き込みエラーを処理するコールバック関数です。

ファイルを保存して終了します。

saveToFile()関数のテストを書いてみましょう。 このテストでは、ファイルが最初に存在することを確認してから、ファイルの内容が正しいことを確認するという2つのことを行います。

index.test.jsファイルを開きます。

nano index.test.js

ファイルの先頭にあるfsモジュールをロードすることから始めましょう。これは、結果のテストに使用するためです。

todos / index.test.js

const Todos = require('./index');
const assert = require('assert').strict;
const fs = require('fs');
...

ここで、ファイルの最後に新しいテストケースを追加しましょう。

todos / index.test.js

...
describe("saveToFile()", function() {
    it("should save a single TODO", function(done) {
        let todos = new Todos();
        todos.add("save a CSV");
        todos.saveToFile((err) => {
            assert.strictEqual(fs.existsSync('todos.csv'), true);
            let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
            let content = fs.readFileSync("todos.csv").toString();
            assert.strictEqual(content, expectedFileContents);
            done(err);
        });
    });
});

以前と同様に、describe()を使用して、新しい機能が含まれるため、テストを他のテストとは別にグループ化します。 it()機能は、他の機能とは少し異なります。 通常、使用するコールバック関数には引数がありません。 今回は、引数としてdoneがあります。 コールバックを使用して関数をテストする場合は、この引数が必要です。 done()コールバック関数は、非同期関数が完了したときにそれを通知するためにMochaによって使用されます。

Mochaでテストされているすべてのコールバック関数は、done()コールバックを呼び出す必要があります。 そうでない場合、Mochaは関数がいつ完了したかを知ることができず、シグナルを待ってスタックします。

続いて、Todosインスタンスを作成し、それに1つのアイテムを追加します。 次に、ファイル書き込みエラーをキャプチャするコールバックを使用して、saveToFile()関数を呼び出します。 この関数のテストがコールバックにどのように存在するかに注意してください。 テストコードがコールバックの外にある場合、ファイルの書き込みが完了する前にコードが呼び出されている限り、テストコードは失敗します。

コールバック関数では、最初にファイルが存在することを確認します。

todos / index.test.js

...
assert.strictEqual(fs.existsSync('todos.csv'), true);
...

fs.existsSync()関数は、引数にファイルパスが存在する場合はtrueを返し、存在しない場合はfalseを返します。

<$>[注] ノート: Thefs モジュールの機能はデフォルトで非同期です。 ただし、主要な機能については、同期の対応物を作成しました。 このテストは、同期関数を使用することで簡単になります。非同期コードをネストして機能させる必要がないためです。 fsモジュールでは、同期機能は通常、名前の末尾に"Sync"が付いています。 <$>

次に、期待値を格納する変数を作成します。

todos / index.test.js

...
let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
...

fsモジュールのreadFileSync()を使用して、ファイルを同期的に読み取ります。

todos / index.test.js

...
let content = fs.readFileSync("todos.csv").toString();
...

ここで、readFileSync()にファイルの正しいパスtodos.csvを提供します。 readFileSync()はバイナリデータを格納するBufferオブジェクトを返すため、toString()メソッドを使用して、その値を保存したと予想される文字列と比較できます。

以前と同様に、assertモジュールのstrictEqualを使用して比較を行います。

todos / index.test.js

...
assert.strictEqual(content, expectedFileContents);
...

done()コールバックを呼び出してテストを終了し、Mochaがそのケースのテストを停止することを確認します。

todos / index.test.js

...
done(err);
...

errオブジェクトをdone()に提供し、エラーが発生した場合にMochaがテストに失敗できるようにします。

保存してindex.test.jsを終了します。

前と同じようにnpm testでこのテストを実行してみましょう。 コンソールに次の出力が表示されます。

Output...
integrated test
    ✓ should be able to add and complete TODOs

  complete()
    ✓ should fail if there are no TODOs

  saveToFile()
    ✓ should save a single TODO


  3 passing (15ms)

これで、コールバックを使用してMochaで最初の非同期関数をテストしました。 ただし、このチュートリアルの執筆時点では、 Node.jsで非同期コードを作成する方法の記事で説明されているように、Promisesは新しいNode.jsコードでのコールバックよりも一般的です。 次に、Mochaでもテストする方法を学びましょう。

約束

Promise は、最終的に値を返すJavaScriptオブジェクトです。 Promiseが成功すると、解決されます。 エラーが発生すると、拒否されます。

saveToFile()関数を変更して、コールバックの代わりにPromisesを使用するようにしましょう。 index.jsを開きます:

nano index.js

まず、fsモジュールのロード方法を変更する必要があります。 index.jsファイルで、ファイルの先頭にあるrequire()ステートメントを次のように変更します。

todos / index.js

...
const fs = require('fs').promises;
...

コールバックの代わりにPromisesを使用するfsモジュールをインポートしました。 次に、saveToFile()にいくつかの変更を加えて、代わりにPromisesで機能するようにする必要があります。

テキストエディタで、saveToFile()関数に次の変更を加えて、コールバックを削除します。

todos / index.js

...
saveToFile() {
    let fileContents = 'Title,Completed\n';
    this.todos.forEach((todo) => {
        fileContents += `${todo.title},${todo.completed}\n`
    });

    return fs.writeFile('todos.csv', fileContents);
}
...

最初の違いは、関数が引数を受け入れなくなったことです。 Promisesでは、コールバック関数は必要ありません。 2番目の変更は、ファイルの書き込み方法に関するものです。 writeFile()の約束の結果を返します。

index.jsを保存して閉じます。

Promisesで動作するように、テストを調整してみましょう。 index.test.jsを開きます:

nano index.test.js

saveToFile()テストを次のように変更します。

todos / index.js

...
describe("saveToFile()", function() {
    it("should save a single TODO", function() {
        let todos = new Todos();
        todos.add("save a CSV");
        return todos.saveToFile().then(() => {
            assert.strictEqual(fs.existsSync('todos.csv'), true);
            let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
            let content = fs.readFileSync("todos.csv").toString();
            assert.strictEqual(content, expectedFileContents);
        });
    });
});

最初に行う必要のある変更は、引数からdone()コールバックを削除することです。 Mochaがdone()引数を渡す場合は、呼び出す必要があります。そうしないと、次のようなエラーがスローされます。

1) saveToFile()
       should save a single TODO:
     Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js)
      at listOnTimeout (internal/timers.js:536:17)
      at processTimers (internal/timers.js:480:7)

Promisesをテストするときは、done()コールバックをit()に含めないでください。

約束をテストするには、アサーションコードをthen()関数に配置する必要があります。 テストでこのpromiseを返すことに注意してください。また、Promiseが拒否されたときにキャッチするcatch()関数がありません。

then()関数でスローされたエラーがすべて、it()関数にバブルアップされるように、promiseを返します。 エラーが発生しない場合、Mochaはテストケースに失敗しません。 Promisesをテストするときは、テストするPromisereturnを使用する必要があります。 そうでない場合は、誤検知が発生するリスクがあります。

また、catch()句は省略しています。これは、Mochaがpromiseが拒否されたことを検出できるためです。 拒否された場合、自動的にテストに失敗します。

テストが完了したので、ファイルを保存して終了し、npm testを指定してMochaを実行し、成功した結果が得られることを確認します。

Output...
integrated test
    ✓ should be able to add and complete TODOs

  complete()
    ✓ should fail if there are no TODOs

  saveToFile()
    ✓ should save a single TODO


  3 passing (18ms)

Promisesを使用するようにコードとテストを変更しましたが、これで確実に機能することがわかりました。 ただし、最新の非同期パターンではasync / awaitキーワードが使用されているため、成功する結果を処理するために複数のthen()関数を作成する必要はありません。 async /awaitでテストする方法を見てみましょう。

非同期/待機

async / awaitキーワードを使用すると、Promiseの操作が煩雑になりません。 asyncキーワードを使用して関数を非同期として定義すると、awaitキーワードを使用してその関数で将来の結果を取得できます。 このようにして、then()またはcatch()関数を使用せずにPromisesを使用できます。

async /awaitに基づいて約束されたsaveToFile()テストを簡略化できます。 テキストエディタで、index.test.jssaveToFile()テストに次の小さな編集を加えます。

todos / index.test.js

...
describe("saveToFile()", function() {
    it("should save a single TODO", async function() {
        let todos = new Todos();
        todos.add("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

最初の変更は、it()関数で使用される関数に、定義時にasyncキーワードが含まれるようになったことです。 これにより、本体内でawaitキーワードを使用できるようになります。

2番目の変更は、saveToFile()を呼び出すときに見つかります。 awaitキーワードは、呼び出される前に使用されます。 これで、Node.jsは、テストを続行する前に、この関数が解決されるまで待機することを認識しています。

then()関数に含まれていたコードをit()関数の本体に移動したため、関数コードが読みやすくなりました。 npm testでこのコードを実行すると、次の出力が生成されます。

Output...
integrated test
    ✓ should be able to add and complete TODOs

  complete()
    ✓ should fail if there are no TODOs

  saveToFile()
    ✓ should save a single TODO


  3 passing (30ms)

これで、3つの非同期パラダイムのいずれかを適切に使用して非同期関数をテストできます。

Mochaを使用して同期および非同期コードをテストすることで多くのことをカバーしました。 次に、Mochaがテストエクスペリエンスを向上させるために提供する他の機能、特にフックがテスト環境をどのように変更できるかについて、もう少し詳しく見ていきましょう。

ステップ5—フックを使用してテストケースを改善する

フックはMochaの便利な機能であり、テストの前後に環境を構成できます。 通常、describe()関数ブロック内にフックを追加します。フックには、一部のテストケースに固有のセットアップおよびティアダウンロジックが含まれているためです。

Mochaは、テストで使用できる4つのフックを提供します。

  • before:このフックは、最初のテストが開始される前に1回実行されます。
  • beforeEach:このフックはすべてのテストケースの前に実行されます。
  • after:このフックは、最後のテストケースが完了した後に1回実行されます。
  • afterEach:このフックはすべてのテストケースの後に実行されます。

関数または機能を複数回テストする場合、フックを使用すると、テストのセットアップコード(todosオブジェクトの作成など)をテストのアサーションコードから分離できるので便利です。

フックの値を確認するために、saveToFile()テストブロックにさらにテストを追加してみましょう。

TODOアイテムをファイルに保存できることを確認しましたが、保存したアイテムは1つだけです。 さらに、アイテムは完了としてマークされませんでした。 モジュールのさまざまな側面が機能することを確認するために、さらにテストを追加しましょう。

まず、TODOアイテムが完成したときにファイルが正しく保存されていることを確認するために、2番目のテストを追加しましょう。 index.test.jsファイルをテキストエディタで開きます。

nano index.test.js

最後のテストを次のように変更します。

todos / index.test.js

...
describe("saveToFile()", function () {
    it("should save a single TODO", async function () {
        let todos = new Todos();
        todos.add("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });

    it("should save a single TODO that's completed", async function () {
        let todos = new Todos();
        todos.add("save a CSV");
        todos.complete("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,true\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

テストは以前と同じです。 主な違いは、saveToFile()を呼び出す前にcomplete()関数を呼び出すことと、expectedFileContentsfalseではなくtrueがあることです。 completed列の値。

ファイルを保存して終了します。

npm testを使用して、新しいテストと他のすべてのテストを実行してみましょう。

npm test

これにより、次のようになります。

Output...
integrated test
    ✓ should be able to add and complete TODOs

  complete()
    ✓ should fail if there are no TODOs

  saveToFile()
    ✓ should save a single TODO
    ✓ should save a single TODO that's completed


  4 passing (26ms)

期待どおりに動作します。 ただし、改善の余地はあります。 どちらも、テストの開始時にTodosオブジェクトをインスタンス化する必要があります。 テストケースを追加すると、これはすぐに繰り返してメモリを浪費します。 また、テストを実行するたびに、ファイルが作成されます。 これは、モジュールにあまり詳しくない人が実際の出力と間違える可能性があります。 テスト後に出力ファイルをクリーンアップすると便利です。

テストフックを使用してこれらの改善を行いましょう。 beforeEach()フックを使用して、TODOアイテムのテストフィクスチャを設定します。 テストフィクスチャは、テストで使用される一貫した状態です。 この場合、テストフィクスチャは新しいtodosオブジェクトであり、すでに1つのTODOアイテムが追加されています。 次に、afterEach()を使用して、テストによって作成されたファイルを削除します。

index.test.jsで、saveToFile()の最後のテストに次の変更を加えます。

todos / index.test.js

...
describe("saveToFile()", function () {
    beforeEach(function () {
        this.todos = new Todos();
        this.todos.add("save a CSV");
    });

    afterEach(function () {
        if (fs.existsSync("todos.csv")) {
            fs.unlinkSync("todos.csv");
        }
    });

    it("should save a single TODO without error", async function () {
        await this.todos.saveToFile();

        assert.strictEqual(fs.existsSync("todos.csv"), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });

    it("should save a single TODO that's completed", async function () {
        this.todos.complete("save a CSV");
        await this.todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,true\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

行ったすべての変更を分析してみましょう。 beforeEach()ブロックをテストブロックに追加しました。

todos / index.test.js

...
beforeEach(function () {
    this.todos = new Todos();
    this.todos.add("save a CSV");
});
...

これらの2行のコードは、各テストで使用できる新しいTodosオブジェクトを作成します。 Mochaでは、beforeEach()thisオブジェクトは、it()の同じthisオブジェクトを参照します。 thisは、describe()ブロック内のすべてのコードブロックで同じです。 thisの詳細については、チュートリアル JavaScriptでの理解、バインド、呼び出し、および適用を参照してください。

この強力なコンテキスト共有により、両方のテストで機能するテストフィクスチャをすばやく作成できます。

次に、afterEach()関数でCSVファイルをクリーンアップします。

todos / index.test.js

...
afterEach(function () {
    if (fs.existsSync("todos.csv")) {
        fs.unlinkSync("todos.csv");
    }
});
...

テストが失敗した場合は、ファイルが作成されていない可能性があります。 そのため、unlinkSync()関数を使用してファイルを削除する前に、ファイルが存在するかどうかを確認します。

残りの変更により、参照が以前にit()関数で作成されたtodosから、Mochaコンテキストで使用可能なthis.todosに切り替わります。 また、個々のテストケースで以前にtodosをインスタンス化した行も削除しました。

それでは、このファイルを実行して、テストがまだ機能することを確認しましょう。 ターミナルにnpm testと入力すると、次のようになります。

Output...
integrated test
    ✓ should be able to add and complete TODOs

  complete()
    ✓ should fail if there are no TODOs

  saveToFile()
    ✓ should save a single TODO without error
    ✓ should save a single TODO that's completed


  4 passing (20ms)

結果は同じであり、利点として、saveToFile()関数の新しいテストのセットアップ時間をわずかに短縮し、残りのCSVファイルの解決策を見つけました。

結論

このチュートリアルでは、TODOアイテムを管理するNode.jsモジュールを作成し、Node.jsREPLを使用してコードを手動でテストしました。 次に、テストファイルを作成し、Mochaフレームワークを使用して自動テストを実行しました。 assertモジュールを使用すると、コードが機能することを確認できました。 また、Mochaを使用して同期および非同期機能をテストしました。 最後に、Mochaを使用してフックを作成しました。これにより、関連する複数のテストケースの記述がはるかに読みやすく保守しやすくなります。

この理解を身に付けて、作成する新しいNode.jsモジュールのテストを作成してみてください。 コードを書く前に、関数の入力と出力について考え、テストを書くことができますか?

Mochaテストフレームワークの詳細については、Mochaの公式ドキュメントをご覧ください。 Node.jsの学習を続けたい場合は、Node.jsシリーズのコーディング方法ページに戻ることができます。