Search code examples
jestjsrabbitmqnestjs

NestJS Mock RabbitMQ in Jest


I have an AppModule file as follows:

import { Module } from '@nestjs/common'
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'

@Module({
    imports: [
        RabbitMQModule.forRoot(RabbitMQModule, {
            exchanges: [
                {
                    name: 'my_rabbit',
                    type: 'direct',
                },
            ],
            uri: process.env.RABBITMQ_URI,
            connectionInitOptions: { wait: true },
        }),
    ],
})
export class AppModule {}

I have tried to mock rabbitmq using @golevelup/nestjs-rabbitmq like this:

import { Module } from '@nestjs/common'
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'

beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            imports: [
                AppModule
            ],
        })
            .overrideProvider(AmqpConnection)
            .useValue(createMock<AmqpConnection>())
            .compile()
    })

This is giving me error:

[Nest] 2745  - 24/07/2022, 17:02:54   ERROR [AmqpConnection] Disconnected from RabbitMQ broker (default)
Error: connect ECONNREFUSED 127.0.0.1:5672

If i mock the whole rabbitmq module like:

jest.mock('@golevelup/nestjs-rabbitmq')

I will get errors like:

Nest cannot create the AppModule instance.
    The module at index [0] of the AppModule "imports" array is undefined.

Has anyone successfully mocked RabbitMQ? Please assist if possible.


Solution

  • The main issue is that AppModule has the RabbitMQModule, which is trying to connect. overrideProvider does not prevent the RabbitMQModule within the AppModule from instantiating, and hence the error.

    There are a few ways to solve this.

    Option 1: Re-create the module

    The simplest way is to not import AppModule, and re-create the module with whatever imports/providers it has. In this case, there's only RabbitMQModule. It returns a few providers, but typically you only need to provide AmqpConnection. So for this, we only needed to provide a mock like this:

    import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'
    import { mock } from 'jest-mock-extended'
    
    beforeEach(async () => {
        const module = await Test.createTestingModule({
            imports: [],
            providers: [
                { provide: AmqpConnection, useValue: mock<AmqpConnection>() }
        })
        .compile()
    })
    

    However, in most instances, a module can grow to have a lot of imports and providers. Re-constructing it is tedious, and you want to be able to just import it, and write __mocks__ to allow it to run in the test environment.

    Option 2: Write a node_modules mock

    You can mock node modules in jest by writing the a manual mock (see https://jestjs.io/docs/manual-mocks).

    However, for NestJS modules, it usually very troublesome as you need to read the source code and "re-construct" the Nest module. Sometimes the source code is not straight forward.

    In this case, the @golevelup/nestjs-rabbitmq mock looks like this:

    Simple mock

    File: src/__mocks__/@golevelup/nestjs-rabbitmq.ts

    (Note: The jest docs said that __mocks__ should be at the same level with node_modules. But that didn't work for me.)

    import { mock } from 'jest-mock-extended'
    
    // create a deeply mocked module
    const rmq = jest.createMockFromModule<typeof import('@golevelup/nestjs-rabbitmq')>(
        '@golevelup/nestjs-rabbitmq',
    )
    
    // all the mocked methods from #createMockFromModule will return undefined
    // but in this case, #forRoot needs to return mocked providers
    // specifically AmqpConnection, and this is how it is done:
    rmq.RabbitMQModule.forRoot = jest.fn(() => ({
        module: rmq.RabbitMQModule,
        providers: [
            {
                provide: rmq.AmqpConnection,
                useValue: mock<typeof rmq.AmqpConnection>(),
            },
        ],
        exports: [rmq.AmqpConnection],
    }))
    
    module.exports = rmq
    
    

    In-memory instances / Testcontainers

    Sometimes you may want to spin up an in-memory instance, or use testcontainer, especially for e2e:

    File src/__mocks__/@golevelup/nestjs-rabbitmq.ts

    import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'
    import { mock } from 'jest-mock-extended'
    import { GenericContainer } from 'testcontainers'
    
    const rmq = jest.createMockFromModule<typeof import('@golevelup/nestjs-rabbitmq')>(
        '@golevelup/nestjs-rabbitmq',
    )
    
    rmq.RabbitMQModule.forRoot = jest.fn(() => ({
        module: rmq.RabbitMQModule,
        providers: [
            {
                provide: rmq.AmqpConnection,
                useFactory: async () => {
                    const RABBITMQ_DEFAULT_USER = 'RABBITMQ_DEFAULT_USER'
                    const RABBITMQ_DEFAULT_PASS = 'RABBITMQ_DEFAULT_PASS'
                    const PORT = 5672
    
                    const rmqContainer = new GenericContainer('rabbitmq:3.11.6-alpine')
                        .withEnvironment({
                            RABBITMQ_DEFAULT_USER,
                            RABBITMQ_DEFAULT_PASS,
                        })
                        .withExposedPorts(PORT)
    
                    const rmqInstance = await rmqContainer.start()
                    const port = rmqInstance.getMappedPort(PORT)
    
                    return new AmqpConnection({
                        uri: `amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@localhost:${port}`,
                    })
                },
            },
        ],
        exports: [rmq.AmqpConnection],
    }))
    
    module.exports = rmq
    

    The same concept can be used to write mocks for stuff like TypeORM, Mongo, Redis etc.