39

If I use sinon with typescript then how do I cast the sinon mock to an instance of my object?

For instance a SinonMock would be returned but my controller under test may require a specific service passed in to its constructor.

var myServiceMock: MyStuff.MyService = <MyStuff.MyService (sinon.mock(MyStuff.MyService));

controllerUnderTest = new MyStuff.MyController(myServiceMock, $log);

Can sinon be used with Typescript?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Brandon
  • 984
  • 1
  • 11
  • 26

4 Answers4

31

Sinon can create a stub based on a constructor quite easily if, instead of mock, you use the createStubInstance method.

An example using mocha, chai, sinon and sinon-chai, could look like this:

import * as sinon from 'sinon';
import * as chai from 'chai';

// ... imports for the classes under test

const expect    = chai.expect;
const sinonChai = require("sinon-chai");

chai.use(sinonChai);

describe('MyController', () => {
    it('uses MyService', () => {

        let myService  = sinon.createStubInstance(MyStuff.MyService),
            controller = new MyStuff.MyController(myService as any, ...);

        // ... perform an action on the controller 
        // that calls myService.aMethodWeAreInterestedIn

        // verify if the method you're interested in has been called if you want to
        expect(myService.aMethodWeAreInterestedIn).to.have.been.called;
    });
});

I've published an article, which you might find useful if you'd like to learn more about the different test doubles and how to use them with Sinon.js.

Hope this helps!

Jan

Jan Molak
  • 4,426
  • 2
  • 36
  • 32
  • 4
    You can also use `import * as sinonChai from 'sinon-chai';` assuming you have the types installed: `npm i --save-dev @types/sinon-chai`. – Westy92 Dec 20 '16 at 17:59
  • This doesn't work for me. I get an error "Type 'SinonStubbedInstance' is not assignable to type type 'Foo'. Property 'bar' is missing in type 'SinonStubbedInstance'." because `createStubInstance` returns a `SinonStubbedInstance` but what I need to pass to my test function is not a `SinonStubbedInstance` but an actual `Foo`. – Frans Jul 01 '21 at 18:03
  • This also doesn't seem to work for types with explicit constructors that take arguments. I get `TypeError: The constructor should be a function.`. The article unfortunately also takes this "happy path" approach as though classes with multi-arg constructors simply didn't exist! – Frans Jul 03 '21 at 13:18
  • @Frans - When passing a stubbed instance to a consumer use `stubbedFoo as unknown as Foo` - see for example https://github.com/serenity-js/serenity-js/blob/master/packages/core/spec/stage/Stage.spec.ts#L58 – Jan Molak Jul 03 '21 at 21:59
  • Thanks @JanMolak. Turned out my `The constructor should be a function` error had a different cause from what I had assumed: I was importing the constructor but it was pulling through as `undefined` for some reason, and obviously `undefined` is not a function. – Frans Jul 05 '21 at 06:33
  • Cool, I'm glad you got to the bottom of it! – Jan Molak Jul 05 '21 at 21:40
19

You may need to use an <any> type assertion to make the type wide before you narrow it to your specific type:

var myServiceMock: MyStuff.MyService = 
    <MyStuff.MyService> <any> (sinon.mock(MyStuff.MyService));

Just to clarify one behaviour of sinon - although you pass in MyStuff.MyService, whatever you pass to the mock method is only used to provide better error messages.

If you want the mock to have methods and properties, you need to add them.

If you want automatically created fakes, you can grab the FakeFactory from tsUnit, which creates a fake version with some default values that you can opt to override - in JavaScript this is pretty easy stuff (plus by not using too much mock functionality, you can ensure you are testing behaviour rather than implementation).

Example use of FakeFactory:

var target = tsUnit.FakeFactory.getFake<RealClass>(RealClass);
var result = target.run();
this.areIdentical(undefined, result);
Fenton
  • 241,084
  • 71
  • 387
  • 401
  • 1
    Yeah, I did something similar to this. I also learned I was using the sinon object incorrectly, I didn't realize all I needed to do is use the original MyService instance in calls and it uses the sinon mocks/stubs. What you have put here seems correct for what I was asking in any case, thanks! – Brandon Jan 29 '15 at 14:22
  • I was back to this and it seems like it doesn't work. myServiceMock doesn't appear like it has any methods. Any ideas? – Brandon Feb 19 '15 at 20:54
  • Have you given it an expectation? `myServiceMock.expects("doSomething").returns(42);` – Fenton Feb 20 '15 at 19:06
  • I tried, but the issue seem to be that sinon can only mock and instance of the object and not the type based on the constructor. I guess that makes sense really, since mocking a javascript type would be difficult. But, that leaves me still have to construct an instance of this class ( with mocks passed into the ctor ), and then mocking the instance. I can't think of anything to do, but just create the mocks by hand. – Brandon Feb 27 '15 at 18:07
7

Use ts-sinon.

It lets you:

  • stub objects
  • stub interfaces
Matt Wynne
  • 143
  • 1
  • 8
5

In Typescript this can be achieved by using sinon.createStubInstance and SinonStubbedInstance class.

Example:

let documentRepository: SinonStubbedInstance<DocumentRepository>;

documentRepository = sinon.createStubInstance(DocumentRepository);

Now you have a full intellisense for working with all stubbed methods of this class.

Example Arrange:

documentRepository.delete.resolves({deletedCount: 1});

documentRepository.update.throws(error);

Example Assert:

sinon.assert.calledOnce(documentRepository.update);

There is only one place where you would need to perform type casting and that is the initiallization of the class you want to unit test.

Example:

documentsController =
  new DocumentsController(
    userContext,
    documentRepository as unknown as DocumentRepository);

Hope this will help. More on this article.

Milos Josic
  • 51
  • 1
  • 1
  • I get a tslint error "Cannot find name 'unknown'.". – Frans Jul 01 '21 at 18:00
  • `unknown` was introduced in TypeScript 3. Using a newer typescript compiler fixed this for me (in IntelliJ: **Settings** > **Languages & Frameworks** > **Typescript**. – Frans Jul 05 '21 at 06:34