概要
テストコードを書くのに色々と苦労しました。
中でも苦労したのは、『クラスのコンストラクタ』と『メソッドのアクセス修飾子』です。
これらの書き方をまとめていきます。
1.クラスのコンストラクタ
インスタンス化
実際にインスタンス化して、できたインスタンスとの比較を行います。
export default class Class {
constructor(public index: number) {}
}
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)+'`');
}
});
});
});
クラスのインスタンス化とメソッド実行の両方を行う
インスタンス化も、メソッドの実行も、スタブします。
その上で、実行回数・実行順序・それぞれの入力と出力を検証します。
あとは、コンストラクタとメソッドをそれぞれ検証するのみです。
export default class Class {
constructor(public index: number) {}
getClass(){
return 'Class';
}
}
import Class from './Class';
export default function main() {
const c = new Class(1);
return c.getClass();
}
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関数でスタブし、実行回数や入力を検証します。
export default class Class {
constructor(public index: number) {}
}
import Class from './Class';
export default class Sub extends Class {
constructor() {
super(5);
}
}
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.メソッドのアクセス修飾子
private
や protected
を修飾しているメソッド
.
ではなく、 ['']
の形で実行できます。
export default class Class {
private getPrivate() {
return 'private';
}
private getProtected() {
return 'protected';
}
}
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 をご覧ください。
export default class Class {
get() {
const modifier = {
private: this.getPrivate(),
protected: this.getProtected(),
};
return modifier;
}
private getPrivate() {
return 'private';
}
private getProtected() {
return 'protected';
}
}
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, []);
});
});