Skip to content

Commit

Permalink
feat: exportAll flag for modules
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Oct 20, 2020
1 parent c1aa4de commit 5f8835c
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 26 deletions.
87 changes: 67 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Or you could use `ngMocks` to mock them out and have the ability to assert on th
* [`MockInstance` in details](#mockinstance)
* [`ngMocks` in details](#ngmocks)

- [Usage with 3rd-party libraries](#usage-with-3rd-party-libraries)
- [Making tests faster](#making-angular-tests-faster)
- [Auto Spy](#auto-spy)

Expand Down Expand Up @@ -362,7 +363,7 @@ describe('MockDirective', () => {
component.value = 'foo';
fixture.detectChanges();

// IMPORTANT: by default structural directives aren't rendered.
// IMPORTANT: by default structural directives are not rendered.
// Because we cannot automatically detect when and which context
// they should be rendered with.
// Usually a developer knows the context and can render it
Expand Down Expand Up @@ -900,7 +901,7 @@ describe('MockBuilder:simple', () => {
// beforeEach(() => TestBed.configureTestingModule({{
// imports: [MockModule(MyModule)],
// }).compileComponents());
// but MyComponent wasn't mocked for the testing purposes
// but MyComponent was not mocked for the testing purposes
// and we can simply pass it to the TestBed.

it('should render content ignoring all dependencies', () => {
Expand Down Expand Up @@ -929,20 +930,18 @@ const ngModule = MockBuilder(MyComponent, MyModule).build();
// The same as code above.
const ngModule = MockBuilder()
.keep(MyComponent, { export: true })
.mock(MyModule)
.mock(MyModule, { exportAll: true })
.build();

// If you don't plan a further customization of ngModule
// then you don't need to call .build().
// If you do not plan a further customization of ngModule
// then you do not need to call .build().
// Simply return result of MockBuilder in beforeEach
beforeEach(() =>
MockBuilder().keep(MyComponent, { export: true }).mock(MyModule)
);
beforeEach(() => MockBuilder(MyComponent, MyModule));
// It is the same as:
beforeEach(() => {
const ngModule = MockBuilder()
.keep(MyComponent, { export: true })
.mock(MyModule)
.mock(MyModule, { exportAll: true })
.build();
TestBed.configureTestingModule(ngModule);
return TestBed.compileComponents();
Expand Down Expand Up @@ -1035,23 +1034,37 @@ const ngModule = MockBuilder(MyComponent, MyModule)
.mock(SomeModule)
.build();

// If we want to test a component, directive or pipe which wasn't
// If we want to test a component, directive or pipe which was not
// exported we should mark it as an 'export'.
// Doesn't matter how deep it is. It will be exported to the level of
// Does not matter how deep it is. It will be exported to the level of
// TestingModule.
const ngModule = MockBuilder(MyComponent, MyModule)
.keep(SomeModuleComponentDirectivePipeProvider1, {
export: true,
})
.build();

// If we want to use declarations of a module which were not
// exported, we should mark the module with the 'exportAll' flag.
// Then all its imports and declarations will be exported.
// If the module is nested, then add `export` beside `exportAll`.
const ngModule = MockBuilder(MyComponent)
.keep(MyModule, {
exportAll: true,
})
.mock(MyNestedModule, {
exportAll: true,
export: true,
})
.build();

// By default all definitions (kept and mocked) are added to the
// TestingModule if they are not dependency of another definition.
// Modules are added as imports to the TestingModule.
// Components, Directive, Pipes are added as declarations to the
// TestingModule.
// Providers and Services are added as providers to the TestingModule.
// If we don't want something to be added to the TestingModule at all
// If we do not want something to be added to the TestingModule at all
// we should mark it as a 'dependency'.
const ngModule = MockBuilder(MyComponent, MyModule)
.keep(SomeModuleComponentDirectivePipeProvider1, {
Expand Down Expand Up @@ -1131,7 +1144,7 @@ The best thing here is that `fixture.point.componentInstance` is typed to the co
If you want, you can specify providers for the render passing them via the 3rd parameter.
It is useful when you want to mock system tokens / services such as `APP_INITIALIZER`, `DOCUMENT` etc.

And don't forget to call `fixture.detectChanges()` and / or `await fixture.whenStable()` to trigger updates.
And do not forget to call `fixture.detectChanges()` and / or `await fixture.whenStable()` to trigger updates.

<details><summary>Click to see <strong>an example how to render a custom template in an Angular tests</strong></summary>
<p>
Expand Down Expand Up @@ -1230,7 +1243,7 @@ describe('MockRender', () => {
> NOTE: it works only for pure mocks without overrides.
> If you provide an own mock via `useValue`
> or like `.mock(MyService, myMock)`
> then `MockInstance` doesn't have an effect.
> then `MockInstance` does not have an effect.
```typescript
MockInstance(MyService, {
Expand Down Expand Up @@ -1309,7 +1322,7 @@ describe('MockInstance', () => {
});
});

// Don't forget to reset MockInstance back.
// Do not forget to reset MockInstance back.
afterEach(MockReset);

it('should render', () => {
Expand Down Expand Up @@ -1416,11 +1429,11 @@ In case if we want to mock methods / properties of a service.

```typescript
// Returns a mocked function / spy of the method. If the method
// hasn't been mocked yet - mocks it.
// has not been mocked yet - mocks it.
const spy: Function = ngMocks.stub(instance, methodName);

// Returns a mocked function / spy of the property. If the property
// hasn't been mocked yet - mocks it.
// has not been mocked yet - mocks it.
const spyGet: Function = ngMocks.stub(instance, propertyName, 'get');
const spySet: Function = ngMocks.stub(instance, propertyName, 'set');

Expand Down Expand Up @@ -1469,6 +1482,40 @@ describe('MockService', () => {

---

## Usage with 3rd-party libraries

`ngMocks` provides flexibility via [`MockBuilder`](#mockbuilder)
that allows developers to use another Angular testing libraries for creation of `TestBed`,
and in the same time mock all dependencies via `ngMocks`.

For example if we use `@ngneat/spectator` and its functions
like `createHostFactory`, `createComponentFactory`, `createDirectiveFactory` and so on,
then to mock everything properly we need:

- exclude the component we want to test
- mock its module
- export all declarations the module has

This means we need `.exclude`, `.mock` and `exportAll` flag.

```typescript
const dependencies = MockBuilder()
.exclude(MyComponent)
.mock(MyModule, {
exportAll: true,
})
.build();

const createComponent = createComponentFactory({
component: MyComponent,
...dependencies,
});
```

Profit.

---

## Making Angular tests faster

There is a `ngMocks.faster` feature that optimizes setup of similar test modules between tests
Expand All @@ -1482,7 +1529,7 @@ the tests will run faster.
describe('performance:correct', () => {
ngMocks.faster(); // <-- add it before

// Doesn't change between tests.
// The TestBed does not change between tests.
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule).keep(TargetService)
);
Expand Down Expand Up @@ -1527,7 +1574,7 @@ describe('beforeEach:mock-instance', () => {
});
});

// Don't forget to reset the spy between runs.
// Do not forget to reset the spy between runs.
afterAll(MockReset);
});
```
Expand All @@ -1551,7 +1598,7 @@ describe('beforeEach:manual-spy', () => {
prop: 123,
};

// Don't forget to reset the spy between runs.
// Do not forget to reset the spy between runs.
beforeEach(() => {
mock.method.calls.reset();
// in case of jest
Expand Down
6 changes: 6 additions & 0 deletions lib/mock-builder/mock-builder-performance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,12 @@ describe('MockBuilderPerformance', () => {

expect(ngModule1.providers?.[0]).not.toBe(ngModule2.providers?.[0]);
});
it('fails on different exportAll configDef', () => {
const ngModule1 = MockBuilder().keep(Target1Module, { exportAll: true }).build();
const ngModule2 = MockBuilder().keep(Target1Module, { exportAll: false }).build();

expect(ngModule1.providers?.[0]).not.toBe(ngModule2.providers?.[0]);
});
it('accepts the same render configDef', () => {
const render = {};
const ngModule1 = MockBuilder().keep(Target1Module, { render }).build();
Expand Down
3 changes: 3 additions & 0 deletions lib/mock-builder/mock-builder-performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@ export class MockBuilderPerformance extends MockBuilderPromise {
if (configPrototype.export !== configThis.export) {
return false;
}
if (configPrototype.exportAll !== configThis.exportAll) {
return false;
}
if (!this.equalRender(configPrototype.render, configThis.render)) {
return false;
}
Expand Down
10 changes: 9 additions & 1 deletion lib/mock-builder/mock-builder-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export interface IMockBuilderConfigAll {
export?: boolean; // will be forced for export in its module.
}

export interface IMockBuilderConfigModule {
exportAll?: boolean; // exports all declarations and imports.
}

export interface IMockBuilderConfigComponent {
render?: {
[blockName: string]:
Expand All @@ -51,7 +55,11 @@ export interface IMockBuilderConfigDirective {
};
}

export type IMockBuilderConfig = IMockBuilderConfigAll | IMockBuilderConfigComponent | IMockBuilderConfigDirective;
export type IMockBuilderConfig =
| IMockBuilderConfigAll
| IMockBuilderConfigModule
| IMockBuilderConfigComponent
| IMockBuilderConfigDirective;

const defaultMock = {}; // simulating Symbol

Expand Down
4 changes: 3 additions & 1 deletion lib/mock-builder/mock-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ export function MockBuilder(keepDeclaration?: Type<any>, itsModuleToMock?: Type<
});
}
if (itsModuleToMock) {
instance.mock(itsModuleToMock);
instance.mock(itsModuleToMock, {
exportAll: true,
});
}
return instance;
}
10 changes: 6 additions & 4 deletions lib/mock-module/mock-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export function MockNgDef(ngModuleDef: NgModule, ngModule?: Type<any>): [boolean
// Unfortunately, in this case tests won't fail when a module has missed exports.
// MockBuilder doesn't have have this issue.
for (const def of flatten([imports || [], declarations || []])) {
const moduleConfig = ngMocksUniverse.config.get(ngModule) || {};
const instance = isNgModuleDefWithProviders(def) ? def.ngModule : def;
const mockedDef = resolve(instance);
if (!mockedDef) {
Expand All @@ -290,14 +291,15 @@ export function MockNgDef(ngModuleDef: NgModule, ngModule?: Type<any>): [boolean
// If we export a declaration, then we have to export its module too.
const config = ngMocksUniverse.config.get(instance) || {};
if (config.export && ngModule) {
const moduleConfig = ngMocksUniverse.config.get(ngModule) || {};
if (!moduleConfig.export) {
moduleConfig.export = true;
ngMocksUniverse.config.set(ngModule, moduleConfig);
ngMocksUniverse.config.set(ngModule, {
...moduleConfig,
export: true,
});
}
}

if (correctExports && !config.export) {
if (correctExports && !config.export && !moduleConfig.exportAll) {
continue;
}
if (mockedModuleDef.exports && mockedModuleDef.exports.indexOf(mockedDef) !== -1) {
Expand Down
83 changes: 83 additions & 0 deletions tests/export-all/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { CommonModule } from '@angular/common';
import { Component, Input, NgModule, Pipe, PipeTransform } from '@angular/core';
import { async, TestBed } from '@angular/core/testing';
import { MockBuilder, MockRender } from 'ng-mocks';

@Pipe({
name: 'target',
})
class TargetPipe implements PipeTransform {
protected readonly name = 'pipe';
public transform(value: string): string {
return `${this.name}:${value}`;
}
}

@Component({
selector: 'target',
template: `<ng-container *ngIf="value">{{ value | target }}</ng-container>`,
})
class TargetComponent {
@Input() public readonly value: string | null = null;
}

@NgModule({
declarations: [TargetPipe, TargetComponent],
imports: [CommonModule],
})
class TargetModule {}

// The goal is to get access to declarations of mocked TargetModule
// when TargetComponent is used externally.
describe('export-all:valid', () => {
beforeEach(() => {
const ngModule = MockBuilder()
.mock(TargetModule, {
exportAll: true,
})
.exclude(TargetComponent)
.build();

return TestBed.configureTestingModule({
...ngModule,
declarations: [...ngModule.declarations, TargetComponent],
}).compileComponents();
});

it('renders component', () => {
expect(() => MockRender(TargetComponent)).not.toThrow();
});
});

describe('export-all:no-export', () => {
it('fails on no exportAll due to lack of access to non-exported declarations', () => {
const ngModule = MockBuilder().mock(TargetModule).exclude(TargetComponent).build();
const testBed = TestBed.configureTestingModule({
...ngModule,
declarations: [...ngModule.declarations, TargetComponent],
});

expect(async(() => testBed.compileComponents())).toThrowError(/The pipe 'target' could not be found/);
});
});

describe('export-all:no-exclude', () => {
beforeEach(() => {
const ngModule = MockBuilder()
.mock(TargetModule, {
exportAll: true,
})
.build();

return TestBed.configureTestingModule({
...ngModule,
declarations: [...ngModule.declarations, TargetComponent],
}).compileComponents();
});

it('fails on no exclude due to a conflict in declarations', () => {
expect(() => MockRender(TargetComponent)).toThrowError(
/Conflicting components: MockOfTargetComponent,TargetComponent/
);
});
});

0 comments on commit 5f8835c

Please sign in to comment.