Skip to content

Commit

Permalink
feat(MockBuilder): default flags as dependency or export #2647
Browse files Browse the repository at this point in the history
BREAKING CHANGE: MockBuilder with 2 params marks all chain calls as dependency
BREAKING CHANGE: MockBuilder with 0-1 params marks all chain calls as export

Please read: https://ng-mocks.sudo.eu/migrations#from-13-to-14
  • Loading branch information
satanTime committed Jun 11, 2022
1 parent 4bdac7a commit f37a663
Show file tree
Hide file tree
Showing 102 changed files with 2,380 additions and 386 deletions.
129 changes: 112 additions & 17 deletions docs/articles/api/MockBuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@ but with minimum overhead.
Usually, we have something simple to test, but time to time, the simplicity is killed by nightmarish dependencies.
The good thing here is that commonly the dependencies have been declared or imported in the same module, where our
tested thing is. Therefore, with help of `MockBuilder` we can quite easily define a testing module,
where **everything in the module will be replaced with their mocks**, except the tested thing: `MockBuilder( TheThing, ItsModule )`.
where **everything in the module will be replaced with their mocks**, except the tested thing:

MockBuilder tends to provide **a simple instrument to turn Angular dependencies into their mocks**,
```ts
beforeEach(() => {
return MockBuilder(TheThing, ItsModule);
});
```

`MockBuilder` tends to provide **a simple instrument to turn Angular dependencies into their mocks**,
does it in isolated scopes,
and has a rich toolkit that supports:

- detection and creation of mocks for root providers
- replacement of modules and declarations in any depth
- exclusion of modules, declarations and providers in any depth
- replacement of modules and declarations at any depth
- exclusion of modules, declarations and providers at any depth

## Simple example

Expand Down Expand Up @@ -50,6 +56,94 @@ describe('MockBuilder:simple', () => {
});
```

## Flex mode

You can use the flex mode to build TestBed in the way you want.
Let's assume you want to test `TargetComponent` and it has 3 dependencies:

- `CurrencyPipe` should be a mock
- `TimeService` should be a mock
- `ReactiveFormModule` should stay as it is

For this case, `MockBuilder` can be called like that:

```ts
beforeEach(() => {
return MockBuilder()
// It will be declared as it is in the TestBed.
.keep(TargetComponent)

// It will be declared as a mock in the TestBed.
.mock(CurrencyPipe)

// It will be provided as a mock in the TestBed.
.mock(TimeService)

// It will be imported as it is in the TestBed.
.keep(ReactiveFormModule);
});
```

This approach is good, however the problem is that dependencies are provided explicitly,
and if someone has removed `ReactiveFormModule` from the module where `TargetComponent` has been declared,
the test won't fail, whereas the app will.

There is where the [strict mode](#strict-mode) shines.

## Strict mode

The strict mode is enabled if you pass 2 parameters to `MockBuilder`:

- the first parameter is what should be provided and kept as it is for testing
- the second parameter is what should be provided and mocked for testing
- the chain calls only customize these declarations

If we consider the example from the [flex mode](#flex-mode), then, to enable the strict mode, the code should look like:

```ts
beforeEach(() => {
// TargetComponent is exported as it is from TargetModule
// all imports and declarations of TargetModule should be mocked
return MockBuilder(TargetComponent, TargetModule)

// It marks ReactiveFormModule to be kept as it is in TargetModule
// and throw an error if TargetModule or its imports don't import it.
.keep(ReactiveFormModule);
});
```

All dependencies of `TargetComponent` are in `TargetModule`, and if any of them have been deleted, tests will fail.

Also, if someone has deleted `ReactiveFormModule` from `TargetModule`, tests will fail too,
because `MockBuilder` will throw an error about missing `ReactiveFormModule` which should be kept.

However, what if more than 1 module is requires? For example, for lazy modules.
In the case of lazy loaded modules, you need to import more than 1 module in TestBed.
Usually, it's an `AppModule` which provides root declarations, and a `LazyModule` which belongs to a specific route.
In order to do so, simply pass arrays as parameters of `MockBuilder`:

```ts
beforeEach(() => {
return MockBuilder(
// It can be an array too, if you want to keep and export more than 1 thing
TargetComponent,

[
// It will mock and import TargetModule in TestBed
TargetModule,
// It will mock and import AppModule in TestBed
AppModule,
],
)

// It will keep CurrencyPipe as it is,
// and throw if neither TargetModule nor AppModule declares or imports it.
.keep(CurrencyPipe);
});
```

**The strict mode is the recommended approach**.

## Chain functions

### .keep()
Expand All @@ -58,7 +152,7 @@ If we want to keep a module, component, directive, pipe or provider as it is. We

```ts
beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
return MockBuilder(MyComponent)
.keep(SomeModule)
.keep(SomeModule.forSome())
.keep(SomeModule.forAnother())
Expand All @@ -76,7 +170,7 @@ If we want to turn anything into a mock object, even a part of a kept module we

```ts
beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
return MockBuilder(MyComponent)
.mock(SomeModule)
.mock(SomeModule.forSome())
.mock(SomeModule.forAnother())
Expand All @@ -92,7 +186,7 @@ For pipes, we can set their handlers as the 2nd parameter of `.mock`.

```ts
beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
return MockBuilder(MyComponent)
.mock(SomePipe, value => 'My Custom Content');
});
```
Expand All @@ -102,7 +196,7 @@ Please keep in mind that the mock object of the service will be extended with th

```ts
beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
return MockBuilder(MyComponent)
.mock(SomeService3, anything1)
.mock(SOME_TOKEN, anything2);
});
Expand Down Expand Up @@ -153,7 +247,7 @@ In case of `RouterTestingModule` we need to use [`.keep`](#keep) for both of the

```ts
beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
return MockBuilder(MyComponent)
.keep(RouterModule)
.keep(RouterTestingModule.withRoutes([]));
});
Expand Down Expand Up @@ -213,7 +307,7 @@ beforeEach(() => {

If we want to test a component, directive or pipe which, unfortunately, has not been exported,
then we need to mark it with the `export` flag.
Does not matter how deep it is. It will be exported to the level of `TestingModule`.
Does not matter how deep it is. It will be exported to the level of `MyModule`.

```ts
beforeEach(() => {
Expand Down Expand Up @@ -248,16 +342,17 @@ beforeEach(() => {

### `dependency` flag

By default, all definitions are added to the `TestingModule` if they are not a dependency of another definition.
Modules are added as imports to the `TestingModule`.
Components, Directive, Pipes are added as declarations to the `TestingModule`.
Tokens and Services are added as providers to the `TestingModule`.
If we do not want something to be added to the `TestingModule` at all, then we need to mark it with the `dependency` flag.
By default, all definitions are added to the `MyModule` if they are not a dependency of another definition.
Modules are added as imports to the `MyModule`.
Components, Directive, Pipes are added as declarations to the `MyModule`.
Tokens and Services are added as providers to the `MyModule`.
If we do not want something to be added to the `MyModule` at all, then we need to mark it with the `dependency` flag.

```ts
beforeEach(() => {
return (
MockBuilder(MyComponent, MyModule)
MockBuilder(MyComponent)
.mock(MyModule)
.keep(SomeModuleComponentDirectivePipeProvider1, {
dependency: true,
})
Expand Down Expand Up @@ -394,7 +489,7 @@ const ngModule = MockBuilder()
.build();
```

Also, we can suppress the first parameter with `null` if we want to create mocks for all declarations.
Also, we can suppress the first parameter with `null` or `undefined` if we want to create mocks for all declarations.

```ts
const ngModule = MockBuilder(null, MyModule)
Expand Down
12 changes: 6 additions & 6 deletions docs/articles/api/MockComponent.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ and [`MockRender`](MockRender.md):
```ts
describe('Test', () => {
beforeEach(() => {
return MockBuilder(TargetComponent).mock(DependencyComponent);
return MockBuilder(TargetComponent, ItsModule);
});

it('should create', () => {
Expand All @@ -105,11 +105,11 @@ Please, pay attention to comments in the code.
```ts title="https://github.com/ike18t/ng-mocks/blob/master/examples/MockComponent/test.spec.ts"
describe('MockComponent', () => {
beforeEach(() => {
return MockBuilder(TestedComponent).mock(DependencyComponent);
return MockBuilder(MyComponent, ItsModule);
});

it('sends the correct value to the child input', () => {
const fixture = MockRender(TestedComponent);
const fixture = MockRender(MyComponent);
const component = fixture.point.componentInstance;

// The same as
Expand All @@ -123,7 +123,7 @@ describe('MockComponent', () => {
).componentInstance;

// Let's pretend that DependencyComponent has 'someInput' as
// an input. TestedComponent sets its value via
// an input. MyComponent sets its value via
// `[someInput]="value"`. The input's value will be passed into
// the mock component so we can assert on it.
component.value = 'foo';
Expand All @@ -134,7 +134,7 @@ describe('MockComponent', () => {
});

it('does something on an emit of the child component', () => {
const fixture = MockRender(TestedComponent);
const fixture = MockRender(MyComponent);
const component = fixture.point.componentInstance;

// The same as
Expand All @@ -145,7 +145,7 @@ describe('MockComponent', () => {
const mockComponent = ngMocks.findInstance(DependencyComponent);

// Again, let's pretend DependencyComponent has an output
// called 'someOutput'. TestedComponent listens on the output via
// called 'someOutput'. MyComponent listens on the output via
// `(someOutput)="trigger($event)"`.
// Let's install a spy and trigger the output.
ngMocks.stubMember(
Expand Down
17 changes: 10 additions & 7 deletions docs/articles/api/MockDirective.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ and [`MockRender`](MockRender.md):
```ts
describe('Test', () => {
beforeEach(() => {
return MockBuilder(TargetComponent).mock(DependencyDirective);
// DependencyDirective is a declaration in ItsModule.
return MockBuilder(TargetComponent, ItsModule);
});

it('should create', () => {
Expand All @@ -105,11 +106,12 @@ Please, pay attention to comments in the code.
```ts title="https://github.com/ike18t/ng-mocks/blob/master/examples/MockDirective-Attribute/test.spec.ts"
describe('MockDirective:Attribute', () => {
beforeEach(() => {
return MockBuilder(TestedComponent).mock(DependencyDirective);
// DependencyDirective is a declaration in ItsModule.
return MockBuilder(MyComponent, ItsModule);
});

it('sends the correct value to the input', () => {
const fixture = MockRender(TestedComponent);
const fixture = MockRender(MyComponent);
const component = fixture.point.componentInstance;

// The same as
Expand All @@ -123,7 +125,7 @@ describe('MockDirective:Attribute', () => {
);

// Let's pretend DependencyDirective has 'someInput'
// as an input. TestedComponent sets its value via
// as an input. MyComponent sets its value via
// `[someInput]="value"`. The input's value will be passed into
// the mock directive so we can assert on it.
component.value = 'foo';
Expand All @@ -134,7 +136,7 @@ describe('MockDirective:Attribute', () => {
});

it('does something on an emit of the child directive', () => {
const fixture = MockRender(TestedComponent);
const fixture = MockRender(MyComponent);
const component = fixture.point.componentInstance;

// The same as
Expand All @@ -148,7 +150,7 @@ describe('MockDirective:Attribute', () => {
);

// Again, let's pretend DependencyDirective has an output called
// 'someOutput'. TestedComponent listens on the output via
// 'someOutput'. MyComponent listens on the output via
// `(someOutput)="trigger()"`.
// Let's install a spy and trigger the output.
ngMocks.stubMember(
Expand Down Expand Up @@ -184,7 +186,8 @@ describe('MockDirective:Structural', () => {
// Usually a developer knows the context and can render it
// manually with proper setup.
beforeEach(() => {
return MockBuilder(TargetComponent, TargetModule).mock(
// DependencyDirective is a declaration in ItsModule.
return MockBuilder(TargetComponent, ItsModule).mock(
DependencyDirective,
{
// render: true, // <-- a flag to render the directive by default
Expand Down
3 changes: 2 additions & 1 deletion docs/articles/api/MockInstance.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ describe('MockInstance', () => {
// A normal setup of the TestBed, TargetComponent will be replaced
// with its mock object.
// Do not forget to return the promise of MockBuilder.
beforeEach(() => MockBuilder(RealComponent).mock(ChildComponent));
// ChildComponent is declaration of ItsModule.
beforeEach(() => MockBuilder(RealComponent, ItsModule));

beforeEach(() => {
// Because TargetComponent is replaced with its mock object,
Expand Down
7 changes: 4 additions & 3 deletions docs/articles/api/MockModule.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,12 @@ Please, pay attention to comments in the code.
```ts title="https://github.com/ike18t/ng-mocks/blob/master/examples/MockModule/test.spec.ts"
describe('MockModule', () => {
beforeEach(() => {
return MockBuilder(TestedComponent).mock(DependencyModule);
// DependencyModule is an import of ItsModule.
return MockBuilder(MyComponent, ItsModule);
});

it('renders TestedComponent with its dependencies', () => {
const fixture = MockRender(TestedComponent);
it('renders MyComponent with its dependencies', () => {
const fixture = MockRender(MyComponent);
const component = fixture.point.componentInstance;

expect(component).toBeTruthy();
Expand Down
13 changes: 8 additions & 5 deletions docs/articles/api/MockPipe.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ and call [`MockRender`](MockRender.md):
```ts
describe('Test', () => {
beforeEach(() => {
return MockBuilder(TargetComponent)
return MockBuilder(TargetComponent, ItsModule)
// DependencyPipe is a declaration in ItsModule
.mock(DependencyPipe, value => `mock:${value}`);
});

Expand Down Expand Up @@ -115,10 +116,12 @@ describe('MockPipe', () => {
// const spy = jest.fn().mockImplementation(fakeTransform);

beforeEach(() => {
return MockBuilder(TargetComponent, TargetModule).mock(
DependencyPipe,
spy,
);
return MockBuilder(TargetComponent, ItsModule)
// DependencyPipe is a declaration in ItsModule
.mock(
DependencyPipe,
spy,
);
});

it('transforms values to json', () => {
Expand Down
5 changes: 3 additions & 2 deletions docs/articles/api/MockProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,9 @@ and call [`MockRender`](MockRender.md):
```ts
describe('Test', () => {
beforeEach(() => {
return MockBuilder(TargetComponent)
.mock(DependencyService)
// DependencyService is a provider in ItsModule.
return MockBuilder(TargetComponent, ItsModule)
// ObservableService is a provider in ItsModule, which we need to customize
.mock(ObservableService, {
prop$: EMPTY,
getStream$: () => EMPTY,
Expand Down
Loading

0 comments on commit f37a663

Please sign in to comment.