Reputation: 1965
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
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 import
s 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
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.
See this answer.
Upvotes: 7