Skip to main content

Internals vs. externals in mock modules

There is an important thing to know when a module should be mocked.

Let's imagine, we have a module and its definition looks like:

@NgModule({
imports: [ExternalModule],
declarations: [
MyComponent,
InternalDirective,
],
exports: [MyComponent],
})
class InternalModule {}

Explanation

There are two declarations in the module: MyComponent and InternalDirective. They can use each other, because they have been declared in the same module.

When we check exports, then we see, that only MyComponent has been exported. It means, that if a module imports InternalModule, there is no way to access InternalDirective directly.

This is fine, if we build an Angular application. InternalDirective is something internal, and we do not need to use it in our application outside of InternalModule. However, in tests we have a different story.

Testing internals

Now, let's imagine we want to cover InternalDirective with tests, and ExternalModule and MyComponent are its dependencies which we would like to replace with mocks.

Because InternalModule has all dependencies, at first glance, it makes sense to mock it:

TestBed.configureTestingModule({
imports: [
MockModule(InternalModule),
],
declarations: [
InternalDirective,
],
});

But, it will not do what we expected, because InternalModule exports only MyComponent, and, therefore, there is no access to ExternalModule in the testing module.

We could add MockModule(ExternalModule) to imports in the testing module, but the code is starting to smell, because ExternalModule has been already imported in the module InternalDirective belongs to, and an additional import of MockModule(ExternalModule) feels wrong.

Seems like, if MockModule exported its imports and declarations, it would solve the issue.

Yes... it was like that in versions before 9, but then another issue appeared, and it belongs to externals (exports).

Testing externals

Now, let's imagine we want to cover MyComponent with tests. The story is the same, ExternalModule and InternalDirective are its dependencies which we would like to replace with mocks.

Because InternalModule has all dependencies, at first glance, it makes sense to mock it:

TestBed.configureTestingModule({
imports: [
MockModule(InternalModule),
],
declarations: [
MyComponent,
],
});

Additionally, to the issue of InternalDirective, which has not been exported, there is another one.

Because MockModule(InternalModule) exports MockComponent(MyComponent), there are two declarations of MyComponent defined in the testing module now. Eventually, it will lead us to the error about declarations of 2 modules.

It means, that in tests for MyComponent where we want to mock dependencies, we cannot use InternalModule at all.

Solution

If you have read quick start, you know it. It can be achieved by MockBuilder or ngMocks.guts.

Both of them solve the issue, but in different ways.

MockBuilder

MockBuilder(InternalDirective, InternalModule) builds a new definition for InternalModule, where InternalDirective has been exported, so InternalDirective has access to all its dependencies as before, and we have access to InternalDirective in the test:

@NgModule({
imports: [
MockModule(ExternalModule),
],
declarations: [
MockComponent(MyComponent),
InternalDirective,
],
exports: [
MockComponent(MyComponent),
InternalDirective,
],
})
class MockInternalModule {}

TestBed.configureTestingModule({
imports: [
MockInternalModule,
],
});

With MockBuilder, we can change export behavior when we need it, it can be achieved with export and exportAll flags.

ngMocks.guts

ngMocks.guts(InternalDirective, InternalModule) simply mocks guts of InternalModule, so the definition of a testing module looks like:

TestBed.configureTestingModule({
imports: [
MockModule(ExternalModule),
],
declarations: [
MockComponent(MyComponent),
InternalDirective,
],
});