Skip to content

Commit

Permalink
feat(mock-instance): simpler interface
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Jan 20, 2021
1 parent f52bf38 commit 0306643
Show file tree
Hide file tree
Showing 11 changed files with 494 additions and 55 deletions.
76 changes: 52 additions & 24 deletions docs/articles/api/MockInstance.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,58 @@ description: Information how to customize mock components, directives, services
sidebar_label: MockInstance
---

A mock instance of declarations or providers in tests may be customized via `MockInstance`.
It is useful, when we want to configure spies before its usage.
**MockInstance** helps to define **customizations for declarations** and providers in test suites
before the desired instance have been created.

It is useful, when we want to configure spies before their usage.
It supports: modules, components, directives, pipes, services and tokens.

There are two ways how to customize a mock instance:
There are **three ways how to customize** a mock instance:

- set desired values
- manipulate the instance (with access to injector)
- return the desired shape (with access to injector)

## Set desired values

It helps to provide a predefined spy or value.

- directly define properties and methods
- return the desired shape
```ts
MockInstance(Service, 'methodName', () => 'fake');
MockInstance(Service, 'propName', 'fake');
MockInstance(Service, 'propName', () => 'fake', 'get');
MockInstance(Service, 'propName', () => undefined, 'set');
```

## Customizing classes
It returns the provided value, that allows to customize spies.

```ts
MockInstance(Service, 'methodName', jasmine.createSpy())
.and.returnValue('fake');
MockInstance(Service, 'propName', jest.fn(), 'get')
.mockReturnValue('fake');
```

## Manipulate the instance

If we pass a callback as the second parameter to **MockInstance**,
then we have access to the instance and to the related injector.

```ts
// setting values to instance
MockInstance(Service, (instance, injector) => {
instance.prop1 = injector.get(SOME_TOKEN);
instance.method1 = jasmine.createSpy().and.returnValue(5);
instance.method2 = value => (instance.prop2 = value);
});
```

// returning a custom shape
## Return the desired shape

If the callback of the second parameter of **MockInstance** returns something,
then the returned value will be applied to the instance.

```ts
// with injector and spies
MockInstance(Service, (instance, injector) => ({
prop1: injector.get(SOME_TOKEN),
method1: jasmine.createSpy().and.returnValue(5),
Expand All @@ -40,7 +72,7 @@ MockInstance(Service, () => ({

## Customizing tokens

In case of tokens, the handler should return the token value.
In case of tokens, a callback should return the token value.

```ts
MockInstance(TOKEN, (instance, injector) => {
Expand All @@ -51,21 +83,22 @@ MockInstance(TOKEN, () => true);

## Resetting customization

In order to reset the handler, `MockInstance` should be called without it.
In order to reset the provided callback, `MockInstance` should be called without it.

```ts
MockInstance(Service);
MockInstance(TOKEN);
// Or simply one call.
// It resets all handlers for all declarations.
// It resets all customizations for all declarations.
MockReset();
```

## Overriding customization

Every call of `MockInstance` overrides the previous handler.
Every call of `MockInstance` overrides the previous callback.
`MockInstance` can be called anywhere,
but if it is called in `beforeEach` or in `it`, then the handler has its effect only during the current spec.
but **suggested usage** is to call `MockInstance` in `beforeEach` or in `it`,
then the callback has its effect only during the current spec.

```ts
beforeAll(() => MockInstance(TOKEN, () => true));
Expand Down Expand Up @@ -127,19 +160,14 @@ a solution here. That is where `ng-mocks` helps again with the `MockInstance` he
It accepts a class as the first parameter, and a tiny callback describing how to customize its instances as the second one.

```ts
beforeEach(() =>
MockInstance(ChildComponent, () => ({
// Now we can customize a mock object
// of ChildComponent in its ctor call.
// The object will be extended
// with the returned object.
update$: EMPTY,
})),
);
// Now we can customize a mock object.
// The update$ property of the object
// will be set to EMPTY in its ctor call.
beforeEach(() => MockInstance(ChildComponent, 'update$', EMPTY));
```

Profit. When Angular creates an instance of `ChildComponent`, the callback is called in its ctor, and `update$` property
of the instance is an `Observable` instead of `undefined`.
Profit. When Angular creates an instance of `ChildComponent`, the rule is applied in its ctor, and `update$` property
of the instance is not `undefined`, but an `Observable`.

## Advanced example

Expand Down
2 changes: 1 addition & 1 deletion docs/articles/api/ngMocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Introduction to ngMocks object and its purpose
sidebar_label: Introduction
---

`ngMocks` is a namespace which provides variety of helper functions which help to customize mocks,
**ngMocks** is a namespace which provides variety of helper functions which help to customize mocks,
access desired elements and instances in fixtures.

## Customizing mock behavior
Expand Down
2 changes: 2 additions & 0 deletions lib/common/core.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const extendClass = <I extends object>(base: AnyType<I>): Type<I> => {
const child: Type<I> = extendClassicClass(base);
Object.defineProperty(child, 'name', {
configurable: true,
enumerable: true,
value: `MockMiddleware${base.name}`,
writable: true,
});
Expand All @@ -114,6 +115,7 @@ export const extendClass = <I extends object>(base: AnyType<I>): Type<I> => {
if (parameters.length) {
Object.defineProperty(child, 'parameters', {
configurable: true,
enumerable: false,
value: [...parameters],
writable: true,
});
Expand Down
7 changes: 6 additions & 1 deletion lib/common/decorate.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ export default function (mock: AnyType<any>, source: AnyType<any>, config: ngMoc
name: { value: `MockOf${source.name}` },
nameConstructor: { value: mock.name },
});
mock.prototype.__ngMocksConfig = config;
Object.defineProperty(mock.prototype, '__ngMocksConfig', {
configurable: true,
enumerable: false,
value: config,
writable: true,
});
}
5 changes: 2 additions & 3 deletions lib/common/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { mapValues } from '../common/core.helpers';
import { AnyType } from '../common/core.types';
import { IMockBuilderConfig } from '../mock-builder/types';
import mockHelperStub from '../mock-helper/mock-helper.stub';
import mockInstanceApply from '../mock-instance/mock-instance-apply';
import helperMockService from '../mock-service/helper.mock-service';

import funcIsMock from './func.is-mock';
Expand Down Expand Up @@ -155,9 +156,7 @@ const applyOverrides = (instance: any, mockOf: any, injector?: Injector): void =
if (instance.__ngMocksConfig.init) {
callbacks.push(instance.__ngMocksConfig.init);
}
if (ngMocksUniverse.configInstance.get(mockOf)?.init) {
callbacks.push(ngMocksUniverse.configInstance.get(mockOf).init);
}
callbacks.push(...mockInstanceApply(mockOf));

for (const callback of callbacks) {
const overrides = callback(instance, injector);
Expand Down
20 changes: 20 additions & 0 deletions lib/mock-instance/mock-instance-apply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ngMocksUniverse from '../common/ng-mocks-universe';
import mockHelperStubMember from '../mock-helper/mock-helper.stub-member';

export default (def: any): any[] => {
const callbacks = [];

const config = ngMocksUniverse.configInstance.get(def);
if (config?.init) {
callbacks.push(config.init);
}
if (config?.overloads) {
for (const [name, stub, encapsulation] of config.overloads) {
callbacks.push((instance: any) => {
mockHelperStubMember(instance, name, stub, encapsulation);
});
}
}

return callbacks;
};
150 changes: 127 additions & 23 deletions lib/mock-instance/mock-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@ import { InjectionToken, Injector } from '@angular/core';
import { AbstractType, Type } from '../common/core.types';
import ngMocksUniverse from '../common/ng-mocks-universe';

let installReporter = true;
const restore = (declaration: any, config: any): void => {
if (installReporter) {
jasmine.getEnv().addReporter(reporter);
installReporter = false;
const stack: any[][] = [[]];
const stackPush = () => {
stack.push([]);
};
const stackPop = () => {
for (const declaration of stack.pop() || /* istanbul ignore next */ []) {
ngMocksUniverse.configInstance.get(declaration)?.overloads?.pop();
}
// istanbul ignore if
if (stack.length === 0) {
stack.push([]);
}
};

ngMocksUniverse.getLocalMocks().push([declaration, config]);
const reporterStack: jasmine.CustomReporter = {
jasmineDone: stackPop,
jasmineStarted: stackPush,
specDone: stackPop,
specStarted: stackPush,
suiteDone: stackPop,
suiteStarted: stackPush,
};

const reporter: jasmine.CustomReporter = {
Expand All @@ -35,6 +48,108 @@ const reporter: jasmine.CustomReporter = {
},
};

let installReporter = true;
const restore = (declaration: any, config: any): void => {
if (installReporter) {
jasmine.getEnv().addReporter(reporter);
installReporter = false;
}

ngMocksUniverse.getLocalMocks().push([declaration, config]);
};

interface MockInstanceArgs {
accessor?: 'get' | 'set';
data?: any;
key?: string;
value?: any;
}

const parseMockInstanceArgs = (args: any[]): MockInstanceArgs => {
const set: MockInstanceArgs = {};

if (typeof args[0] === 'string') {
set.key = args[0];
set.value = args[1];
set.accessor = args[2];
} else {
set.data = args[0];
}

return set;
};

const mockInstanceConfig = <T>(declaration: Type<T> | AbstractType<T> | InjectionToken<T>, data?: any): void => {
const config = typeof data === 'function' ? { init: data } : data;
const universeConfig = ngMocksUniverse.configInstance.has(declaration)
? ngMocksUniverse.configInstance.get(declaration)
: {};
restore(declaration, universeConfig);

if (config) {
ngMocksUniverse.configInstance.set(declaration, {
...universeConfig,
...config,
});
} else {
ngMocksUniverse.configInstance.set(declaration, {
...universeConfig,
init: undefined,
overloads: undefined,
});
}
};

let installStackReporter = true;
const mockInstanceMember = <T>(
declaration: Type<T> | AbstractType<T> | InjectionToken<T>,
name: string,
stub: any,
encapsulation?: 'get' | 'set',
) => {
if (installStackReporter) {
jasmine.getEnv().addReporter(reporterStack);
installStackReporter = false;
}
const config = ngMocksUniverse.configInstance.has(declaration) ? ngMocksUniverse.configInstance.get(declaration) : {};
const overloads = config.overloads || [];
overloads.push([name, stub, encapsulation]);
config.overloads = overloads;
ngMocksUniverse.configInstance.set(declaration, config);
stack[stack.length - 1].push(declaration);

return stub;
};

/**
* @see https://github.com/ike18t/ng-mocks#ngmocksstubmember
*/
export function MockInstance<T extends object, K extends keyof T, S extends () => T[K]>(
instance: Type<T> | AbstractType<T>,
name: K,
stub: S,
encapsulation: 'get',
): S;

/**
* @see https://github.com/ike18t/ng-mocks#ngmocksstubmember
*/
export function MockInstance<T extends object, K extends keyof T, S extends (value: T[K]) => void>(
instance: Type<T> | AbstractType<T>,
name: K,
stub: S,
encapsulation: 'set',
): S;

/**
* @see https://github.com/ike18t/ng-mocks#ngmocksstubmember
*/
export function MockInstance<T extends object, K extends keyof T, S extends T[K]>(
instance: Type<T> | AbstractType<T>,
name: K,
stub: S,
): S;

/**
* @see https://github.com/ike18t/ng-mocks#mockinstance
*/
Expand Down Expand Up @@ -71,24 +186,13 @@ export function MockInstance<T>(
},
): void;

export function MockInstance<T>(declaration: Type<T> | AbstractType<T> | InjectionToken<T>, data?: any) {
const config = typeof data === 'function' ? { init: data } : data;
const universeConfig = ngMocksUniverse.configInstance.has(declaration)
? ngMocksUniverse.configInstance.get(declaration)
: {};
restore(declaration, universeConfig);

if (config) {
ngMocksUniverse.configInstance.set(declaration, {
...universeConfig,
...config,
});
} else {
ngMocksUniverse.configInstance.set(declaration, {
...universeConfig,
init: undefined,
});
export function MockInstance<T>(declaration: Type<T> | AbstractType<T> | InjectionToken<T>, ...args: any[]) {
const { key, value, accessor, data } = parseMockInstanceArgs(args);
if (key) {
return mockInstanceMember(declaration, key, value, accessor);
}

mockInstanceConfig(declaration, data);
}

export function MockReset() {
Expand Down
5 changes: 2 additions & 3 deletions lib/mock-service/helper.use-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { mapValues } from '../common/core.helpers';
import { isNgInjectionToken } from '../common/func.is-ng-injection-token';
import ngMocksUniverse from '../common/ng-mocks-universe';
import mockHelperStub from '../mock-helper/mock-helper.stub';
import mockInstanceApply from '../mock-instance/mock-instance-apply';
import { MockService } from '../mock-service/mock-service';

const applyCallbackToken = (def: any): boolean => isNgInjectionToken(def) || typeof def === 'string';
Expand Down Expand Up @@ -49,9 +50,7 @@ export default <D, I>(
if (overrides) {
callbacks.push(overrides);
}
if (ngMocksUniverse.configInstance.get(def)?.init) {
callbacks.push(ngMocksUniverse.configInstance.get(def).init);
}
callbacks.push(...mockInstanceApply(def));

return applyCallback(def, instance, callbacks, injector, overrides);
},
Expand Down
Loading

0 comments on commit 0306643

Please sign in to comment.