From b7cbf0e69a9571cd595b12dc89dfffbda401c621 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 16 Mar 2020 15:12:02 -0700 Subject: [PATCH] Add global restore function to allow for single call import restoration (#21) * Add restore() onto ImportMock class * Add restore() to documentation * Update expected versions * 1.3.0 --- README.md | 113 +++++++++++++++++------------- package-lock.json | 2 +- package.json | 7 +- src/import-mock.ts | 24 +++++-- src/managers/manager.ts | 4 +- src/types/index.ts | 1 + src/types/manager.ts | 3 + test/resources/consumers/index.ts | 2 + test/spec/class-mock.spec.ts | 7 ++ test/spec/import-mock.spec.ts | 43 ++++++++++++ test/types/index.d.ts | 2 + 11 files changed, 149 insertions(+), 59 deletions(-) create mode 100644 src/types/manager.ts create mode 100644 test/spec/import-mock.spec.ts diff --git a/README.md b/README.md index b5302f9..db11f39 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 { @@ -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'; @@ -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 @@ -99,7 +107,7 @@ 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'; @@ -107,7 +115,7 @@ const mockManager = ImportMock.mockClass(fooModule, 'Foo'); ``` Default imports: -```javascript +```typescript // export default Foo import * as foo from '../foo'; @@ -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'); @@ -126,7 +134,7 @@ const mockManager = ImportMock.mockClass(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'); @@ -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'); @@ -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'); @@ -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'); @@ -189,6 +197,31 @@ const mockManager = ImportMock.mockOther(fooModule, 'fooName', 'fakeName'); Requires an object that matches Partial. 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') + +// + +ImportMock.restore() + +// all mocked imports will now be restored to their original values +``` + + --- ### MockManager (and MockStaticManager) @@ -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'); @@ -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'); @@ -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(() => { @@ -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'); @@ -275,7 +308,7 @@ mockManager.set('count', newVal); **`MockManager.getMockInstance(): T`** Returns an instance of the mocked class. -```javascript +```typescript import * as fooModule from '../foo'; const mockManager = ImportMock.mockClass(fooModule, 'Foo'); @@ -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'); @@ -324,7 +357,7 @@ mockManager.set(newVal); **`OtherManager.getValue(): T`** Returns the current mockValue -```javascript +```typescript import * as fooModule from '../foo'; const mockManager = ImportMock.mockOther(fooModule, 'FooName', 'fakeName'); @@ -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` diff --git a/package-lock.json b/package-lock.json index 536b01d..9bf3254 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-mock-imports", - "version": "1.2.6", + "version": "1.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e0705d7..32ac081 100644 --- a/package.json +++ b/package.json @@ -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", @@ -29,7 +29,7 @@ "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", @@ -37,7 +37,8 @@ "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": { diff --git a/src/import-mock.ts b/src/import-mock.ts index aba377e..3b1eaa4 100644 --- a/src/import-mock.ts +++ b/src/import-mock.ts @@ -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(mock: T): T { + ImportMock.sandboxedItems.push(mock); + return mock; + } + public static mockClass( module: { [importName: string]: IConstruct } | K, importName: keyof K = 'default'): MockManager { - return new MockManager(module, importName as string); + return ImportMock.sandbox(new MockManager(module, importName as string)); } public static mockStaticClass( module: { [importName: string]: IConstruct } | K, importName: keyof K = 'default'): StaticMockManager { - return new StaticMockManager(module, importName as string); + return ImportMock.sandbox(new StaticMockManager(module, importName as string)); } public static mockFunction(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(module: { [importName: string]: T[K] } | T, importName?: K, replaceWith?: Partial): OtherManager { + return ImportMock.sandbox(new OtherManager(module, importName as string || 'default', replaceWith)); } - public static mockOther(module: { [importName: string]: T[K] } | T, importName?: K, replaceWith?: Partial) { - return new OtherManager(module, importName as string || 'default', replaceWith); + public static restore(): void { + ImportMock.sandboxedItems.forEach(item => item.restore()); + ImportMock.sandboxedItems = []; } } diff --git a/src/managers/manager.ts b/src/managers/manager.ts index 11ca0e4..bbd0822 100644 --- a/src/managers/manager.ts +++ b/src/managers/manager.ts @@ -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) { diff --git a/src/types/index.ts b/src/types/index.ts index f9baf28..5c60815 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ export * from './construct'; +export * from './manager'; export * from './module'; export * from './stringKeyOf'; diff --git a/src/types/manager.ts b/src/types/manager.ts new file mode 100644 index 0000000..c6964ff --- /dev/null +++ b/src/types/manager.ts @@ -0,0 +1,3 @@ +export interface IManager { + restore: () => void +} diff --git a/test/resources/consumers/index.ts b/test/resources/consumers/index.ts index f154ca5..55fe696 100644 --- a/test/resources/consumers/index.ts +++ b/test/resources/consumers/index.ts @@ -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'; diff --git a/test/spec/class-mock.spec.ts b/test/spec/class-mock.spec.ts index 390a382..695aae9 100644 --- a/test/spec/class-mock.spec.ts +++ b/test/spec/class-mock.spec.ts @@ -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', () => { diff --git a/test/spec/import-mock.spec.ts b/test/spec/import-mock.spec.ts new file mode 100644 index 0000000..793df82 --- /dev/null +++ b/test/spec/import-mock.spec.ts @@ -0,0 +1,43 @@ +import 'mocha'; + +import { expect, assert } from 'chai'; +import { ImportMock } from '../../src/import-mock'; +import * as staticTestClass from '../resources/classes/static-test-class'; +import * as testClass from '../resources/classes/test-class'; +import * as funcModule from '../resources/functions/test-function'; +import * as otherModule from '../resources/other/test'; +import { StaticTestClassConsumer, TestClassConsumer, FunctionConsumer, OtherConsumer } from '../resources/consumers'; + +describe('Import Mock', () => { + beforeEach(() => { + ImportMock.restore() + }); + + it('should restore single mocked item back to default', () =>{ + ImportMock.mockClass(testClass, 'TestClass'); + ImportMock.restore(); + const consumer = new TestClassConsumer(); + expect(consumer.foo()).to.equal('bar'); + expect(consumer.getCount()).to.equal(1); + }); + + it('should restore all mocked items back to default', () => { + ImportMock.mockClass(testClass, 'TestClass'); + ImportMock.mockStaticClass(staticTestClass, 'StaticTestClass'); + ImportMock.mockFunction(funcModule, 'testFunction', 'bar'); + ImportMock.mockOther(otherModule, 'testConst', 'bar'); + + ImportMock.restore(); + + const consumer = new TestClassConsumer(); + const staticConsumer = new StaticTestClassConsumer(); + const functionConsumer = new FunctionConsumer(); + const otherConsumer = new OtherConsumer(); + + assert(consumer.foo() === 'bar', 'test class restores correctly'); + expect(consumer.getCount()).to.equal(1); + assert(staticConsumer.foo() === 'bar', 'static test class restores correctly'); + assert(functionConsumer.foo() === 'foo', 'function restores correctly'); + assert(otherConsumer.foo() ==='foo', 'other restores correctly'); + }); +}); diff --git a/test/types/index.d.ts b/test/types/index.d.ts index 60f335c..5355971 100644 --- a/test/types/index.d.ts +++ b/test/types/index.d.ts @@ -9,3 +9,5 @@ // TypeScript Version: 3.4 // TypeScript Version: 3.5 // TypeScript Version: 3.6 +// TypeScript Version: 3.7 +// TypeScript Version: 3.8