Simon Corcos
Simon Corcos

Reputation: 1022

Angular 5 testing code in module constructor

I'm trying to test code that's in one of my module's constructor. Basically, the module (named GraphqlModule) configures a service (named Graphql) and provides it. The configuration happens in the module's constructor.

Here's the code I use to test the module.

it('should use the GraphqlConfigs and ServerConfigs', (done: DoneFn) => {

    // Adding spies to Config classes
    let serverConfigs = new ServerConfigs();
    let serverDomainSpy = spyOnProperty(serverConfigs, 'ServerDomain', 'get').and.callThrough();
    let serverPortSpy = spyOnProperty(serverConfigs, 'ServerPort', 'get').and.callThrough();

    let gqlConfigs = new GraphqlConfigs();
    let protocolSpy = spyOnProperty(gqlConfigs, 'EndpointProtocol', 'get').and.callThrough();
    let endpointNameSpy = spyOnProperty(gqlConfigs, 'EndpointName', 'get').and.callThrough();

    TestBed.configureTestingModule({
        imports: [ GraphqlModule ],
        providers: [
            {provide: ServerConfigs, useValue: serverConfigs}, // Replacing real config classes with the ones spied on.
            {provide: GraphqlConfigs, useValue: gqlConfigs}
        ]

    }).compileComponents().then(() => {

        // This line seems to make Angular instantiate GraphqlModule
        const graphql = TestBed.get(Graphql) as Graphql;

        expect(serverDomainSpy.calls.count()).toBe(1, 'ServerConfigs.ServerDomain was not used.');
        expect(serverPortSpy.calls.count()).toBe(1,  'ServerConfigs.ServerPort was not used.');

        expect(protocolSpy.calls.count()).toBe(1, 'GraphqlConfigs.EndpointProtocol was not used.');
        expect(endpointNameSpy.calls.count()).toBe(1,  'GraphqlConfigs.EndpointName was not used.');

        done();
    });
});

As is, the test passes and works, but if I dont use the following (useless) line const graphql = TestBed.get(Graphql) as Graphql; the GraphqlModule gets instantiated after the test has been executed, which makes the test fail.

Since it's GraphqlModule that provides the Graphql service, I understand that there's some lazy loading algorithm in Angular that's triggered when I do TestBed.get(Graphql). That's fine... my question is, is there a way to make my module load in a more explicit way?

Here's the GraphqlModule class definition:

imports...

@NgModule({
    imports: [
        CommonModule,
        HttpClientModule,
        ApolloModule,
        HttpLinkModule
    ],
    declarations: [],

    providers: [
        Graphql, // Is an alias for Apollo
        GraphqlConfigs
    ]
})
export class GraphqlModule {

    constructor(
        @Optional() @SkipSelf() parentModule: GraphqlModule,
        graphql: Graphql,
        httpLink: HttpLink,
        serverConfigs: ServerConfigs,
        graphqlConfigs: GraphqlConfigs
    ) {

        // Making sure this is not imported twice.
        // https://angular.io/guide/ngmodule#prevent-reimport-of-the-coremodule
        if (parentModule) {
            throw new Error(
                'GraphqlModule is already loaded. Import it in the '+CoreModule.name+' only.');
        }

        // Gql setup:


        const gqlHttpLink = httpLink.create({
            uri: GraphqlModule.buildEndpointUrl(serverConfigs, graphqlConfigs)
        });

        graphql.create({
            link: gqlHttpLink,
            cache: new InMemoryCache(),
        });
    }

    private static buildEndpointUrl(serverConfigs: ServerConfigs, graphqlConfigs: GraphqlConfigs): string {

        return graphqlConfigs.EndpointProtocol +                            // eg. http://
            serverConfigs.ServerDomain+":"+serverConfigs.ServerPort+'/' +   // eg. example.com:80/
            graphqlConfigs.EndpointName;                                    // eg. graphql
    }
}

Upvotes: 2

Views: 4163

Answers (1)

Estus Flask
Estus Flask

Reputation: 222309

Graphql is already instantiated in GraphqlModule constructor for root injector, and GraphqlModule is eagerly instantiated when it's specified in TestBed imports. There's no lazy loading involved.

As it's explained in this answer, TestBed injector is instantiated on first inject callback call, or TestBed.get, or TestBed.createComponent call. The injector doesn't exist until the first injection, so don't any module or provider instances. Since almost all TestBed tests perform at least one of these calls in it or beforeEach, this issue usually never appears.

Since graphql instance isn't needed in this test, in order for the test to pass it can be just:

TestBed.get(Injector);

Or:

TestBed.get(TestBed);

Also, .compileComponents().then(() => { ... }) and done() are unnecessary, the test doesn't involve components and is synchronous.

The fact that the injector needs to be instantiated manually suggests that TestBed isn't required for this test, although it's beneficial because this way GraphqlModule constructor DI annotation can be tested, including the one for parentModule.

Upvotes: 3

Related Questions