Search code examples
angulardependency-injectionfrontendkarma-jasmineangular7

Angular 7 testing with inheritance and global injector


I'm starting a new project in Angular 7 and have a base component to center some of my global variables and services. As the project can grow quite big, I chose this approach to avoid a massive amount of imports at the beginning of each component and also at super() calls.

To do this, I implemented the approach described here, where an external class stores a dependency injector to be used in the base component. As discussed here, a modification had to be made to comply with Angular 7 (the injector had to be initialized at the AppModule). (See plunker below)

This works fine for running the project, the problem comes when I try to run unit tests (using karma-jasmine). Since the AppInjector is initialized in the module, the testing run does not initialize it. Then, since my base component tries to call a function of the injector, it breaks.

If anyone has any solution for this, either by modifying the test structure or the AppInjector itself, it would be greatly appreciated.

Here is a plunker with all the code involved. The example is a global notification component using primeng toast module and a global notification service in the base component. (I couldn't get the plunker to work properly, some sort of MIME type error, but the code is all there)

https://next.plnkr.co/edit/7v5nB8cORWsnpPAL?preview

When I run ng test, this is the error I get:

Chrome 71.0.3578 (Windows 10.0.0) AppComponent should create the app FAILED
        TypeError: Cannot read property 'get' of undefined
            at NotificationsComponent.BaseComponent (http://localhost:9876/src/app/components/base.component.ts?:22:50)
            at new NotificationsComponent (http://localhost:9876/src/app/components/utils/notifications/notifications.component.ts?:17:13)
            at createClass (http://localhost:9876/node_modules/@angular/core/fesm5/core.js?:20716:1)
            at createDirectiveInstance (http://localhost:9876/node_modules/@angular/core/fesm5/core.js?:20595:1)
            at createViewNodes (http://localhost:9876/node_modules/@angular/core/fesm5/core.js?:21821:1)
            at callViewAction (http://localhost:9876/node_modules/@angular/core/fesm5/core.js?:22137:1)
            at execComponentViewsAction (http://localhost:9876/node_modules/@angular/core/fesm5/core.js?:22056:1)
            at createViewNodes (http://localhost:9876/node_modules/@angular/core/fesm5/core.js?:21849:1)
            at createRootView (http://localhost:9876/node_modules/@angular/core/fesm5/core.js?:21735:1)
            at callWithDebugContext (http://localhost:9876/node_modules/@angular/core/fesm5/core.js?:22767:1)
        Error: Uncaught (in promise): TypeError: Cannot read property 'get' of undefined
            at resolvePromise (http://localhost:9876/node_modules/zone.js/dist/zone.js?:831:1)
            at http://localhost:9876/node_modules/zone.js/dist/zone.js?:896:1
            at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (http://localhost:9876/node_modules/zone.js/dist/zone.js?:423:1)
            at AsyncTestZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.AsyncTestZoneSpec.onInvokeTask (http://localhost:9876/node_modules/zone.js/dist/zone-testing.js?:698:1)
            at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvokeTask (http://localhost:9876/node_modules/zone.js/dist/zone-testing.js?:317:1)
            at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (http://localhost:9876/node_modules/zone.js/dist/zone.js?:422:1)
            at Zone../node_modules/zone.js/dist/zone.js.Zone.runTask (http://localhost:9876/node_modules/zone.js/dist/zone.js?:195:1)
            at drainMicroTaskQueue (http://localhost:9876/node_modules/zone.js/dist/zone.js?:601:1)
        Expected undefined to be truthy.
            at UserContext.<anonymous> (http://localhost:9876/src/app/app.component.spec.ts?:35:21)
            at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (http://localhost:9876/node_modules/zone.js/dist/zone.js?:391:1)
            at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/node_modules/zone.js/dist/zone-testing.js?:289:1)

Solution

  • In tests you can get the injector from the TestBed. So you can initialize your AppInjector in the beforeEach like all other things. Just do it before the createComponent call so it is ready.

    Here is the changed code from your Plunker sample:

    beforeEach(async(() => {
            TestBed.configureTestingModule({
                imports: [
                    RouterTestingModule,
                    ToastModule
                ],
                declarations: [
                    AppComponent,
                    NotificationsComponent
                ]
            }).compileComponents().then(() => {
                AppInjector.setInjector(TestBed.get(Injector));
                fixture = TestBed.createComponent(AppComponent);
                app = fixture.debugElement.componentInstance;
            });
        }));