LoginSignup
95
100

More than 1 year has passed since last update.

JavaScriptを単体テストする流行りのフレームワークJestを試してみる

Last updated at Posted at 2020-09-17

Jestとは

JestはJavaScriptの単体テストのフレームワークです。
https://jestjs.io/ja/

テストランナーだけでなく、モック機能やカバレッジの取得を使用することができます。npmのトレンドとしては2019年から伸びてmochaを超えるものとなっています。

image.png
https://www.npmtrends.com/jest-vs-jasmine-vs-mocha-vs-qunit

この記事は公式のサンプルコードを弄ってその挙動を確認するものとなっています。
また、実験環境は以下の通りです。

OS:MacOS Catalina 10.15.6
node.js: v14.10.1

簡単なはじめ方

jestを使用するためにnode.jsのプロジェクトを以下のように作成します。

 # package.jsonを作成する
 npm init -y
 # package.jsonにjestを追加してnode_modulesにインストール
 npm install --save-dev jest

jestではデフォルトでは__tests__フォルダ中のjavascritpをテストコードとして実行するので、__tests__フォルダにテストコードを追加します。

__tests__/sample.test.js
test('test 1.', ()=>{
  expect(1+2).toBe(3);
});

テストを動かすには以下のコマンドを実行します。

# テストを開始する
npx jest

image.png

設定ファイル

jestの設定は以下のいずれかの方法で指定することができます。

  • package.jsonに記載する
  • jest.config.jsに記載する
  • 実行時に--configオプションで指定したファイルに指定する

jest.config.jsの作成

下記のコマンドで対話式でjest.config.jsを作成します。

npx jest --init

image.png

カバレッジの収集方法

下記のように--coverageオプションを指定して実行することでカバレッジの収集を行います。

npx jest --coverage

このコマンドを実行するとテストが実行されて、coverageフォルダに結果が格納されます。
coverage/lcov-report/index.htmlにはhtml形式でレポートが表示されています。

image.png

Jenkins用のテストレポートを作成

Jenkins用のファイルを出力するには下記のプラグインが必要になります。
https://www.npmjs.com/package/jest-jenkins-reporter

以下のコマンドでインストールしてください。

npm install --save-dev jest-jenkins-reporter

レポートの出力方法の指定はjest.config.jsとpackage.jsonを使用します。

jest.config.js
// 略
 testResultsProcessor: "jest-jenkins-reporter",
// 略
package.json
   "jestSonar": {
     "reportPath": "reports",
     "reportFile": "test-reporter.xml",
     "indent": 4
   }

この設定を行った後にjestを実行するとreportsフォルダにtest-reporter.xmlに保存されます。
Jenkinsはこれを利用することで単体テストの結果を取得します。

VSCodeを使用したテストコードのデバッグ方法

VSCodeでテストコードをデバッグするにはワークスペースに以下のファイルを作成します。

.vscode/launch.json
   {
     "version": "0.2.0",
     "configurations": [

       {
         "type": "node",
         "request": "attach",
         "name": "Attach",
         "port": 9229
       }
     ]
   }

次にターミナル上で以下のコマンドを実行します。

node --inspect-brk node_modules/.bin/jest --runInBand __tests__/matcher.test.js

この後に、VSCodeのAttachボタンを押下することでデバッグ実行が可能になります。
image.png

以下は実際の画面操作のスクリーンキャプチャとなります。
debug.gif

VSCodeのテストランナー

Jest Test Explore

(2021/11追記)
vscode-jestを導入した方がいいです。Jest Test Explorerと同様な操作感で使用でき、vueでも安定して動作します。
https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest

Jest Test Explorerを使用することでVSCode上でテストが実行できる。
ここのテストを選択してテストを簡単に実行することが可能。

https://marketplace.visualstudio.com/items?itemName=kavod-io.vscode-jest-test-adapter
image.png

このプラグインはnode_modules/.bin/jestを使用して実行している。
このvueで使用する場合は以下のようにパスを変更する必要がある

vscode/settings.json
{
    "jestTestExplorer.pathToJest": "/full/workspace/path/node_modules/.bin/vue-cli-service test:unit"
}

非同期の試験

do引数の利用

テストメソッドの引数にdoneを与えて、テストメソッド中にそのdoneを実行することで任意のタイミングでテストの終了を通知することができます。

async.test.js
 function async_func() {
   return new Promise(resolve =>{
     console.log('start async_func');
     setTimeout(function() {
       console.log('end async_func');
       resolve('done!');
     }, 100);
   });
 }

 test('async callback test', done=> {
   async_func().then(msg => {
     expect(msg).toBe('done!');
     done();
   });
 });

doneを使用してテストメソッド中に呼ばれない場合はタイムアウトのエラーとなります。

エラーが発生するサンプル
 test('async callback not called', done=> {
   async_func();
 }); 

image.png

Promisesの試験

テスト対象のメソッドがPromisesを返す場合、テストメソッドでそのPromisesを返すことで、それが解決するまでテストメソッドの完了を待つことができます。
マッチャー関数のresolves/rejectsを使用してPromissesの結果を判定することも可能です。

async.test.js
 function async_func() {
   return new Promise(resolve =>{
     console.log('start async_func');
     setTimeout(function() {
       console.log('end async_func');
       resolve('done!');
     }, 100);
   });
 }

 function async_func_err() {
   return new Promise((resolve, reject) =>{
     console.log('start async_func_err');
     setTimeout(function() {
       console.log('end async_func_err');
       reject('error!');
     }, 100);
   });
 }
 test('async promiss resolve test 1', ()=> {
   return async_func().then(msg => {
     console.log('resolve test');
     expect(msg).toBe('done!');
   });
 });

 test('async promiss resolve test 2', ()=> {
   return expect(async_func()).resolves.toBe('done!');
 });

 test('async promiss reject test', ()=> {
   return async_func_err().catch(msg => {
     console.log('reject test');
     expect(msg).toBe('error!');
   });
 });

 test('async promiss reject test 2', ()=> {
   return expect(async_func_err()).rejects.toBe('error!');
 });

async/awaitの使用

async と awaitをテスト中に使用することも可能です。

async.test.js
 test('await ', async ()=> {
   const ret = await async_func();
   console.log(ret);
   expect(ret).toBe('done!');
 });

 test('await 2', async () => {
    await expect(async_func()).resolves.toBe('done!');
 });

setup,teardown使用したテスト毎の処理

各テストの開始前と開始後に特定の処理を実行することができます。

  • beforeAll
    • ファイル内のいずれかのファイルを実行する場合に実行される。
  • afterAll
    • ファイル内の全てのテストが完了した際に実行される。
  • beforeEach
    • ファイルまたはテストスイート内の各テストが実行される前に、関数を実行する。
  • afterEach
    • ファイルまたはテストスイート内の各テストが完了した際に、関数を実行する。

これらの関数はpromiseを返すことができ、jestはそのpromiseが解決するまでは処理が進みません。このデフォルトのタイムアウトは5000msとなっており、以下のように第二引数でタイムアウトの時間を調整することが可能です。

// 500msタイムアウトするのでエラーとなるサンプル
afterAll(()=> {
  console.log('afterall');
  return new Promise(r=> setTimeout(r, 1000));
}, 500);

beforeEach/afterEachは階層化されたdescribeごとに設定することができます。下記のコードはその実行順を検証したものとなります。

setup_teardown.test.js
 beforeAll(() => {
   console.log('beforeAll'); 
 });

 afterAll(() => {
   console.log('afterAll'); 
 });

 beforeEach(() => {
   console.log('before Each アウトスコープ');
 });

 afterEach(() => {
   console.log('after Each アウトスコープ');
 });

 test('test1', ()=> {
   console.log('tes1 を実行する');
 });

 test('test2', ()=> {
   console.log('tes2 を実行する');
 });

 describe('テストスィート', ()=> {
   beforeEach(() => {
     console.log('before Each スコープ内');
   });
   afterEach(() => {
     console.log('after Each スコープ内');
   });
   test('test3', ()=> {
     console.log('test3 を実行する');
   });
   test('test4', ()=> {
     console.log('test4 を実行する');
   });
 });

 test('test5', ()=>{
   console.log('test5を実行する');
 });

このコードの実行結果は以下のようになります。
describe中で指定したbeforeEach,afterEachは、その構造を考慮した順番で実行されていることが確認できます。

  ● Console

     console.log
       beforeAll

       at Object.<anonymous> (__tests__/setup_teardown.test.js:2:11)

     console.log
       before Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)

     console.log
       tes1 を実行する

       at Object.<anonymous> (__tests__/setup_teardown.test.js:18:11)

     console.log
       after Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)

     console.log
       before Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)

     console.log
       tes2 を実行する

       at Object.<anonymous> (__tests__/setup_teardown.test.js:22:11)

     console.log
       after Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)

     console.log
       before Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)

     console.log
       before Each スコープ内

       at Object.<anonymous> (__tests__/setup_teardown.test.js:27:13)

     console.log
       test3 を実行する

       at Object.<anonymous> (__tests__/setup_teardown.test.js:33:13)

     console.log
       after Each スコープ内

       at Object.<anonymous> (__tests__/setup_teardown.test.js:30:13)

     console.log
       after Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)

     console.log
       before Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)

     console.log
       before Each スコープ内

       at Object.<anonymous> (__tests__/setup_teardown.test.js:27:13)

     console.log
       test4 を実行する

       at Object.<anonymous> (__tests__/setup_teardown.test.js:36:13)

     console.log
       after Each スコープ内

       at Object.<anonymous> (__tests__/setup_teardown.test.js:30:13)

     console.log
       after Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)

     console.log
       before Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)

     console.log
       test5を実行する

       at Object.<anonymous> (__tests__/setup_teardown.test.js:41:11)

     console.log
       after Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)

     console.log
       afterAll

       at Object.<anonymous> (__tests__/setup_teardown.test.js:6:11)

特定のテストを実行するまたは実行しない。

test,it,describeにはskiponlyを使用してテストを実行するかどうかの制御を行うことができます。

skipは動かなくなったテストを暫定的にスキップする場合や、負荷が高くて通常は実行したくないテストを弾く場合に使用します。
まだ未実装のことを現したい場合はtodoを使用しましょう。todoではエラーを出力します。

onlyはデバッグ中に特定のテストのみを動作させる場合に使用します

なお、MsTestにあるpriorityは存在しないので、優先度を指定してのテストは行えないようです。

スキップの例.js
 describe('desc1', () => {
   test.skip('test1', () => {
   });

   test('test2', () => {
     // これだけ実行される
   });
 });

 describe.skip('desc2', ()=> {
   test('test3', () => {
   });

   test('test4', () => {
   });
 });
onlyの例.js
 describe.only('desc1', () => {
   test('test1', () => {
     // 実行される
   });

   test('test2', () => {
     // 実行される
   });
 });

 describe('desc2', ()=> {
   test('test3', () => {
   });

   test('test4', () => {
   });
 });

 describe.only('desc3', ()=> {
   test.only('test5', ()=> {
     // 実行される
   });

   test('test6', ()=> {
   });
 });

モック

jestではモックが使用できます。これを利用してテスト対象に依存する関数をテストに都合のいい振る舞いにすることができます。

特定の関数をモックにする例

spyOnを用いることで特定の関数をモックにすることができます。

テスト対象の関数

calc.js
 function sum(a, b) {
   return a + b;
 }
 function minus(a, b) {
   return a - b;
 }
 module.exports = {
   sum : sum,
   minus : minus
 }

テストコード

__tests__/sample.test.js
 const calc = require('../calc');

 test('test sum.', ()=> {
   expect(calc.sum(1, 2)).toBe(3);
 });
 test('特定の関数についてmockを使用して特定の値を返却する', ()=> {
   // モックを設定する
   console.log('spyOnの前', calc.sum, calc.minus);
   jest.spyOn(calc, 'sum').mockReturnValue(5);
   console.log('spyOnの後', calc.sum, calc.minus);

   // モックを呼び出す
   expect(calc.sum(1,2)).toBe(5);

   // モックを元の関数に戻す
   calc.sum.mockRestore();
   console.log('mockRestoreの後', calc.sum, calc.minus);
   expect(calc.sum(1,2)).not.toBe(5);
 });
 test('特定の関数についてmockを使用して特定の関数を実行する', ()=> {
   jest.spyOn(calc, 'sum').mockImplementation((a,b) => {
     console.log('mock function');
     return 100;
   });
   expect(calc.sum(1,2)).toBe(100);
   jest.restoreAllMocks();
   expect(calc.sum(1,2)).toBe(3);
 });

モジュール内の関数を全てモックにする

jest.mockを使用することで、jest.mockを実行したファイル内では、そのモジュールをモックとして使用します。
この例ではテストコード中でfsモジュールの関数をモックとして使用します。

テスト対象のモジュール

read_file.js
 const fs = require('fs');

 module.exports = function(path) {
   var text = fs.readFileSync(path, 'utf8');
   var lines = text.toString().split('\n');
   return lines;
 }

テストコード

テストコード.js
 const read_file = require('../read_file');
 const fs = require('fs')
 jest.mock('fs');
 test('test 1.', ()=>{
   const contents = 'test\ntest2\ntest3'
   fs.readFileSync.mockReturnValue(contents);
   const results = read_file('test.txt')
   console.log(results);
   expect(results).toEqual(['test','test2','test3']);
 });
 test('test 2.', ()=>{
   const contents = ''
   fs.readFileSync.mockReturnValue(contents);
   const results = read_file('test.txt')
   console.log(results);
   expect(results).toEqual([""]);
 });

マニュアルモックの使用例

マニュアルモックはモックデータを返すスタブを別ファイルに作成することができます。
__mocks__サブディレクトリにモックモジュールを作成します。

モックモジュール

__mocks__/fs.js
 'use strict';
 const fs = jest.createMockFromModule('fs');
 fs.readFileSync = function(path, enc) {
   return '1234\nabcde\n56789\n';
 }
 module.exports = fs;

テストコード
テストコードではfs.readFileSyncを使用するとモックモジュールで指定した関数が実行されます。

__tests__/manual_mock.test.js
 const read_file = require('../read_file');
 const fs = require('fs')
 jest.mock('fs');
 test('manual mock.', ()=>{
   const results = read_file('test.txt')
   expect(results).toEqual(['1234','abcde','56789', '']);
 });

タイマーと時刻の偽装

useFakeTimers を使用することでsetTimeout, setInterval, clearTimeout, clearInterval, nextTick, setImmediate、clearImmediateなどのタイマー関係の関数やシステム時刻の偽装を行えます。

システム時刻の偽装

setSystemTimeで偽の時間を指定することができます。ただし、これはuseFakeTimersでmodernを指定した時のみ有効です。
時刻を偽装しているときに本当の時刻を取得したい場合はgetRealSystemTimeを使用します。
時刻の偽装を止める場合はuseRealTimersを使用します。

時刻の偽装のテストコードの例.js
 test('時計のモックの確認', ()=> {

   const dumytime = new Date(2020, 0, 1, 23, 55,40);
   expect(new Date()).not.toEqual(dumytime);

   // Fakeの時間を使用する
   jest.useFakeTimers('modern');
   jest.setSystemTime(new Date(2020, 0, 1, 23,55,40));
   expect(new Date()).toStrictEqual(dumytime);

   console.log(new Date(jest.getRealSystemTime()));
   expect(new Date(jest.getRealSystemTime())).not.toStrictEqual(dumytime);

   // 通常の時間を使う
   jest.useRealTimers();
   expect(new Date()).not.toEqual(dumytime);
 });

タイマーの挙動の偽装

以下ではsetTimeoutの偽装を行いモックとして利用する例を示します。
https://jestjs.io/docs/ja/timer-mocks

テスト対象の関数

timerGame.js
 'use strict';

 function timerGame(callback) {
   console.log('Ready....go!');
   setTimeout(() => {
     console.log("Time's up -- stop!");
     callback && callback();
   }, 1000);
 }

 module.exports = timerGame;

テストコード

タイマーのモックの操作例.js
 test('waits 1 second before ending the game', () => {
   console.log(setTimeout);

   // setTimeout, setInterval, clearTimeout, clearInterval, nextTick, setImmediate
   // そして clearImmediateを偽装する
   jest.useFakeTimers();

   // setTimeoutがmockConstructorになる
   console.log(setTimeout);

   const timerGame = require('../timerGame');
   timerGame();

   // setTimeoutがどのように実行されたかを確認する
   expect(setTimeout).toHaveBeenCalledTimes(1);
   expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

   // タイマーシステムが保留しているあらゆるタイマーを削除します。
   // これを実行しないと他のテストでjest.runAllTimersを実行した際にsetTimeoutのコールバックが動作する
   jest.clearAllTimers()

   // 本物のTimer関数を使用するように戻す。
   jest.useRealTimers()
 });

useFakeTimers()を使用すると以降、setTimeoutがモックの関数になります。

偽装したタイマーを進める

偽装したタイマーはadvanceTimersByTimeで指定のmsを経過したことにすることができます。

タイマーを進めるテストコード.js
 test('タイマーを経過させる', () => {
   jest.useFakeTimers();


   const timerGame = require('../timerGame');
   const callback = jest.fn();

   timerGame(callback);

   // At this point in time, the callback should not have been called yet
   expect(callback).not.toBeCalled();

   // 半分だけ進める
   jest.advanceTimersByTime(500);

   expect(callback).not.toBeCalled();

   // 残り半分を進める
   jest.advanceTimersByTime(500);

   // Now our callback should have been called!
   expect(callback).toBeCalled();
   expect(callback).toHaveBeenCalledTimes(1);

   jest.clearAllTimers()
   jest.useRealTimers()
 });
偽装したタイマーのコールバックを実行する

偽装されたsetTImerはjest.runAllTimersまたはjest.runOnlyPendingTimersで設定したコールバックを実行することができます。
runAllTimersでは全てのタイマーを実行し、runOnlyPendingTimersは保留中のタイマーのみ実行します。
再帰的なタイマーの場合、runAllTimersを使用すると無限ループになるので、runOnlyPendingTimersを使用するようにします。

テスト対象のコード
再帰的なタイマーを使用しているとします。

infiniteTimerGame.js
 // infiniteTimerGame.js
 'use strict';

 function infiniteTimerGame(callback) {
   console.log('Ready....go!');

   setTimeout(() => {
     console.log("Time's up! 10 seconds before the next game starts...");
     callback && callback();

     // Schedule the next game in 10 seconds
     setTimeout(() => {
       infiniteTimerGame(callback);
     }, 10000);
   }, 1000);
 }

 module.exports = infiniteTimerGame;

テストコード

テストコードの例.js
 test('再帰的なタイマーを実行した場合', () => {
   jest.useFakeTimers();


   const infiniteTimerGame = require('../infiniteTimerGame');
   const callback = jest.fn();

   infiniteTimerGame(callback);


   // At this point in time, the callback should not have been called yet
   expect(callback).not.toBeCalled();


   // これだと無限ループになる
   // jest.runAllTimers();
   jest.runOnlyPendingTimers();

   // Now our callback should have been called!
   expect(callback).toBeCalled();
   expect(callback).toHaveBeenCalledTimes(1);

   expect(setTimeout).toHaveBeenCalledTimes(2);

   jest.clearAllTimers()
   jest.useRealTimers()
 });

期待値の確認

expectを使用して確認します。

テストコード.js
 test('test', ()=> {
   // test1()の結果が1であるか確認する
   expect(test1()).toBe(1);
   // test1()の結果が2でないことを確認する。
   expect(test1()).not.toBe(2);
 }

notを使用することで条件の反転が可能です。

  • toBe

    • プリミティブ値を比較したり、オブジェクトインスタンスの参照IDを確認したりします。
    • 浮動小数点のチェックには使用しないでください。
  • toBeCloseTo

    • 浮動小数点の近似的な値を比較します。
    • デフォルトでは小数点の第二位までチェックします。
浮動小数点のテスト例.js
 test('浮動小数点 - 桁数の確認', ()=> {
   // デフォルトでは有効桁二桁まで確認するーこれは合格する
   expect(0.1+0.201).toBeCloseTo(0.3);

   // 第二引数に有効桁を指定する。ーこれはNGとなる
   expect(0.1+0.201).toBeCloseTo(0.3,3);
 })
toEqualのテストコード.js
 function test5() {
   return {
     name : 'Abc',
     age : 15,
     group: {
       name : "Def"
     }
   }
 }

 test('equal', ()=> {
   expect(test5()).toEqual({
     name : 'Abc',
     age : 15,
     group: {
       name : "Def"
     }
   });
 }); 
  • toStrictEqual
    • toEqualに加えて型が一致するかも確認している
toStrictEqualのサンプルコード
 class LaCroix {
   constructor(flavor) {
     this.flavor = flavor;
   }
 }
 test('equal-strict',()=> {
   expect(new LaCroix('レモン')).toEqual({flavor: 'レモン'});
   expect(new LaCroix('レモン')).not.toStrictEqual({flavor: 'レモン'});
 });
  • toContain
    • 配列内に特定の値が含まれているか確認する
    • 文字列内に特定の文字列が含まれているか確認する
テストコード.js
 test('contein1', ()=> {
   const lst = ['abc', 'efg', 'hij'];
   expect(lst).toContain('efg');

   expect('abcdefghij').toContain('efg');
 });
  • toContainEqual
    • オブジェクトの配列中に同じ構造で同じ値のオブジェクトが含まれているか確認する
サンプルコード
 test('contein2', ()=> {
   const list = [
     {a:1234},
     {b:2345, c:'def'},
     {c:1111, d: { e:2222}}
   ];
   expect(list).toContainEqual({a:1234});
   expect(list).toContainEqual({b:2345, c:'def'});
   expect(list).toContainEqual({c:1111, d:{e:2222}});
 });
  • toThrow
    • 例外が発生することを確認する
    • 引数としてエラメッセージの文字、正規表現、エラーオブジェクト、クラスを与えることができる
サンプルコード.js
 test.only('throw', ()=> {
   expect(()=> {
     error_func();
   }).toThrow();

   expect(()=> {
     error_func();
   }).toThrow('Error');

   expect(()=> {
     error_func();
   }).toThrow(/rr/);

   expect(()=> {
     error_func();
   }).toThrow(new Error('Error'));

   expect(()=> {
     error_func();
   }).toThrow(Error);

 });

テストの同時実行

test.concurrentを使用することでテストを同時に実行することが可能です。
ただし26.4時点で実験的な機能なため以下のような問題が存在しています。
https://github.com/facebook/jest/labels/Area%3A%20Concurrent

以下のコードはconcurrentを使用しないケースと使用したケースの挙動を確認しています。

concurrentを使用しないケース
test('addition of 2 numbers', () => {
  console.log('test1');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test1');
      expect(5 + 3).toBe(8);
      resolve('done!');
    }, 3000);
  });
});

test('subtraction 2 numbers', () => {
  console.log('test2');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test2');
      expect(5 - 3).toBe(2);
      resolve('done!');
    }, 1000);
  });
});

この実行結果は以下のようになります。
image.png
1つ目のテストケースが完了した後に、2つ目のテストケースが完了することがわかります。

以下はconcurrentを使用したケースです。

concurrentを使用した場合
test.concurrent('addition of 2 numbers', () => {
  console.log('test1');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test1');
      expect(5 + 3).toBe(8);
      resolve('done!');
    }, 3000);
  });
});

test.concurrent('subtraction 2 numbers', () => {
  console.log('test2');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test2');
      expect(5 - 3).toBe(2);
      resolve('done!');
    }, 1000);
  });
});

この実行結果は以下のようになります。
image.png

この結果より同時にテストケースが動作していることが確認できます。

VueでJestを利用する

前提:
@vue/cli 4.5.6 がインストールされている。

単純な作成例

「vue create」を実行した際にマニュアルで設定を行うと単体テストをjestで行えるように指定できます。
ここでは後からjestを導入することを想定した手順とします。

以下のコマンドを実行することで、vueプロジェクトにjestを使うためのプラグインをインストールしています。

コマンド
 # デフォルトでvueのプロジェクトを作成する
 vue create vue_test
 cd vue_test
 # テストツールのインストール
 npm install --save-dev @vue/cli-plugin-unit-jest vue-template-compiler @vue/test-utils
 mkdir -p tests/unit

jest用の設定ファイルを作成します。

jest.config.js
 module.exports = {
   preset: '@vue/cli-plugin-unit-jest'
 }

tests/unitフォルダにテストコードを追加します。

tests/unit/example.spec.js
 import { shallowMount } from '@vue/test-utils'
 import HelloWorld from '@/components/HelloWorld.vue'

 describe('HelloWorld.vue', () => {
   it('renders props.msg when passed', () => {
     const msg = 'new message'
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     })
     expect(wrapper.text()).toMatch(msg)
   })
 })

cli-plugin-unit-jestプラグインをインストールした後は、vue-cli-serviceコマンドを使用することで単体テストが行えるようになります。

テスト実行用コマンド
npx vue-cli-service test:unit

スナップショットテスト

jestはスナップショットテストをサポートしており、レンダリングしたHTMLが前回のテスト時と変更されたかをチェックすることができます。

https://jestjs.io/docs/ja/snapshot-testing#inline-snapshots
https://system.blog.uuum.jp/entry/2017/12/12/110000

スナップショットテストをjest+vueで行うにはvue-server-rendererをインストールして、node.jsでVueをレンダリングできるようにします。

インストール用のコマンド
npm install --save-dev vue-server-renderer

テストコード

test/unit/example.spec.js
 import { shallowMount } from '@vue/test-utils'
 import HelloWorld from '@/components/HelloWorld.vue'
 import { createRenderer } from 'vue-server-renderer'

 describe('HelloWorld.vue', () => {
   it('renders props.msg when passed', () => {
     const msg = 'new message'
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     })
     expect(wrapper.text()).toMatch(msg)
   })
   it('snap shot test', () => {
     const msg = 'new message'
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     })

     // コンポーネントをHTML文字列にレンダリング
     // https://system.blog.uuum.jp/entry/2017/12/12/110000
     const renderer = createRenderer()
     renderer.renderToString(wrapper.vm, (err, str) => {
       if (err) throw new Error(err)
       // 最新のスナップショットと一致するか比較
       expect(str).toMatchSnapshot()
     })    
   })
 })

このテストコードを実行すると、tests/unitに__snapshots__フォルダが作成されます。
何も変更せずにテストを際実行するとテストは合格します。
そこで、テストコードの以下を修正します。

// 略
const msg = 'new message!!'
// 略

修正後にテストを際実行すると以下のようなエラーが発生します。

image.png

もし、このエラーが予定通りの場合はスナップショットを更新してテストを合格にする必要があります。そのためには以下のコマンドを実行します。

スナップショットの更新のコマンド
npx vue-cli-service test:unit -u

このコマンドを実行後、スナップショットは更新されて、テストが合格するようになります。
この例のようにtoMatchSnapshotを使用すると外部ファイルにスナップショットを保存しますが、toMatchInlineSnapshotを使用することでテストコード中に期待するHTMLを記載することができます。

以下はインラインスナップショットのテストコードのサンプルです。初回実行前には期待値は存在しません。

   it("inline snap shot test"", () => {
     const msg = "new message!!";
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     });

     // コンポーネントをHTML文字列にレンダリング
     // https://system.blog.uuum.jp/entry/2017/12/12/110000
     const renderer = createRenderer();
     renderer.renderToString(wrapper.vm, (err, str) => {
       if (err) throw new Error(err);
       expect(str).toMatchInlineSnapshot();
     });
   });

テストを実行するとテストコードは次のように変更されます。


   it("inline snap shot test", () => {
     const msg = "new message!!";
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     });

     // コンポーネントをHTML文字列にレンダリング
     // https://system.blog.uuum.jp/entry/2017/12/12/110000
     const renderer = createRenderer();
     renderer.renderToString(wrapper.vm, (err, str) => {
       if (err) throw new Error(err);
       // 最新のスナップショットと一致するか比較
       expect(str).toMatchInlineSnapshot(`
         <div class="hello">
           <h1>new message!!</h1>
           <p>
             For a guide and recipes on how to configure / customize this project,<br>
             check out the
             <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
           </p>
           <h3>Installed CLI Plugins</h3>
           <ul>
             <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
             <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
           </ul>
           <h3>Essential Links</h3>
           <ul>
             <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
             <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
             <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
             <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
             <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
           </ul>
           <h3>Ecosystem</h3>
           <ul>
             <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
             <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
             <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
             <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
             <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
           </ul>
         </div>
       `);
     });

トラブルシュート

requireのキャッシュをクリアしたい

jest使用中にrequireのキャッシュをクリアしたいケースがある。
require.cacheの内容を削除することで対応できるが、jest使用中は、ここにキャッシュが格納されない。
beforeEachにてresetModulesを実行することで、読み込んでいたrequireのキャッシュを消すことが可能。

beforeEach(() => {
  jest.resetModules()
})

依存しているファイルの一部だけモックしたい

/* eslint-disable import/first */
jest.mock('src/utility', () => ({
  ...(jest.requireActual('src/utility')), // 特定の関数以外は本物を使う
  sleep: jest.fn()
}))
import { sleep } from 'src/utility' // モックされた関数

import hoge from './src/hoge.js' // sleepに依存しているモジュールでもsleepはモックされた関数となる

https://stackoverflow.com/questions/39755439/how-to-mock-imported-named-function-in-jest-when-module-is-unmocked
https://jestjs.io/docs/bypassing-module-mocks

いつも動いてるテストが突然不安定になった

jestは規定の動きではキャッシュを保存しています。
このキャッシュを以下のコマンドでクリアします。

npx jest --clearCache 

Canvasを使用するテストをしたい

jestの仮想DOMはCanvasをサポートしていないので、getContextがnullになります。
getContextをモックアップする方法もありますが、面倒ならば
jest-canvas-mockを使用しましょう。

95
100
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
95
100