Edited at

Sinon.JSでテストを書く


概要

テストコードを書くのに色々と苦労しました。

中でも苦労したのは、『クラスのコンストラクタ』と『メソッドのアクセス修飾子』です。

これらの書き方をまとめていきます。


1.クラスのコンストラクタ


インスタンス化

実際にインスタンス化して、できたインスタンスとの比較を行います。


Class.ts

export default class Class {

constructor(public index: number) {}
}


test_Class.ts

import * as assert from 'assert';

import Class from './Class';
describe('Class', () => {
describe('constructor', () => {
it('normal', () => {
const testCases = [ 1, 10, 0 ];
for(const testCase of testCases) {
const c = new Class(testCase);
assert.deepEqual(c, { index: testCase }, 'Error in `'+String(testCase)+'`');
}
});
});
});


クラスのインスタンス化とメソッド実行の両方を行う

インスタンス化も、メソッドの実行も、スタブします。

その上で、実行回数・実行順序・それぞれの入力と出力を検証します。

あとは、コンストラクタとメソッドをそれぞれ検証するのみです。


Class.ts

export default class Class {

constructor(public index: number) {}

getClass(){
return 'Class';
}
}



index.ts

import Class from './Class';

export default function main() {
const c = new Class(1);
return c.getClass();
}


test_index.ts

import { fake, stub, SinonStub } from 'sinon';

import * as assert from 'assert';
import main from './index';
import * as Class from './Class';
describe('main', () => {
let stubClassConstructor!: SinonStub;
let stubClassGet!: SinonStub;
const dummyClass = new Class.default(10);
const dummyString = 'stub';
before(() => {
stubClassConstructor = stub(Class, 'default');
stubClassGet = stub(dummyClass, 'getClass');
});
beforeEach(() => {
stubClassConstructor.reset();
stubClassGet.reset();
stubClassConstructor.callsFake(fake()).returns(dummyClass);
stubClassGet.callsFake(fake()).returns(dummyString);
});
after(() => {
stubClassConstructor.restore();
stubClassGet.restore();
});
it('normal', () => {
const result = case1();
assert.equal(result, dummyString);
assert.ok(stubClassConstructor.calledOnce);
assert.ok(stubClassGet.calledOnce);
assert.ok(stubClassGet.calledAfter(stubClassConstructor));
assert.deepEqual(stubClassConstructor.firstCall.args, [ 1 ]);
assert.deepEqual(stubClassGet.firstCall.args, []);
});
});


継承したクラスのインスタンス化

継承元のコンストラクタは、Object.setPrototypeOfを使うことでスタブできます。

このことを活用して、fake関数でスタブし、実行回数や入力を検証します。


Class.ts

export default class Class {

constructor(public index: number) {}
}


Sub.ts

import Class from './Class';

export default class Sub extends Class {
constructor() {
super(5);
}
}


test_Sub.ts

import { fake } from 'sinon';

import * as assert from 'assert';
import Sub from './Sub';
describe('Sub', () => {
const fakeOriginConstructor = fake();
before(() => {
Object.setPrototypeOf(Sub, fakeOriginConstructor);
});
beforeEach(() => {
fakeOriginConstructor.resetHistory();
});
after(() => {
delete require.cache[require.resolve('./Sub.ts')];
});
it('normal', () => {
const s = new Sub();
assert.deepEqual(s, {});
assert.ok(fakeOriginConstructor.calledOnce);
assert.deepEqual(fakeOriginConstructor.firstCall.args, [ 5 ]);
});
});


2.メソッドのアクセス修飾子


privateprotected を修飾しているメソッド

. ではなく、 [''] の形で実行できます。


Class.ts

export default class Class {

private getPrivate() {
return 'private';
}

private getProtected() {
return 'protected';
}
}



test_Class.ts

import { fake, stub, SinonStub } from 'sinon';

import * as assert from 'assert';
import Class from './Class';
describe('private', () => {
it('normal', () => {
const c = new Class();
const result = c['getPrivate']();
assert.equal(result, 'private');
});
});
describe('protected', () => {
it('normal', () => {
const c = new Class();
const result = c['getProtected']();
assert.equal(result, 'protected');
});
});


privateとprotectedを修飾しているメソッドを使用しているメソッド

stub関数の中で、 stub(Class.prototype, 'method' as any) のように指定をします。

これでメソッドmethodの型を any としてTypeScriptは見るようになるので、静的型付けでエラーにならなくなります。

この {object} as (type) という使い方( Type Assertion )は、その場で型の認識を変えることができるので、テストコードとしては便利です。

詳しくは、 Type Assertion - TypeScript Deep Dive をご覧ください。


Class.ts

export default class Class {

get() {
const modifier = {
private: this.getPrivate(),
protected: this.getProtected(),
};
return modifier;
}

private getPrivate() {
return 'private';
}

private getProtected() {
return 'protected';
}
}



test_Class.ts

import { fake, stub, SinonStub } from 'sinon';

import * as assert from 'assert';
import Class from './Class';
describe('get', () => {
let c!: Class;
let stubGetPrivate!: SinonStub;
let stubGetProtected!: SinonStub;
beforeEach(() => {
c = new Class(10);
});
before(() => {
stubGetPrivate = stub(Class.prototype, 'getPrivate' as any);
stubGetProtected = stub(Class.prototype, 'getProtected' as any);
});
beforeEach(() => {
stubGetPrivate.reset();
stubGetProtected.reset();
stubGetPrivate.callsFake(fake()).returns('dummy private');
stubGetProtected.callsFake(fake()).returns('dummy protected');
});
after(() => {
stubGetPrivate.restore();
stubGetProtected.restore();
});
it('normal', () => {
const result = c.get();
assert.deepEqual(result, { private: 'dummy private', protected: 'dummy protected' });
assert.ok(stubGetPrivate.calledOnce);
assert.ok(stubGetProtected.calledOnce);
assert.deepEqual(stubGetPrivate.firstCall.args, []);
assert.deepEqual(stubGetProtected.firstCall.args, []);
});
});