同じオブジェクトをテストによってスパイ、あるいはスタブで監視させる方法

同じオブジェクトをテストによってスパイ/スタブを切り替えて監視したい場合の書き方。

# NGパターン
スパイ、スタブは共に対象オブジェクトをラップするものである。スパイとスタブが同じテスト内で同じオブジェクトをラップするのは、スパイを二重にラップするのと等しいため、エラーとなる。

```typescript
import * as sinon from 'sinon';

class SinonResetMixed {
  public methodToBeObserved(): void {
    // do nothing
  }
}

describe('Wraps same object by both spy and stub and uses simultaneously', () => {
  let instance: SinonResetMixed;
  let spy: sinon.SinonSpy;
  let stub: sinon.SinonStub;
  beforeAll(() => {
    spy = sinon.spy(SinonResetMixed.prototype, 'methodToBeObserved');
    stub = sinon.stub(SinonResetMixed.prototype, 'methodToBeObserved');
  });
  afterEach(() => {
    sinon.restore();
  });

  it('cannot wrap same object by both spy and stub', () => {
    instance = new SinonResetMixed();
    instance.methodToBeObserved();
    console.log(`Spy says methodToBeObserved has called ${spy.callCount} time(s)`);
    console.log(`Stub says methodToBeObserved has called ${stub.callCount} time(s)`);
    expect(spy.calledOnce).toBe(true);
    expect(stub.calledOnce).toBe(true);
  });
});
```

```bash
$ yarn test
 FAIL  src/SinonResetMixedNG.test.tsx
  ● Wraps same object by both spy and stub and uses simultaneously › cannot wrap same object by both spy and stub

    TypeError: Attempted to wrap methodToBeObserved which is already wrapped

      at checkWrappedMethod (node_modules/sinon/lib/sinon/util/core/wrap-method.js:38:21)
      at wrapMethod (node_modules/sinon/lib/sinon/util/core/wrap-method.js:85:13)
      at stub (node_modules/sinon/lib/sinon/stub.js:71:44)
      at Sandbox.stub (node_modules/sinon/lib/sinon/sandbox.js:291:33)
      at Object.<anonymous> (src/SinonResetMixedNG.test.tsx:15:18)
          at new Promise (<anonymous>)
      at Promise.resolve.then.el (node_modules/p-map/index.js:46:16)
      at process._tickCallback (internal/process/next_tick.js:68:7)
      --------------
      Error: Stack Trace for original
      at wrapMethod (node_modules/sinon/lib/sinon/util/core/wrap-method.js:110:31)
      at Function.spy (node_modules/sinon/lib/sinon/spy.js:45:16)
      at Sandbox.spy (node_modules/sinon/lib/sinon/sandbox.js:275:26)
      at Object.<anonymous> (src/SinonResetMixedNG.test.tsx:14:17)
          at new Promise (<anonymous>)
      at Promise.resolve.then.el (node_modules/p-map/index.js:46:16)
      at process._tickCallback (internal/process/next_tick.js:68:7)

  ● Wraps same object by both spy and stub and uses simultaneously › cannot wrap same object by both spy and stub

    TypeError: Cannot read property 'callCount' of undefined

      at Object.<anonymous> (src/SinonResetMixedNG.test.tsx:25:65)
          at new Promise (<anonymous>)
      at Promise.resolve.then.el (node_modules/p-map/index.js:46:16)
      at process._tickCallback (internal/process/next_tick.js:68:7)

  Wraps same object by both spy and stub and uses simultaneously
    ✕ cannot wrap same object by both spy and stub (1ms)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.944s, estimated 1s
Ran all test suites matching /ng/.

  console.log src/SinonResetMixedNG.test.tsx:26
    Spy says methodToBeObserved has called 1 time(s)

```

# OKパターン

対象となるオブジェクトが同じでも、スパイとスタブが同じテスト内に共存しなければOK。

```typescript
import * as sinon from 'sinon';

class SinonResetMixed {
  public methodToBeObserved(): void {
    // do nothing
  }
}

describe('Wraps same object by both spy and stub but uses separately', () => {
  let instance: SinonResetMixed;
  let spy: sinon.SinonSpy;
  let stub: sinon.SinonStub;
  afterEach(() => {
    sinon.restore();
  });

  it('only uses spy', () => {
    spy = sinon.spy(SinonResetMixed.prototype, 'methodToBeObserved');
    instance = new SinonResetMixed();
    instance.methodToBeObserved();
    console.log(`Spy says methodToBeObserved has called ${spy.callCount} time(s)`);
    expect(spy.calledOnce).toBe(true);
  });

  it('only uses stub', () => {
    stub = sinon.stub(SinonResetMixed.prototype, 'methodToBeObserved');
    instance = new SinonResetMixed();
    instance.methodToBeObserved();
    console.log(`Stub says methodToBeObserved has called ${stub.callCount} time(s)`);
    expect(stub.calledOnce).toBe(true);
  });
});
```

```bash
 PASS  src/SinonResetMixedOK.test.tsx
  Wraps same object by both spy and stub but uses separately
    ✓ only uses spy (2ms)
    ✓ only uses stub (5ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.031s
Ran all test suites matching /ok/.

  console.log src/SinonResetMixedOK.test.tsx:23
    Spy says methodToBeObserved has called 1 time(s)

  console.log src/SinonResetMixedOK.test.tsx:30
    Stub says methodToBeObserved has called 1 time(s)

```