Recently I wrote an article about the Dependency Inversion Principle in Angular and today I would like to follow up a bit on that one and show the advantage of using it.

Angular CLI projects come with a testing framework and automatically generated test files, however, it is still quite a lot of work to set up those tests for services or components which requires multiple dependencies. In this article, I am going to show you a trick on how to take advantage of already existing module configuration and avoid setting up your (test)bed all the time!

Extracting module definition

The very first step in the process is the extraction of the module definition out of the @NgModule decorator and defining it as a constant. I also recommend creating factories or tokens for services and objects which are usually hard to mock, e.g. localStorage.

export function getLocalStorage(): Storage {
  return localStorage;
}

export const coreModuleDef: NgModule = {
  imports: [
    BrowserModule,
    CommonModule,
    HttpClientModule,
    RouterModule,
    NoopAnimationsModule,
    ...
  ],
  declarations: [],
  providers: [
    {
      provide: IAuthService,
      useClass: AuthService
    },
    {
      provide: ILocalStorageService,
      useFactory: getLocalStorage
    },
    ...
  ],
  exports: [],
  schemas: []
};

@NgModule(coreModuleDef)
export class CoreModule {
  constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error('CoreModule is already loaded. Import it in the AppModule only.');
    }
  }
}

Creating shared TestBed

Once we have our module definition we can create a testing module which will contain a factory for the creation of the TestBed. After we have configured it with the default definition, we can call configureTestingModule one more time and mock the services or modules we might need to.

export const configureCoreTestingModule = (): TestBedStatic =>
  TestBed.configureTestingModule(coreModuleDef).configureTestingModule({
    imports: [RouterTestingModule.withRoutes([]), HttpClientTestingModule],
    providers: [
      {
        provide: IAuthService,
        useClass: AuthServiceStub
      },
      {
        provide: ILocalStorageService,
        useFactory: (): jasmine.SpyObj<ILocalStorageService> => localStorageServiceSpyFactory()
      }
    ]
  });

This also works perfectly with the module structure within your application, because in each module we can create a new testing module and easily chain them with already existing ones.

export const configureSharedTestingModule = (): TestBedStatic =>
  configureCoreTestingModule().configureTestingModule(sharedModuleDef);

Using a testing module

Finally, when we have our testing module defined, we can import them and use it instead of the manual setup or copy-paste approach. And the best thing? You can always call configureTestingModule one more time and make your test group even more customized.

describe('IAuthService', () => {
  let service: IAuthService;

  beforeEach(() => {
    configureCoreTestingModule().configureTestingModule({
         ...any further overrides
    });

    service = TestBed.inject(IAuthService);
  });
});

Conclusion

IMO it is hard to say whether this is a common approach for unit testing since you write much more non-isolated tests, however, overall it saves me a lot of time and gives a lot more confidence in a tested environment.

So, how will you make up your testbed tomorrow? Let me know in the comments 🙂

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *