diff --git a/README.md b/README.md index 3fbeb2a..c17e6f1 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Sinon extension providing functions to: - stub all object methods - stub interface +- stub object constructor ## Prerequisites @@ -11,7 +12,9 @@ Sinon extension providing functions to: ## Installation -`npm install ts-sinon` +`npm install --save-dev ts-sinon` +or +`yarn add --dev ts-sinon` ## Object stubs example @@ -59,7 +62,7 @@ expect(testStub.methodA()).to.be.undefined; expect(testStub.methodB()).to.equal('B: original'); ``` -Stub with predefined return values: +Stub with predefined return values (deprecated - see the deprecation note): ```javascript class Test { @@ -87,27 +90,60 @@ import * as sinon from "ts-sinon"; const stubInterface = sinon.stubInterface; ``` -Interface stub with predefined return values (recommended): +Interface stub (stub all methods): ```javascript interface Test { method(): string; } -const testStub = stubInterface({ method: 'stubbed' }); +const testStub = stubInterface(); + +expect(testStub.method()).to.be.undefined; + +testStub.method.returns('stubbed'); expect(testStub.method()).to.equal('stubbed'); ``` -Interface stub (not recommended due to interface stub method return types incompatibility - if the return value of stubInterface method is not cast to "any" type and returned type of interface method is not compatible with "object" we'll get a compiler error). +Interface stub with predefined return values (deprecated - see the deprecation note): ```javascript interface Test { method(): string; } -// if we have "Test" type instead of "any" code does not compile -const testStub: any = stubInterface(); +const testStub = stubInterface({ method: 'stubbed' }); + +expect(testStub.method()).to.equal('stubbed'); +``` + +## Object constructor stub example + +Importing stubConstructor function: + +- import single function: +```javascript +import { stubConstructor } from "ts-sinon"; +``` + +- import as part of sinon singleton: +```javascript +import * as sinon from "ts-sinon"; + +const stubConstructor = sinon.stubConstructor; +``` + +Object constructor stub (stub all methods): + +```javascript +class Test { + method(): string { + return 'value'; + } +} + +const testStub = stubConstructor(Test); expect(testStub.method()).to.be.undefined; @@ -116,6 +152,26 @@ testStub.method.returns('stubbed'); expect(testStub.method()).to.equal('stubbed'); ``` +## Method map argument deprecation note + +Due to a potential risk of overwriting return value type of stubbed method (that won't be caught by TypeScript compiler) I have decided to mark it as deprecated. +Please look at the following example of type overwriting using method map: + +```javascript +interface ITest { + method1(): void; + method2(num: number): string; +} + +const interfaceStub: ITest = stubInterface({ + method2: 12345 +}); + +// following expression will assign 12345 value of type number to v variable which is incorrect (it will compile without an error) +const v: string = interfaceStub.method2(1); + +``` + ## Sinon methods By importing 'ts-sinon' you have access to all sinon methods. diff --git a/package.json b/package.json index 7eab0ae..8fc1c5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-sinon", - "version": "1.0.20", + "version": "1.0.21", "description": "sinon library extension to stub whole object and interfaces", "author": { "name": "Tomasz Tarnowski", diff --git a/src/index.spec.ts b/src/index.spec.ts index 480a8fe..97e2ba4 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,58 +1,98 @@ import * as chai from "chai"; import * as sinonChai from "sinon-chai"; -import { stubObject, stubInterface } from "./index"; -import { SinonStubbedInstance } from "sinon"; +import { stubObject, stubInterface, stubConstructor } from "./index"; chai.use(sinonChai); const expect = chai.expect; describe('ts-sinon', () => { describe('stubObject', () => { - it('returns stub es6 object with all methods stubbed when no methods or method map given', () => { - const object = new class { - test() { - return 123; - } - - run() { - return 'run'; + describe('when no methods or method map given', () => { + it('returns stub es6 object with all methods stubbed', () => { + class A { + test() { + return 123; + } + + run() { + return 'run'; + } } - } - - const objectStub = stubObject(object); - expect(object.test()).to.equal(123); - expect(object.run()).to.equal('run'); + const object = new A(); + + const objectStub = stubObject(object); + + expect(object.test()).to.equal(123); + expect(object.run()).to.equal('run'); + + expect(objectStub.test()).to.be.undefined; + expect(objectStub.run()).to.be.undefined; + + expect(objectStub.run).to.have.been.called; + expect(objectStub.test).to.have.been.called; + }); + + it('returns stub literal object with all methods stubbed', () => { + const object = { + test: () => { + return 123; + }, + run: () => { + return 'run'; + } + }; + + const objectStub = stubObject(object); + + expect(object.test()).to.equal(123); + expect(object.run()).to.equal('run'); + + expect(objectStub.test()).to.be.undefined; + expect(objectStub.run()).to.be.undefined; + + expect(objectStub.run).to.have.been.called; + expect(objectStub.test).to.have.been.called; + }); - expect(objectStub.test()).to.be.undefined; - expect(objectStub.run()).to.be.undefined; + it('allows to change stub values', () => { + const object1 = new class { + methodA() { + return 'A'; + } - expect(objectStub.run).to.have.been.called; - expect(objectStub.test).to.have.been.called; - }); + methodB() { + return 'B'; + } + } - it('returns stub literal object with all methods stubbed when no methods or method map given', () => { - const object = { - test: () => { - return 123; - }, - run: () => { - return 'run'; + const object2 = { + methodC: () => { + return 'C'; + }, + methodD: () => { + return 'D'; + } } - }; - const objectStub = stubObject(object); + const object1Stub = stubObject(object1); + const object2Stub = stubObject(object2); - expect(object.test()).to.equal(123); - expect(object.run()).to.equal('run'); + object1Stub.methodA.returns('new A'); + object1Stub.methodB.returns('new B'); - expect(objectStub.test()).to.be.undefined; - expect(objectStub.run()).to.be.undefined; + expect(object1Stub.methodA()).to.equal('new A'); + expect(object1Stub.methodB()).to.equal('new B'); - expect(objectStub.run).to.have.been.called; - expect(objectStub.test).to.have.been.called; + object2Stub.methodC.returns('1'); + object2Stub.methodD.returns('2'); + + expect(object2Stub.methodC()).to.equal('1'); + expect(object2Stub.methodD()).to.equal('2'); + }); }); + it('returns partial stub object with only "test" method stubbed when array with "test" has been given', () => { const object = new class { @@ -94,6 +134,9 @@ describe('ts-sinon', () => { expect(objectStub.run()).to.equal(1); expect(objectStub.test()).to.equal(123); + objectStub.run.returns('new run'); + expect(objectStub.run()).to.equal('new run'); + expect(objectStub.run).to.have.been.called; }); }); @@ -103,6 +146,7 @@ describe('ts-sinon', () => { method2(num: number): string; } + /** @deprecated @see stubInterface @docs */ it('returns stub object created from interface with all methods stubbed with "method2" predefined to return value of "abc" and "method1" which is testable with expect that has been called', () => { const expectedMethod2Arg: number = 2; const expectedMethod2ReturnValue = 'abc'; @@ -127,6 +171,7 @@ describe('ts-sinon', () => { expect(interfaceStub.method2).to.have.been.calledWith(expectedMethod2Arg); }); + /** @deprecated @see stubInterface @docs */ it('returns stub object created from interface with all methods stubbed including "method2" predefined to return "x" when method map to value { method: x } has been given', () => { const interfaceStub: ITest = stubInterface({ method2: 'test' @@ -146,26 +191,50 @@ describe('ts-sinon', () => { expect(object.run(123)).to.equal('test'); }); - it('gives an access to method stubs of the stub object created from interface when the type of the interface type is cast to "any" and the ability to stub and test interface methods', () => { + it('gives an access to method stubs of the stub object created from interface', () => { const expectedMethod2Arg = 2; + const expectedMethod2Value = 'string'; - const interfaceStub: any = stubInterface(); - interfaceStub.method2.returns('string'); + const interfaceStub = stubInterface(); - const object = new class { - test: ITest; - constructor(test: ITest) { - this.test = test; - this.test.method1(); + expect(interfaceStub.method2(1)).to.be.undefined; + interfaceStub.method2.returns(expectedMethod2Value); + + const actualMethod2Value = interfaceStub.method2(expectedMethod2Arg); + interfaceStub.method1(); + + expect(interfaceStub.method2).to.have.been.calledWith(expectedMethod2Arg); + expect(actualMethod2Value).to.equal(expectedMethod2Value); + expect(interfaceStub.method1).to.have.been.called; + }); + }); + + describe('stubConstructor', () => { + it('stubs all object constructor methods', () => { + class A { + method1(): string { + return 'value1'; } - run(num: number): string { - return this.test.method2(num); + method2(x: number): number { + return 13; } - }(interfaceStub); + } + const expectedNewMethod1Value = 'new value'; + const expectedNewMethod2Value = 43; + const expectedMethod2Argument = 111; - expect(object.run(expectedMethod2Arg)).to.equal('string'); - expect(interfaceStub.method1).to.have.been.called; - expect(interfaceStub.method2).to.have.been.calledWith(expectedMethod2Arg); + const stub = stubConstructor(A); + + expect(stub.method1()).to.be.undefined; + expect(stub.method2(expectedMethod2Argument)).to.be.undefined; + + stub.method1.returns(expectedNewMethod1Value); + stub.method2.returns(expectedNewMethod2Value); + expect(stub.method2).to.have.been.calledWith(expectedMethod2Argument); + + expect(stub.method1()).to.equal(expectedNewMethod1Value); + expect(stub.method2(222)).to.equal(expectedNewMethod2Value); + expect(stub.method2).to.have.been.calledWith(222); }); - }); + }); }); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a138ffb..bf96922 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,10 @@ import * as sinon from "sinon"; -export function stubObject(object: T, methods?: string[] | object): T { - const stubObject = Object.assign( {}, object); +/** + * @param methods passing map of methods has become @deprecated as it may lead to overwriting stubbed method type + */ +export function stubObject(object: T, methods?: string[] | object): sinon.SinonStubbedInstance { + const stubObject = Object.assign(> {}, object); const objectMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(object)); const excludedMethods: string[] = [ '__defineGetter__', '__defineSetter__', 'hasOwnProperty', @@ -41,8 +44,15 @@ export function stubObject(object: T, methods?: string[] | obj return stubObject; } -export function stubInterface(methods: object = {}): T { - const object: T = stubObject( {}, methods); +export function stubConstructor(constructor: sinon.StubbableType): sinon.SinonStubbedInstance { + return sinon.createStubInstance(constructor); +} + +/** + * @param methods passing map of methods has become @deprecated as it may lead to overwriting stubbed method type + */ +export function stubInterface(methods: object = {}): sinon.SinonStubbedInstance { + const object = stubObject( {}, methods); const proxy = new Proxy(object, { get: (target, name) => {