11
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaScript のテストを書いてみる

Posted at

ちょっとした仕事で、ちゃんとした、 JavaScript のテストコードを書く必要性が生じた。動くテストはかけるが、あまり慣れていない言語で「適切な」テストコードがかけているかは正直わからない。まとまったラーニングコースを受けてみるつもりだが、2ビジネス営業日かかるみたいなので、まず最初に作ってみた。

テストフレームワークをどれを使うかはあたりがついている。Mochachai。これは、TypeScriptの頃から同じなので、馴染みがある。そもそもBDD形式のフレームワークなので、使い方はどれも似ている。

Mocha は BDDのテストフレームワークで、chai は, expect などのアサーション部分をBDDフレームワークの形式で書けるようになっている。インストールは次の通り

npm init -y
npm install mocha chai --save-dev

ざっくりと仕様を確認する。

Mocha

下記のサンプルをみるとそれだけで雰囲気がわかるだろう。他のBDDフレームワークと違いはない。node なのでコールバックの形式になっている。

var assert = require('assert');
describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal(-1, [1,2,3].indexOf(4));
    });
  });
});

メソッドは、Promise でも書けるし

const assert = require('assert');

it('should complete this test', function (done) {
  return new Promise(function (resolve) {
    assert.ok(true);
    resolve();
  })
    .then(done);
});

async/await でも書ける様子。


beforeEach(async function() {
  await db.clear();
  await db.save([tobi, loki, jane]);
});

describe('#find()', function() {
  it('responds with matching records', async function() {
    const users = await db.find({ type: 'User' });
    users.should.have.length(3);
  });
});

残りは、おなじみの Setup/Teardown だがこんな感じ。

describe ("hooks", function() {
    before(function(){
        console.log("before called");
    });
    after(function(){
        console.log("after called");
    });
    beforeEach(function() {
        console.log("beforeEach called");
    });
    afterEach(function(){
        console.log("afterEach called");
    })
    it ("should be true", (done) => {
        console.log("test 01 called");
        done();
    });
    it ("should be false", (done) => {
        console.log("test 02 called");
        done();
    })

})

実行結果

$ npm test

> mock@1.0.0 test /Users/ushio/Codes/node/mock
> mocha **.spec.js

  hooks
before called
beforeEach called
test 01 called
    ✓ should be true
afterEach called
beforeEach called
test 02 called
    ✓ should be false
afterEach called
after called


  2 passing (6ms)

簡単ですな。

Chai

アサーションの部分。Expect で当然書きたいよね。だから、これで。文法は見ての通り。

var expect = require('chai').expect
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
expect(beverages).to.have.property('tea').with.lengthOf(3);

Sinon

コードを書いていて、今回はスタブを書く必要があった。ちなみに、モックとスタブの違いは次の記事が良かった。

スタブは受信メッセージのために使い、モックは送信メッセージのために使います。メソッドそのものをテストしたくて、偽のオブジェクトを渡したい時は、スタブ、メソッドの中で使われているオブジェクトに渡ってくる値、呼ばれた回数などを検査したい時はモックという名前で呼ばれます。どちらも偽オブジェクトですが用途が違います。これに加えて、Spy という機能も持ったフレームワークが

です。シノーンはギリシャ神話に出てくる人で、トロイの木馬のそばにいて、なんのためにその木馬が作られたなどについて正しいことを言わなかった。それによって、トロイの木馬が城内に持ち込まれた。というエピソードで出てくる人みたい。

Sinon.js では、Spy, Stub, Mock が使える。

によると

名称 意味
Stubbing スタブしたファンクションは、呼ばれず、代わりにあなたが提供した実装が呼ばれる。もし、あなたが用意してなかった、空の関数が呼ばれる
Spying スパイしたファンクションは、元の実装が呼ばれる。ただ、あたはそれのアサーションを実施できる 
Mocking スタブと同じだが、ファンクションだけではなく、オブジェクトに使える 

と定義がいささか違う。今回は、ある関数をテストするのに、渡ってくる引数を偽物にして、関数をテストしたかったので、stub が良さげだが、結局Spyにしている。

月から実際のコードをみていこう。

テスト対象コード

module.exports = function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');

    if (req.query.name || (req.body && req.body.name)) {
        context.res = {
            // status: 200, /* Defaults to 200 */
            body: "Hello " + (req.query.name || req.body.name)
        };
        context.bindings.outputQueueItem = req.query.name;
    }
    else {
        context.res = {
            status: 400,
            body: "Please pass a name on the query string or in the request body"
        };
    }
    context.done();
};

このパラメータのオブジェクトを騙したい。テストを書いてみた。

npm install sinon --save-dev
'use strict'

const Scheduler = require('../Scheduler/index.js')
const expect = require('chai').expect
const sinon = require('sinon');

describe('Scheduler function', () => {
    var req = {};
    var context = {};

    beforeEach(function() {  // 1. Stub 
        req = {
            query: {
                name: "Azure"
            },
            body: {
                name: "taro"
            }
        };
        context = {
            res: {
                status: 200
            },
            log: function (str) {
                console.log(str);
            },
            bindings: {
                outputQueueItem: "fake_queue_value"
            },
            done: function () {

            }
        };
    });

    it('should pass query name to queue bindings', (done) => {
        sinon.spy(context, "done"); // 2. Spy
        Scheduler(context, req);
        expect(context.bindings.outputQueueItem).to.equal("Azure", "Queue isn't set");
        expect(context.done.called).to.be.true
        context.done.restore();
        done();
    }) 
    it ('should be ok if the query name has been passed', (done) => {
        Scheduler(context, req);
        expect(context.res.body).to.equal("Hello Azure");
        done();
    })
    it ('should not be ok if the query name isnt supplied', (done) => {
        req = {
            query: {
            },
            body: {
            }
        };
        Scheduler(context, req);
        expect(context.res.status).to.equal(400);
        expect(context.res.body).to.equal("Please pass a name on the query string or in the request body");
        done();
    })
})

確かにこれでテストができる。期待通りに動作する。

$ npm test

> tests@1.0.0 test /Users/ushio/Codes/AzureFunctions/nodeci/node/Tests
> mocha *.spec.js



  Scheduler function
JavaScript HTTP trigger function processed a request.
    ✓ should pass query name to queue bindings
JavaScript HTTP trigger function processed a request.
    ✓ should be ok if the query name has been passed
JavaScript HTTP trigger function processed a request.
    ✓ should not be ok if the query name isnt supplied


  3 passing (9ms)
{
  "name": "tests",
  "version": "1.0.0",
  "description": "UnitTest project",
  "main": "index.js",
  "scripts": {
    "test": "mocha *.spec.js",
    "report": "mocha *.spec.js --reporter mocha-junit-reporter"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "chai": "^4.1.2",
    "mocha": "^4.0.1",
    "mocha-junit-reporter": "^1.15.0",
    "sinon": "^4.1.2"
  }
}

ポイントは、1. Stub で偽オブジェクトを作っている。2. Spy で done() メソッドをスパイして、実際に呼ばれたかを context.done.called メソッドで確認している。このスパイのオブジェクトは、context.done.restore() で元のオブジェクトに戻される。

stub を使おうと思ったが、結局元のオブジェクトが無いといけなかったり、オブジェクトの値を変更できない感じだったので、今のコードになっている。もっと良いベストプラクティスは無いのだろうか。 次回はもっと適切な書き方を探ってみたい。

こうしたらもっといいよー。というのがあれば是非コメントをお願いいたします。

11
14
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
11
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?