プログラミング初心者がアーキテクトっぽく語る

見苦しい記事も多数あるとは思いますが訂正しつつブログと共に成長していければと思います

JavaScript ES6 + JestでTDD(環境構築+基本編)

ブラウザ向けのコードをJavaScript ES6で書きましょう。

コードを書くならユニットテストをしながら書きたいですね。

今回はJavaScriptでTDD環境を整えます。ES6のimportが原因でエラーが出やすいので環境構築部分を詳細に書きました。(長文です)

node.jsのコードをTDDするケースは別記事にしたいと思います。


Jest

JestはJavaScriptユニットテストフレームワークです。

他にもMochaなど有名なフレームワークがありますが今回はJestを使います。


Babel

ブラウザ上で動かすコードをES6で書いてJestを実行するとエラーが出ます。原因はES6とCommonJSの違いです。

Jestはnode.jsで動きます。node.jsではモジュールの読み込みは「require」で行います。

一方、ES6ではモジュールの読み込みは「import」で行います。この「import」をJestが理解できなくてエラーが出ます。

解決策としてES6 コードをnode.jsが理解できるES5 コードに変換してJestを実行します。しかもJestで試験するときだけ変換を実行します。この変換作業を行うのがBabelです。


node.js

以降の手順は既にnode.jsがインストールされていることが前提です。

まだの人は下記のリンクを参考にしてインストールしてください

MacにNode.jsをインストールする - プログラミング初心者がアーキテクトっぽく語る


環境作成手順

プロジェクトフォルダを作成

  • 適当な名前のプロジェクトフォルダを作成します。
mkdir jest-sample
  • 作成したらVSCodeでOpen folder...からプロジェクトフォルダを開きます。

package.jsonの雛形作成

  • VSCodeのViewメニューからTerminalを選択します。
  • Terminalが開くので「npm init」と打ちます。
  • 質問には全てEnterでOKです。
  • package.jsonが作成されます。

モジュールをインストール

  • Babelをインストールします。
npm install --save-dev @babel/core @babel/preset-env
  • Jestをインストールします。
npm install --save-dev jest babel-jest @types/jest
  • --save-devを忘れないでください。

  • @types/jestも入れておくとVSCodeで補完ができて便利です。


jest.config.jsを作成

  • プロジェクトフォルダのトップ階層にjest.config.jsを作成して以下の内容を記述します。
module.exports = {
    verbose: true ,
    testMatch: [
        "**/test/**/*.test.js"
    ],
};

babel.config.jsの作成

  • プロジェクトフォルダのトップ階層にbabel.config.jsを作成して以下の内容を記述します。
module.exports = {
    presets: [
        [
            '@babel/preset-env',
            {
                'modules': 'false',
                'useBuiltIns': 'usage',
                'targets': '> 0.25%, not dead',
            }
        ]
    ],
    env: {
        test: {
            presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
        },
    },
};

package.jsonを編集

  • package.jsonのscripts > testsを下記の通り「jest」に変更します。
<変更前>
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
<変更後>
  "scripts": {
    "test": "jest"
  }

動作確認

  • プロジェクトフォルダにtestフォルダを作成します。
  • test/car.test.jsを作成し、以下の内容を記載します。
  • 「i」と入力すると「it」が補完候補として表示されることも確認して下さい。
  • 補完機能が動かない場合は@types/jestがインストールされていない可能性があります。
describe('nothing', () => {
    it('should be nothing', () => {

    });
});
  • Terminalで「npm test」を実行します。
$ npm test

> jest-sample@1.0.0 test /Users/john/Code/JavaScript/jest-sample
> jest

 PASS  test/car.test.js
  nothing
    ✓ should be nothing (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.471 s
Ran all test suites.
$ 
  • これで環境は完成です。

コーディング

  • 次はTest Firstでコーディングしていきます。内容はIntellijかPyCharmのTutorialに登場したcarクラスをいじってます。

インスタンスを作成するテスト

  • 最初のテストを削除してCarクラスをインスタンス化するテストをcar.test.jsに書きます。
  • この時点ではなにもAssertしていません。
  • 試験は「it」か「test」から始めます。
describe('Car', () => {
    it('should be created', () => {
        new Car();
    });
});
  • テストがFailすることを確認します。
$ npm test

> jest-sample@1.0.0 test /Users/john/Code/JavaScript/jest-sample
> jest

 FAIL  test/car.test.js
  Car
    ✕ should be created (1 ms)

  ● Car › should be created

    ReferenceError: Car is not defined

      1 | describe('Car', () => {
      2 |     it('should be created', () => {
    > 3 |         new Car();
        |         ^
      4 |     });
      5 | });

      at Object.<anonymous> (test/car.test.js:3:9)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.414 s
Ran all test suites.
npm ERR! Test failed.  See above for more details.
  • Carクラスがないので怒っています。

  • car.jsを作り空のCarクラスを記述します。

class Car {

}

export default Car;
  • car.test.jsでcar.jsをインポートします。
import Car from '../car'
  • テストがパスすることを確認します。
$ npm test

> jest-sample@1.0.0 test /Users/john/Code/JavaScript/jest-sample
> jest

 PASS  test/car.test.js
  Car
    ✓ should be created (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.174 s
Ran all test suites.
$
  • TDDではこんな感じのTest > Pass > Refactorを繰り返します。

  • このテストでimport関連のエラーが出る場合はBabel設定が間違っている可能性あります。Babel関係の設定を見直してください。


期待結果と比較する

  • 期待結果との比較はexpect().toXXXで行います。
describe('Car', () => {
    it('should be empty at first', () => {
        const car = new Car();
        expect(car.speed).toBe(0);
    });
});
  • どんなtoXXXが利用可能かはVSCodeで補完機能を使って確かめてください。

Fixtureを使う

  • 試験の最初にいつも実施する作業はbeforeEachにまとめます。

  • 下の例では各テストの中で実施していたインスタンス作成作業をbeforeEachに移しました。

describe('Car', () => {
    let car;
    beforeEach(() => {
        car = new Car();    <<< 移動
    });

    it('should be empty at first', () => {
        expect(car.speed).toBe(0);
    });

    it('should have speed of 5 after one acceleration', () => {
        car.accelerate();
        expect(car.speed).toBe(5);
    });
});

Exceptionを試験する

  • ExceptionはtoThrowで確認できます。便利。。。
    it('should throw exception if driver is distracted', () => {
        const action = () => {
            car.driveByGirls();
        };
        expect(action).toThrow('Keep your eyes on the road!');
    });

非同期の試験(Promise/then)

  • JavaScriptで多用する非同期処理の試験方法です。

  • Promiseを返す関数なら単純です。JestはPromiseがresolveされるまで待ってくれます。thenにexpectを書いておけばOKです。

  • car.jsに以下の関数を追加します。関数はPromiseを返します。

    coolDownForGivenTime(time) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(15);
            }, time * 1000);
        })
        .then((currentTemp) => currentTemp );
    }
  • car.test.jsに以下のテストを追加します。

  • Promiseが resolveされるのを待って、戻ってきたcurrentTempの値をexpect.toBeでチェックしてます。

    it('should cool down the room asynchronousy', () => {
        return car.coolDownForGivenTime(5).then((temp) => {
            expect(temp).toBe(15);
        })
    });

非同期の試験(async/await)

  • Promiseを返す関数ならasync/awaitを使って試験することもできます。
    it('should cool down the room asynchronousy with async/await', async() => {
        const temp = await car.coolDownForGivenTime(5);
        expect(temp).toBe(15);
    });

外部ライブラリをMockする

  • 外部ライブラリをモックする方法を紹介します。ここではradio.jsのRadio.playMusicメソッドをStub化します。

  • radio.jsの空ファイルを作成します。中身はなくても大丈夫です。

  • car.jsでradio.jsをimportしてRadio.playMusicを使用します。

import Radio from './radio';
<略>
    playMusic() {
        return Radio.playMusic();
    }
  • car.test.jsでradio.jsをimportしてRadio.playMusicを「La Bamba」を返すStubにしています。
import Radio from '../radio';
<略>
    it(`should play a music and return the title`, () => {
        Radio.playMusic = jest.fn().mockReturnValue('La Bamba');
        const title = car.playMusic();
        expect(title).toBe('La Bamba');
    });

fetch APIをテスト

長くなったので別記事にしました。

architecting.hateblo.jp

architecting.hateblo.jp


参考

https://jestjs.io/docs/en/getting-started

https://qiita.com/riversun/items/6c30a0d0897194677a37