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.
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.
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.
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:
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
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.