2

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
    }
}
Simon Corcos
  • 962
  • 14
  • 31
  • It's unclear what you're testing, because the question doesn't contain GraphqlModule or Graphql. *I'm trying to test code that's in one of my module's constructor* - it isn't listed. – Estus Flask Dec 08 '17 at 03:46
  • @estus The module is listed in the `Testbed`'s import configuration. This test is part of a test suite that test different features of the `GraphqlModule` – Simon Corcos Dec 08 '17 at 15:32
  • Sorry, I put it in the question's body, however, I'm not sure it's relevant to my question though. – Simon Corcos Dec 08 '17 at 17:25
  • It was necessary to understand what's going on, thanks. – Estus Flask Dec 08 '17 at 18:46

1 Answers1

3

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.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Wow, epic answer. Thanks! – Simon Corcos Dec 09 '17 at 15:22
  • 1
    @Estus my test is failing due to (@Optional() @SkipSelf() parentModule: GraphqlModule) getting error Property name expected type of string but got null i am using ngMock. when i remove parentModule property from module constructor it work fine – dev verma Mar 22 '22 at 05:33