Skip to content

Commit

Permalink
Merge pull request #15 from ttarnowski/version-1.0.21
Browse files Browse the repository at this point in the history
Fixed typing issues. Added new stub function. Made method map depreca…
  • Loading branch information
ttarnowski authored Sep 11, 2019
2 parents e6af158 + 020a5ac commit 7a5c8b5
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 62 deletions.
70 changes: 63 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Sinon extension providing functions to:
- stub all object methods
- stub interface
- stub object constructor

## Prerequisites

Expand All @@ -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

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Test>({ method: 'stubbed' });
const testStub = stubInterface<Test>();

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<Test>();
const testStub = stubInterface<Test>({ 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>(Test);

expect(testStub.method()).to.be.undefined;

Expand All @@ -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<ITest>({
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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
169 changes: 119 additions & 50 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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<A>(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 {
Expand Down Expand Up @@ -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;
});
});
Expand All @@ -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';
Expand All @@ -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<ITest>({
method2: 'test'
Expand All @@ -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<ITest>();
interfaceStub.method2.returns('string');
const interfaceStub = stubInterface<ITest>();

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>(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);
});
});
});
});
18 changes: 14 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import * as sinon from "sinon";

export function stubObject<T extends object>(object: T, methods?: string[] | object): T {
const stubObject = Object.assign(<T> {}, object);
/**
* @param methods passing map of methods has become @deprecated as it may lead to overwriting stubbed method type
*/
export function stubObject<T extends object>(object: T, methods?: string[] | object): sinon.SinonStubbedInstance<T> {
const stubObject = Object.assign(<sinon.SinonStubbedInstance<T>> {}, object);
const objectMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(object));
const excludedMethods: string[] = [
'__defineGetter__', '__defineSetter__', 'hasOwnProperty',
Expand Down Expand Up @@ -41,8 +44,15 @@ export function stubObject<T extends object>(object: T, methods?: string[] | obj
return stubObject;
}

export function stubInterface<T extends object>(methods: object = {}): T {
const object: T = stubObject<T>(<T> {}, methods);
export function stubConstructor<T extends object>(constructor: sinon.StubbableType<T>): sinon.SinonStubbedInstance<T> {
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<T extends object>(methods: object = {}): sinon.SinonStubbedInstance<T> {
const object = stubObject<T>(<T> {}, methods);

const proxy = new Proxy(object, {
get: (target, name) => {
Expand Down

0 comments on commit 7a5c8b5

Please sign in to comment.