Skip to content

Commit

Permalink
Add global restore function to allow for single call import restorati…
Browse files Browse the repository at this point in the history
…on (#21)

* Add restore() onto ImportMock class

* Add restore() to documentation

* Update expected versions

* 1.3.0
  • Loading branch information
EmandM authored Mar 16, 2020
1 parent 5aa0ad0 commit b7cbf0e
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 59 deletions.
113 changes: 66 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@

## About

ts-mock-imports is useful if you want to replace imports with stub versions of those imports. This allows ES6 code to be easily unit-tested without the need for an explicit dependency injection library.
ts-mock-imports leverages the ES6 `import` syntax to mock out imported code with stub versions of the imported objects. This allows ES6 code to be easily unit-tested without the need for an explicit dependency injection library.

ts-mock-imports is built on top of sinon. [Sinon stub documentation](https://sinonjs.org/releases/latest/stubs/)

Mocked classes take all of the original class functions, and replace them with noop functions (functions returning `undefined`).
Mocked classes take all of the original class functions, and replace them with noop functions (functions returning `undefined`) while maintaining type safety.

This library needs to be run on TypeScript 2.6.1 or later.


- [About](#about)
- [Installation](#installation)
- [Usage](#usage)
- [API](#api)
- [Typescript Mock Imports](#typescript-mock-imports)
- [Intuitive mocking for Typescript imports.](#intuitive-mocking-for-typescript-imports)
- [About](#about)
- [Installation](#installation)
- [Usage](#usage)
- [API](#api)
- [ImportMock](#importmock)
- [MockManager (and MockStaticManager)](#mockmanager-and-mockstaticmanager)
- [OtherManager](#othermanager)
- [Test](#test)
- [Typescript Tests](#typescript-tests)
- [Unit Tests](#unit-tests)
- [Limitations](#limitations)
- [Test](#test)

## Installation

Expand All @@ -45,16 +45,21 @@ npm install ts-mock-imports --save-dev
## Usage

`src/foo.ts`
```javascript
```typescript
export class Foo {
private count: number;
constructor() {
throw new Error();
}

public getCount(): number {
return count;
}
}
```

`src/bar.ts`
```javascript
```typescript
import { Foo } from './foo';

export class Bar {
Expand All @@ -65,7 +70,7 @@ export class Bar {
```

`test/bar.spec.ts`
```javascript
```typescript
import { ImportMock } from 'ts-mock-imports';
import { Bar } from './Bar';
import * as fooModule from '../src/foo';
Expand All @@ -78,8 +83,11 @@ const mockManager = ImportMock.mockClass(fooModule, 'Foo');
// No longer throws an error
const bar = new Bar();

// Call restore to reset to original imports
mockManager.restore();
// Easily add mock responses for testing
mockmanager.mock('getCount', 3)

// Call restore to reset all mocked objects to original imports
ImportMock.restore();
```

## API
Expand All @@ -99,15 +107,15 @@ Both the source file and test file need to use the same path to import the mocke
What the class is exported as. If exported using `export default` then this parameter is not needed.

Using importName:
```javascript
```typescript
// export class Foo
import * as fooModule from '../src/foo';

const mockManager = ImportMock.mockClass(fooModule, 'Foo');
```

Default imports:
```javascript
```typescript
// export default Foo
import * as foo from '../foo';

Expand All @@ -117,7 +125,7 @@ const mockManager = ImportMock.mockClass(foo);
Import mock will infer the type of `Foo` if it is the only item exported out of it's file. If more things are exported, you will need to explicitly provide types to Import mock.

Explicit typing:
```javascript
```typescript
import * as fooModule from '../foo';

const mockManager = ImportMock.mockClass<fooModule.Foo>(fooModule, 'Foo');
Expand All @@ -126,7 +134,7 @@ const mockManager = ImportMock.mockClass<fooModule.Foo>(fooModule, 'Foo');
If you wish to ensure that `Foo` is the correct name for the mocked class, give import mock the type of your module.

Explicit typing with full type assurance
```javascript
```typescript
import * as fooModule from '../foo';

const mockManager = ImportMock.mockClass<fooModule.Foo, typeof fooModule>(fooModule, 'Foo');
Expand All @@ -145,7 +153,7 @@ Takes the same arguments as `mockClass` but only replaces static functions on th

Static classes:
(Only recreates static methods)
```javascript
```typescript
import * as fooModule from '../foo';

const mockManager = ImportMock.mockStaticClass(fooModule, 'Foo');
Expand All @@ -160,7 +168,7 @@ Returns a SinonStub that is set up to return the optional argument.
Call restore on the stub object to restore the original export.

Function exports:
```javascript
```typescript
import * as fooModule from '../foo';

const stub = ImportMock.mockFunction(fooModule, 'fooFunction', 'bar');
Expand All @@ -178,7 +186,7 @@ stub.restore()
Useful for mocking out or removing variables and enums.

Variable mocking:
```javascript
```typescript
import * as fooModule from '../foo';

const mockManager = ImportMock.mockOther(fooModule, 'fooName', 'fakeName');
Expand All @@ -189,6 +197,31 @@ const mockManager = ImportMock.mockOther(fooModule, 'fooName', 'fakeName');

Requires an object that matches Partial<OriginalType>. This argument is an optional shorthand, and the value can be updated using mockMangaer.set().

---

**`restore(): void`**

`restore()` will restore all mocked items. Allows `ImportMock` to be used as a sandbox.

Useful for restoring when multiple mocks have been created.

Variable mocking:
```typescript
import * as fooModule from '../foo';
import * as bazModule from '../baz';

ImportMock.mockClass(fooModule, 'Foo');
ImportMock.mockClass(fooModule, 'Bar');
ImportMock.mockFunction(bazModule, 'mainFunction')

// <run tests>

ImportMock.restore()

// all mocked imports will now be restored to their original values
```


---

### MockManager (and MockStaticManager)
Expand All @@ -211,7 +244,7 @@ The value returned when the mocked function is called.

Mocking functions:
(Returns a sinon stub)
```javascript
```typescript
import * as fooModule from '../foo';

const fooManager = ImportMock.mockClass(fooModule, 'Foo');
Expand All @@ -222,7 +255,7 @@ fooManager.mock('bar');
```

Mocking functions with a return object:
```javascript
```typescript
import * as fooModule from '../foo';

const mockManager = ImportMock.mockClass(fooModule, 'Foo');
Expand All @@ -232,7 +265,7 @@ mockManager.mock('bar', 'Bar');
```

If you wish to run modified code when the mocked function is called, you can use `sinon.callsFake()`
```javascript
```typescript
const mockManager = ImportMock.mockClass(fooModule, 'Foo');
const sinonStub = mockManager.mock('bar');
sinonStub.callsFake(() => {
Expand Down Expand Up @@ -260,7 +293,7 @@ The mock value of the property.


Mocking variable with a return object:
```javascript
```typescript
import * as fooModule from '../foo';

const mockManager = ImportMock.mockClass(fooModule, 'Foo');
Expand All @@ -275,7 +308,7 @@ mockManager.set('count', newVal);
**`MockManager<T>.getMockInstance(): T`**

Returns an instance of the mocked class.
```javascript
```typescript
import * as fooModule from '../foo';

const mockManager = ImportMock.mockClass(fooModule, 'Foo');
Expand Down Expand Up @@ -308,7 +341,7 @@ The mock value of the export.


Mocking variable with a return object:
```javascript
```typescript
import * as fooModule from '../foo';

const mockManager = ImportMock.mockOther(fooModule, 'FooName', 'fakeName');
Expand All @@ -324,7 +357,7 @@ mockManager.set(newVal);
**`OtherManager<T>.getValue(): T`**

Returns the current mockValue
```javascript
```typescript
import * as fooModule from '../foo';

const mockManager = ImportMock.mockOther(fooModule, 'FooName', 'fakeName');
Expand All @@ -350,22 +383,8 @@ Requirejs is not currently compatible with this library.

## Test

This library contains two types of tests. Typescript tests ensure the typing systems work as intended, while unit tests check the runtime functionality of the library.

### All tests

```
npm run test
```

### Typescript Tests

```
npm run dtslint
```
This library contains two types of tests.
1. Typescript tests to ensure typing works as intended: `npm run dtslint`
2. Unit tests to check the runtime functionality of the library: `npm run unit-test`

### Unit Tests

```
npm run unit-test
```
Both test suites are run when using `npm run test`
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-mock-imports",
"version": "1.2.6",
"version": "1.3.0",
"description": "Intuitive mocking for Typescript class imports",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down Expand Up @@ -29,15 +29,16 @@
"homepage": "https://github.com/EmandM/ts-mock-imports",
"peerDependencies": {
"typescript": ">=2.6.1 < 4",
"sinon": ">= 4.1.2 < 7.4"
"sinon": ">= 4.1.2"
},
"scripts": {
"dtslint": "dtslint test/types",
"lint": "tslint --project tsconfig.json 'src/**/*{.ts,.tsx}'",
"lint-fix": "tslint --project tsconfig.json --fix 'src/**/*{.ts,.tsx}'",
"unit-test": "mocha -r ts-node/register test/**/*.spec.ts",
"test": "npm run dtslint && npm run unit-test",
"compile": "rimraf lib && tsc -p src"
"compile": "rimraf lib && tsc -p src",
"build": "npm run compile"
},
"dependencies": {},
"devDependencies": {
Expand Down
24 changes: 18 additions & 6 deletions src/import-mock.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import * as sinonModule from 'sinon';
import { MockManager, OtherManager, StaticMockManager } from './managers/index';
import { IConstruct, IModule } from './types';
import { IConstruct, IModule, IManager } from './types';
const sinon = sinonModule as sinonModule.SinonStatic;

export class ImportMock {
private static sandboxedItems: IManager[] = [];

private static sandbox<T extends IManager>(mock: T): T {
ImportMock.sandboxedItems.push(mock);
return mock;
}

public static mockClass<T, K extends IModule = any>(
module: { [importName: string]: IConstruct<T> } | K, importName: keyof K = 'default'): MockManager<T> {
return new MockManager<T>(module, importName as string);
return ImportMock.sandbox(new MockManager<T>(module, importName as string));
}

public static mockStaticClass<T, K extends IModule = any>(
module: { [importName: string]: IConstruct<T> } | K, importName: keyof K = 'default'): StaticMockManager<T> {
return new StaticMockManager<T>(module, importName as string);
return ImportMock.sandbox(new StaticMockManager<T>(module, importName as string));
}

public static mockFunction<K extends IModule>(module: { [importName: string]: () => any } | K, importName: keyof K = 'default', returns?: any): sinon.SinonStub {
return sinon.stub(module, importName as string).returns(returns);
return ImportMock.sandbox(sinon.stub(module, importName as string).returns(returns));
}

public static mockOther<T extends IModule, K extends keyof T>(module: { [importName: string]: T[K] } | T, importName?: K, replaceWith?: Partial<T[K]>): OtherManager<T[K]> {
return ImportMock.sandbox(new OtherManager<T[K]>(module, importName as string || 'default', replaceWith));
}

public static mockOther<T extends IModule, K extends keyof T>(module: { [importName: string]: T[K] } | T, importName?: K, replaceWith?: Partial<T[K]>) {
return new OtherManager<T[K]>(module, importName as string || 'default', replaceWith);
public static restore(): void {
ImportMock.sandboxedItems.forEach(item => item.restore());
ImportMock.sandboxedItems = [];
}
}
4 changes: 2 additions & 2 deletions src/managers/manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IModule } from '../types';
import { IModule, IManager } from '../types';

export class Manager {
export class Manager implements IManager {
protected original: any;

constructor(protected module: IModule, protected importName: string) {
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './construct';
export * from './manager';
export * from './module';
export * from './stringKeyOf';
3 changes: 3 additions & 0 deletions src/types/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IManager {
restore: () => void
}
2 changes: 2 additions & 0 deletions test/resources/consumers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './default-class-consumer';
export * from './static-test-class-consumer';
export * from './test-class-consumer';
export * from './function-consumer';
export * from './other-consumer';
7 changes: 7 additions & 0 deletions test/spec/class-mock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ describe('Class Mock', () => {
expect(consumer.foo()).to.be.undefined;
manager.restore();
});

it('should restore back to the original import', () => {
const manager = ImportMock.mockStaticClass(staticTestClass, 'StaticTestClass');
const consumer = new StaticTestClassConsumer();
manager.restore();
expect(consumer.foo()).to.equal('bar');
});
});

describe('Mock Manager', () => {
Expand Down
Loading

0 comments on commit b7cbf0e

Please sign in to comment.