Reputation: 718
I'm using the mongoose module from NestJS so I have my schema and an interface, and in my service I use @InjectModel to inject my model. I do not realize how I can mock the model to inject in my service.
My service looks like this:
@Injectable()
export class AuthenticationService {
constructor(@InjectModel('User') private readonly userModel: Model<User>) {}
async createUser(dto: CreateUserDto): Promise<User> {
const model = new this.userModel(dto);
model.activationToken = this.buildActivationToken();
return await model.save();
}
}
and in my service test, I have this:
const mockMongooseTokens = [
{
provide: getModelToken('User'),
useValue: {},
},
];
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
...mockMongooseTokens,
AuthenticationService,
],
}).compile();
service = module.get<AuthenticationService>(AuthenticationService);
});
But when I run the test I got this error:
TypeError: this.userModel is not a constructor
I would also like to get my model to perform unit tests over it, as is shown in this article
Upvotes: 29
Views: 30861
Reputation: 83
The way I do it is more clean and creates more flexibility in testing. You dont need to use Model.
describe('MyRepo', () => {
let repository: MyRepo
let model: MyModel
beforeAll(async () => {
const module = await Test.createTestingModule({
providers: [
{ provide: MyRepo, useClass: MyRepoImplementation },
{
provide: getModelToken(MyModel.name),
useValue: {
create: jest.fn(),
},
},
],
}).compile()
repository = module.get(MyRepo)
model = module.get(getModelToken(MyModel.name))
it('should call the model', async () => {
const spy = jest.spyOn(model, 'create').mockResolvedValueOnce({})
await repository.create({})
expect(spy).toHaveBeenCalledTimes(1)
})
})
})
Upvotes: 0
Reputation: 98
beforeAll(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [
// THIS IS MOCK FOR OUT TEST-APP, MODULE...
{
provide: getModelToken(User.name),
useValue: {},
},
UserService, // SUPPOSE THESE PROVIDERS ALSO NEED OUR USER-MODEL
HealthService, // SO THEY ARE SIBLINGS FOR OUT USER-MODEL
],
imports: [UserModule],
}) // SO IN THIS PLACE WE MOCK USER-MODEL AGAIN
.overrideProvider(getModelToken(User.name)) // <-----
.useValue({}) // <-----
.compile();
}); Code screenshot
Upvotes: 1
Reputation: 644
I know this post is older but if anyone should get to this question again in the future here is an example of how to setup a mocked model and spy on any underlying query call methods. It took me longer than I wanted to figure this out but here is a full example test that doesn't require any extra factory functions or anything.
import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';
import { Model } from 'mongoose';
// User is my class and UserDocument is my typescript type
// ie. export type UserDocument = User & Document; <-- Mongoose Type
import { User, UserDocument } from './models/user.model';
import { UsersRepository } from './users.repository';
import * as CustomScalars from '@common/graphql/scalars/data.scalar';
describe('UsersRepository', () => {
let mockUserModel: Model<UserDocument>;
let mockRepository: UsersRepository;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getModelToken(User.name),
useValue: Model // <-- Use the Model Class from Mongoose
},
UsersRepository,
...Object.values(CustomScalars),
],
}).compile();
// Make sure to use the correct Document Type for the 'module.get' func
mockUserModel = module.get<Model<UserDocument>>(getModelToken(User.name));
mockRepository = module.get<UsersRepository>(UsersRepository);
});
it('should be defined', () => {
expect(mockRepository).toBeDefined();
});
it('should return a user doc', async () => {
// arrange
const user = new User();
const userID = '12345';
const spy = jest
.spyOn(mockUserModel, 'findById') // <- spy on what you want
.mockResolvedValue(user as UserDocument); // <- Set your resolved value
// act
await mockRepository.findOneById(userID);
// assert
expect(spy).toBeCalled();
});
});
Upvotes: 42
Reputation:
The error message you get is quite explicit: this.userModel
is indeed not a constructor, as you provided an empty object to useValue
. To ensure valid injection, useValue
has to be a subclass of mongoose.Model
. The mongoose github repo itself gives a consistent explanation of the underlying concept (from line 63):
* In Mongoose, the term "Model" refers to subclasses of the `mongoose.Model`
* class. You should not use the `mongoose.Model` class directly. The
* [`mongoose.model()`](./api.html#mongoose_Mongoose-model) and
* [`connection.model()`](./api.html#connection_Connection-model) functions
* create subclasses of `mongoose.Model` as shown below.
In other words, a mongoose Model is a class with several methods that attempt to connect to a database. In our case, the only Model method used is save()
. Mongoose uses the javascript constructor function syntax, the same syntax can be used to write our mock.
The mock should be a constructor function, with a save()
param.
The service test is the following:
beforeEach(async () => {
function mockUserModel(dto: any) {
this.data = dto;
this.save = () => {
return this.data;
};
}
const module = await Test.createTestingModule({
providers: [
AuthenticationService,
{
provide: getModelToken('User'),
useValue: mockUserModel,
},
],
}).compile();
authenticationService = module.get<AuthenticationService>(AuthenticationService);
});
I also did a bit of refactoring, to wrap everything in the beforeEach
block.
The save()
implementation I chose for my tests is a simple identity function, but you can implement it differently, depending on the way you want to assert on the return value of createUser()
.
One problem with this solution is precisely that you assert on the return value of the function, but cannot assert on the number of calls, as save()
is not a jest.fn()
. I could not find a way to use module.get
to access the Model Token outside of the module scope. If anyone finds a way to do it, please let me know.
Another issue is the fact that the instance of userModel
has to be created within the tested class. This is problematic when you want to test findById()
for example, as the model is not instantiated but the method is called on the collection. The workaround consists in adding the new
keyword at the useValue
level:
const module = await Test.createTestingModule({
providers: [
AuthenticationService,
{
provide: getModelToken('User'),
useValue: new mockUserModel(),
},
],
}).compile();
The return await
syntax should not be used, as it raises a ts-lint error (rule: no-return-await). See the related github doc issue.
Upvotes: 29
Reputation: 161
in response to @jbh solution, a way to resolve the problem of not instanciating the class in a call of a method like findById()
is to use static methods, you can use like that
class mockModel {
constructor(public data?: any) {}
save() {
return this.data;
}
static findOne({ _id }) {
return data;
}
}
mockModel.findOne();
More info about static methods: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static
Upvotes: 5