Yaron
Yaron

Reputation: 1965

How to jest mock nestjs imports?

I want to write a unit test for my nestjs 'Course' repository service (a service that has dependencies on Mongoose Model and Redis).

courses.repository.ts:

    import { Injectable, HttpException, NotFoundException } from "@nestjs/common";
    import { InjectModel } from "@nestjs/mongoose"
    import { Course } from "../../../../shared/course";
    import { Model } from "mongoose";
    import { RedisService } from 'nestjs-redis';


    @Injectable({}) 
    export class CoursesRepository {

      private redisClient;
      constructor(
        @InjectModel('Course') private courseModel: Model<Course>,
        private readonly redisService: RedisService,
      ) {
        this.redisClient = this.redisService.getClient();

      }


      async findAll(): Promise<Course[]> {
        const courses = await this.redisClient.get('allCourses');
        if (!courses) {
          console.log('return from DB');
          const mongoCourses = await this.courseModel.find();
          await this.redisClient.set('allCourses', JSON.stringify(mongoCourses), 'EX', 20);
          return mongoCourses;
        }

        console.log('return from cache');
        return JSON.parse(courses);
      }
}

The test is initialized this way:

beforeEach(async () => {
  const moduleRef = await Test.createTestingModule({
    imports: [
      MongooseModule.forRoot(MONGO_CONNECTION,  { 
        useNewUrlParser: true,
        useUnifiedTopology: true
      }),
      MongooseModule.forFeature([
        { name: "Course", schema: CoursesSchema },
        { name: "Lesson", schema: LessonsSchema }
      ]),
      RedisModule.register({})
    ],
      controllers: [CoursesController, LessonsController],
      providers: [
         CoursesRepository,
         LessonsRepository
        ],
    }).compile();

  coursesRepository = moduleRef.get<CoursesRepository>(CoursesRepository);
  redisClient = moduleRef.get<RedisModule>(RedisModule);

});

My Course repository service has 2 dependencies - Redis and Mongoose Model (Course). I would like to mock both of them.

If I was mocking a provider I would use that syntax:

providers: [
    {provide: CoursesRepository, useFactory: mockCoursesRepository},
     LessonsRepository
    ],

Can I create a mock Redis service which will be used instead of the an actual Redis service during a test?

How ?

Thanks, Yaron

Upvotes: 9

Views: 22165

Answers (2)

Eric Haynes
Eric Haynes

Reputation: 5786

Note that the accepted answer mocks a provider, which will satisfy the dependencies of your class under test IF it is also supplied from the TestingModule with a provider. However, it's not mocking imports as asked, which can be different. E.g. if you need to import some module which also depends on the mocked value, this will NOT work:

// DOES NOT WORK
const module = await Test.createTestingModule({
  imports: [
    ModuleThatNeedsConfig,
  ],
  providers: [
    { provide: ConfigService, useValue: mockConfig },
    ExampleService, // relies on something from ModuleThatNeedsConfig
  ]
}).compile();

To mock an import, you can use a DynamicModule that exports the mocked values:

const module = await Test.createTestingModule({
  imports: [
    {
      module: class FakeModule {},
      providers: [{ provide: ConfigService, useValue: mockConfig }],
      exports: [ConfigService],
    },
    ModuleThatNeedsConfig,
  ],
  providers: [
    ClassUnderTest,
  ]
}).compile();

As that's a bit verbose, a utility function such as this can be helpful:

export const createMockModule = (providers: Provider[]): DynamicModule => {
  const exports = providers.map((provider) => (provider as any).provide || provider);
  return {
    module: class MockModule {},
    providers,
    exports,
    global: true,
  };
};

Which can then be used like:

const module = await Test.createTestingModule({
  imports: [
    createMockModule([{ provide: ConfigService, useValue: mockConfig }]),
    ModuleThatNeedsConfig,
  ],
  providers: [
    ClassUnderTest,
  ]
}).compile();

This often has the added benefit of letting you import the module for the class under test, rather than having the test bypass the module and provide the value directly.

const module = await Test.createTestingModule({
  imports: [
    createMockModule([{ provide: ConfigService, useValue: mockConfig }]),
    ModuleThatNeedsConfig,
    ModuleUnderTest,
  ],
}).compile();

const service = module.get(ClassUnderTest);

Upvotes: 11

Kim Kern
Kim Kern

Reputation: 60357

You can mock your RedisService just as any other dependency. Since you are really interested in the Redis client and not the service, you have to create an intermediate mock for the service. For mongoose, you need the getModelToken method for getting the correct injection token, see this answer:

const redisClientMockFactory = // ...
const redisServiceMock = {getClient: () => redisClientMockFactory()}

providers: [
  { provide: RedisService, useValue: redisServiceMock },
  { provide: getModelToken('Course'), useFactory: courseModelMockFactory },
  CoursesRepository
],

Please also note, that you probably should not import modules in unit tests (unless it is a testing module). See this answer on a distinction between unit and e2e tests.

How to create mocks?

See this answer.

Upvotes: 7

Related Questions