Reputation: 8798
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
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
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.
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.
Upvotes: 3
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