Calvintwr
Calvintwr

Reputation: 8798

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.

Upvotes: 4

Views: 3733

Answers (3)

Krzysiek
Krzysiek

Reputation: 778

I also struggled with this a lot now - and most of the answer didn't work (I didn't try mocking the whole node_module as I strongly believed in simpler solution). What finally worked for me was as simple as adding those 3 lines before my tests:

import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';

AmqpConnection.prototype.init = jest.fn();
AmqpConnection.prototype.close = jest.fn();

describe('AppController (e2e)', () => {

Upvotes: 0

Calvintwr
Calvintwr

Reputation: 8798

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.

Upvotes: 3

Henrique Teixeira
Henrique Teixeira

Reputation: 21

I solve this problem mocking an AmqpConnection like this.

    import { AmqpConnection } from "@nestjs-plus/rabbitmq";
    import { TestingModule, Test } from "@nestjs/testing";
    import { IntegrationQueueService } from "./integration-queue.service";

    describe('IntegrationQueueService', () => {

      type MockType<T> = {
         [P in keyof T]?: jest.Mock<{}>;
      };
  

      const mockFactory: () => MockType<AmqpConnection> = jest.fn(() => ({
         publish: jest.fn(() => AmqpConnection),
      }))

 
      let service: IntegrationQueueService;

      beforeEach(async () => {
         const module: TestingModule = await Test.createTestingModule({
            providers: [
                IntegrationQueueService,
                {
                    provide: AmqpConnection,
                    useFactory: mockFactory,
                },
            ],

         })
            .compile();

        service = module.get<IntegrationQueueService> (IntegrationQueueService);
    });


    it('should be defined', () => {
        expect(service).toBeDefined();
    });

})

Upvotes: 2

Related Questions