Understanding mongoose Model
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.
TL;DR
The mock should be a constructor function, with a save()
param.
Writing the mock
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()
.
Limits of this solution
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();
One more thing...
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.