LoginSignup
0
1

More than 3 years have passed since last update.

Sinon.JSでテストを書く

Last updated at Posted at 2019-07-15

概要

テストコードを書くのに色々と苦労しました。
中でも苦労したのは、『クラスのコンストラクタ』と『メソッドのアクセス修飾子』です。
これらの書き方をまとめていきます。

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, []);
  });
});
0
1
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
0
1