Catch the highlights of GraphQLConf 2023! Click for recordings. Or check out our recap blog post.
Docs
Essentials
Testing

Testing

GraphQL Modules provides a set of utilities for testing your modules and also for more granular testing of module's smaller units, like providers and middlewares.

To access the testing utilities, import testkit object from graphql-modules package:

import { testkit } from 'graphql-modules'

The testkit object and its API will grow over time, we expect to implement more and more useful features in upcoming releases.

⚠️

GraphQL Modules depend on Reflect API for reflection and for defining dependencies between DI parts, please import reflect-metadata in every test file or setup your testing framework to import it somewhere globally.

💡

The Jest framework is used in all examples but its API is very similar to other testing frameworks.

Testing application

When it comes to integration testing of an Application, the best practice is to avoid any significant modifications. That's why in our Test Kit you will find mockApplication(app) function that accepts the original Application and lets you modify the Modules and application-level providers.

The testkit.mockApplication() resolves a MockedApplication object that extends your original Application with few useful methods.

Replacing a Module

One of those methods is replaceModule(). In combination with testkit.mockModule(), it allows you to modify a module and overwrite its providers.

import 'reflect-metadata'
import { testkit } from 'graphql-modules'
import { application } from './application'
import { myModule, ENVIRONMENT } from './my-module'
 
test('ing', () => {
  const app = testkit.mockApplication(application).replaceModule(
    testkit.mockModule(myModule, {
      providers: [
        {
          provide: ENVIRONMENT,
          useValue: 'testing'
        }
      ]
    })
  )
 
  expect(app.schema.getQueryType()).toBeDefined()
})

In the example above we modified the original application by setting testing ENVIRONMENT in myModule. We used testkit.mockApplication, replaceModule and testkit.mockModule together.

Overwriting Application Providers

Now let's talk about addProviders() function. It allows you to overwrite application-level providers.

⚠️

In GraphQL Modules, always the last provider wins. What does it mean? When you pass a list of providers and two of them try to provide the same token, only the last one counts.

With this in mind, it's easy to overwrite providers, just put them at the end of the list. This is exactly how addProviders() works.

import 'reflect-metadata'
import { testkit } from 'graphql-modules'
import { application, ENVIRONMENT } from './application'
 
test('ing', () => {
  const app = testkit.mockApplication(application).addProviders([
    {
      provide: ENVIRONMENT,
      useValue: 'testing'
    }
  ])
 
  expect(app.schema.getQueryType()).toBeDefined()
})

In the example above we modified the original application by setting the ENVIRONMENT to testing. We used testkit.mockApplication and addProviders.

Testing Modules

In general, the idea behind testing a module is to create an application out of it. Instead of using createApplication(), our Test Kit provides a testModule() function. It calls createApplication under the hood but comes with a set of helpful options.

The easiest way to test a module would be to write the following code:

import 'reflect-metadata'
import { testkit } from 'graphql-modules'
import { myModule } from './my-module'
 
test('ing', () => {
  const app = testkit.testModule(myModule)
 
  expect(app.schema.getQueryType()).toBeDefined()
})

Probably none of your modules will work with testkit.testModule out of the box. That's because the module and especially its type definitions depend on types from another module or Dependency Injection is incomplete.

Turning Type Extensions into Definitions

In case your module extends the Query type or other types and does not depend on other modules, transforming extend type X into type X should do the work.

To turn on the transformation please enable replaceExtensions flag in options - testModule(mod, options).

my-module.spec.ts
import 'reflect-metadata'
import { testkit } from 'graphql-modules'
import { myModule } from './my-module'
 
test('ing', () => {
  const app = testkit.testModule(myModule, {
    replaceExtensions: true
  })
 
  expect(app.schema.getQueryType()).toBeDefined()
})

The replaceExtensions flag turned your module's schema into a valid and executable schema.

Extending Module's Schema

In case your module extends the Query type or other types and using replaceExtensions flag won't work, testkit.testModule allows defining additional typeDefs and resolvers.

my-module.spec.ts
import 'reflect-metadata'
import { testkit, gql } from 'graphql-modules'
import { myModule } from './my-module'
 
test('ing', () => {
  const app = testkit.testModule(myModule, {
    typeDefs: gql`
      type User {
        name: String
      }
    `,
    resolvers: {
      Query: {
        me() {
          return {
            name: 'Bob'
          }
        }
      }
    },
    replaceExtensions: true
  })
 
  expect(app.schema.getQueryType()).toBeDefined()
})

In the example above, the replaceExtensions transformed extend type Query into type Query and additional typeDefs and resolvers were provided. As you can see, these two approaches of extending the schema can be used together and simplified testing a lot.

Inherit typeDefs from Other Modules

The testkit.testModule allows to add type definitions from other modules using inheritTypeDefs option. Thanks to tree-shaking performed by inheritTypeDefs, your tested module includes only the relevant types.

my-module.spec.ts
import 'reflect-metadata'
import { testkit } from 'graphql-modules'
import { myModule } from './my-module'
import { otherModule } from './other-module'
 
test('ing', () => {
  const app = testkit.testModule(myModule, {
    inheritTypeDefs: [otherModule]
  })
 
  expect(app.schema.getTypes().Message).not.toBeDefined()
})

In the example above, the tested module inherits User type from other-module and thanks to tree-shaking the Message type and Query.messages and Query.users are not in the schema.

The inheritTypeDefs is useful when you don't want to define types manually but rather use existing definitions.

Importing Other Modules

There's a chance you may want to include other modules in the tested application.

The testkit.testModule() lets you do it with modules options.

It accepts an array of modules and has the same effect as createAppliction({ modules: [...] }).

import 'reflect-metadata'
import { testkit } from 'graphql-modules'
import { myModule } from './my-module'
import { otherModule } from './other-module'
 
test('ing', () => {
  const app = testkit.testModule(myModule, {
    modules: [otherModule]
  })
})

Providers and Middlewares

The testkit.testModule() accepts providers and middlewares. They both end up on the application level.

More on that in next two sections: Testing Providers, Testing Middlewares.

import 'reflect-metadata'
import { testkit } from 'graphql-modules'
import { myModule } from './my-module'
import { myMiddleware } from './my-middleware'
import { MyProvider } from './my-provider'
 
test('ing', () => {
  const app = testkit.testModule(myModule, {
    providers: [
      {
        provide: MyProvider,
        useValue: {}
      }
    ],
    middlewares: {
      Query: {
        '*': [myMiddleware]
      }
    }
  })
})

Executing Operations

As explained earlier, testing a module means creating an application. There's a reason behind it.

The testkit.testModule calls createApplication internally, this way we keep exactly same logic as your GraphQL API operates on.

We highly recommend testing the entire execution flow instead of focusing on individual pieces. That's why testkit ships with execute helper.

import 'reflect-metadata'
import { testkit, gql } from 'graphql-modules'
import { myModule, UsersProvider } from './my-module'
 
test('ing', () => {
  const app = testkit.testModule(myModule, {
    providers: [
      {
        provide: UsersProvider,
        useValue: {
          getCurrentUser() {
            return {
              name: 'Bob'
            }
          }
        }
      }
    ]
  })
 
  const result = testkit.execute(app, {
    document: gql`
      {
        me {
          name
        }
      }
    `
  })
 
  expect(result.data.me.name).toEqual('Bob')
})

The testkit.execute doesn't help a lot but without it, you would have to call app.createExecution, extract app.schema and put it all together to execute an operation. Two lines of code you don't need to worry about!

Testing Providers

There are two ways of testing providers, using testkit.testModule and testkit.testInjector. The former was already covered in one of the previous sections.

The testkit.testInjector lets you play with providers purely on the Injector level or in other words in total isolation. There are no modules or application which means no hierarchy and layering.

testInjector

logger.spec.ts
import 'reflect-metadata'
import { testkit } from 'graphql-modules'
import { Logger, LoggerTransport } from './logger'
 
test('ing', () => {
  const transportLogSpy = jest.fn()
  const injector = testkit.testInjector([
    Logger,
    {
      provide: LoggerTransport,
      useValue: {
        log: transportLogSpy
      }
    }
  ])
 
  const logger = injector.get(Logger)
 
  logger.log('hello')
 
  expect(transportLogSpy).toHaveBeenCalledWith('hello')
})

In the example above, thanks to the abstraction, we were able to provide a custom transport layer for our Logger. This way we know that every call of Logger.log(msg) passes the msg to the ILoggerTransport.log(msg).

readProviderOptions

From the performance perspective, it's important to make sure all singleton providers are in fact singletons and testkit.readProviderOptions helps with that.

Let's use again the Logger example. This time we want to check the scope of the provider.

import 'reflect-metadata'
import { testkit, Scope } from 'graphql-modules'
import { Logger } from './logger'
 
test('ing', () => {
  const options = testkit.readProviderOptions(Logger)
 
  expect(options.scope).toEqual(Scope.Singleton)
})

The testkit.readProviderOptions returns the ProviderOptions object with scope, global and executionContextIn properties.

Testing Middlewares

Testing a middleware requires a GraphQL Schema. Using testkit.testModule fits perfectly in this scenario.

Depending on the complexity of a middleware function, you may want to use or mock different pieces of GraphQL Modules. We tried to cover the most common scenario in the example below.

my-module.spec.ts
import 'reflect-metadata'
import { testkit, gql } from 'graphql-modules'
import { myModule } from './my-module'
 
test('ing', () => {
  const app = testkit.testModule(myModule)
 
  const result = await testkit.execute(app, {
    document: gql`
      {
        me {
          name
        }
      }
    `,
    contextValue: {
      isLoggedIn: false
    }
  })
 
  expect(result.errors).toHaveLength(1)
  expect(result.data.me).toBeNull()
})

The authMiddleware prevents private data from leaking out by using AuthProvider.isLoggedIn() method. The isLoggedIn flag is provided via context of the GraphQL operation. This is done for simplicity of the example but usually, you would validate a visitor's session or something similar.

Because in contextValue we marked the incoming request as not authenticated ({ isLoggedIn: false }), we expect the GraphQL Operation to fail before resolving the original Query.me field.

💡

If you wish to see more testing utilities or have some ideas, reach out to us.