Search code examples
typescriptunit-testingmockingsinonspy

using sinon.spy when two methods have the same name but different arguments


I am trying to use sinon.spy() on an object with two methods called draw (both with same name but different arguments) as follows:

const spy = sinon.spy(obj,'draw')

When I try to test the spy with expect(spy.calledOnceWith(expectedArgument)).toBeTruthy(), where expectedArgument matches the type given by one of the function definitions of draw, I get an error saying that the argument is not assignable to the type specified by the other function declaration of draw. In situations like this, is there a way to specify which function header/declaration I'm referring to when I create the spy, so the argument will match?


Solution

  • YES, you need to do double assertions to the sinon spy in order to avoid typescript lint error. This lint error because of sinon spy automatically apply the last definition known. You need to provide the correct assertion that you will use in the test.

    For example: I have class Add in file add.ts. If both input are numbers, then process method will try to add them. If both input are strings, then process method will try to concatenate the strings. Other will produce TypeError.

    class Add {
      process(a: number, b: number): number;
      process(a: string, b: string): string;
      process(a: any, b: any):number|string|Error {
        if (typeof a === 'number' && typeof b === 'number') {
          return a + b;
        } else if (typeof a === 'string' && typeof b === 'string') {
          return `${a}${b}`;
        }
        throw new TypeError('Incorrect type to process');
      }
    }
    
    export default Add;
    

    I have test file: add.spec.ts that will test all conditions.

    import sinon from 'sinon';
    import { expect } from 'chai';
    import Add from '../src/add';
    
    describe('Add', () => {
      afterEach(() => {
        sinon.restore();
      });
      it('should process number', () => {
        const obj = new Add();
        const a = 1;
        const b = 2;
        // Sinon spy automatically try to apply the last definition known, which is:
        // sinon.SinonSpy<[a: string, b: string], string>
        // So we need to do type assertions to:
        // sinon.SinonSpy<[a: number, b: number], number>
        const spy = sinon.spy(obj, 'process') as unknown as sinon.SinonSpy<[a: number, b: number], number>;
        const result = obj.process(a, b);
        expect(result).to.equal(a + b);
        // This line: linter will not complain.
        expect(spy.calledOnceWith(a, b)).to.equal(true);
      });
    
      it('should process string', () => {
        const obj = new Add();
        const a = '1';
        const b = '2';
        // Sinon spy has automatically apply the correct definition.
        const spy = sinon.spy(obj, 'process'); // as unknown as sinon.SinonSpy<[a: string, b: string], string>;
        const result = obj.process(a, b);
        expect(result).to.equal(`${a}${b}`);
        // This line: linter will not complain.
        expect(spy.calledOnceWith(a, b)).to.equal(true);
      });
    
      it('should throw error for invalid type', () => {
        const obj = new Add();
        const a = 1;
        const b = '2';
        // Sinon spy automatically try to apply the last definition known, which is:
        // sinon.SinonSpy<[a: string, b: string], string>
        // So we need to do type assertions to:
        // sinon.SinonSpy<[a: any, b: any], Error>
        const spy = sinon.spy(obj, 'process') as unknown as sinon.SinonSpy<[a: any, b: any], Error>;
        // We also need to cast obj as any, or there will be lint error:
        // The call would have succeeded against this implementation,
        // but implementation signatures of overloads are not externally visible.
        expect(() => (obj as any).process(a, b)).to.throw(TypeError);
        // This line: linter will not complain.
        expect(spy.calledOnceWith(a, b)).to.equal(true);
      });
    });
    

    When I call the runner using ts-mocha at terminal and use nyc for coverage, I get: (no protest from my editor also).

    $ npx nyc ts-mocha test/add.spec.ts 
    
    
      Add
        ✓ should process number
        ✓ should process string
        ✓ should throw error for invalid type
    
    
      3 passing (5ms)
    
    ----------|---------|----------|---------|---------|-------------------
    File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    ----------|---------|----------|---------|---------|-------------------
    All files |     100 |      100 |     100 |     100 |                   
     add.ts   |     100 |      100 |     100 |     100 |                   
    ----------|---------|----------|---------|---------|-------------------
    

    Reference: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions