NestJS Jest error: TypeError: Cannot read properties of undefined (reading '[any variable from required config]') - jestjs

While trying to cover out project in unit tests using nest's jest I've bumped into a problem of a testing module not being able to pull variables from config.
Basically, I have an EmailService, I want to test it, I use it as a Provider in my testing module. Naturally, as EmailService takes ConfigService in its constructor to pull some variables from config (that initially come from env) I put ConfigService into the providers array as well... well, then upon initialization testing module drops
NestJS Jest error: TypeError: Cannot read properties of undefined (reading 'region')
note: region variable is taken from env in a registered config module
code example of my test that throws
describe('EmailService', () => {
let emailService: EmailService;
let configService: ConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EmailService, ConfigService],
}).compile();
emailService = module.get<EmailService>(EmailService);
configService = module.get<ConfigService>(ConfigService);
});
it('should be defined', () => {
expect(emailService).toBeDefined();
});
});
I have came to the conclusion that it throws an error specifically because EmailService takes ConfigService in it's constructor in this way:
export class EmailService {
private readonly config: IAwsConfig;
private readonly region: IRegion;
constructor(private readonly configService: ConfigService) {
this.config = this.configService.get('aws');
this.region = this.config.region;
}
aditional info: both EmailService and ConfigService work just fine during a normal runtime, it only fails during jest testing
seems like this.configService.get method returns 'undefined' during a test run and i'm, not sure why or how to fix it. Any ideas?

In case you don't want to import the entire ConfigService but just the config values themselves, then you use them in the test as follows :)
// my-config.ts
import { registerAs } from '#nestjs/config';
export default registerAs('myConfig', () => ({ propA: 'aa', propB: 123 }));
import { Inject } from '#nestjs/common';
import { ConfigType } from '#nestjs/config';
import myConfig from './my-config.ts';
export class EmailService {
private propA: string;
private propB: number;
constructor(
#Inject(myConfig.KEY) config: ConfigType<typeof myConfig>
) {
this.propA = config.propA;
this.propB = config.propB;
}
}
import { ConfigModule, registerAs } from '#nestjs/config';
import { Test, TestingModule } from '#nestjs/testing';
describe('Test', () => {
const configValues = { propA: 'aa', proprB: 123 };
const config = registerAs('testConfig', () => configValues);
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
imports: [ConfigModule.forFeature(config)],
providers: [EmailService],
}).compile();
});
});

Was not able to find an answer for 2 hours straight, but then, 10 minutes after asking a question, there you go, an answer.
Seems like ConfigService doesn't provide configs during jest testing so you have to provide it in the testing module with replaced get method, something like such:
providers: [
EmailService,
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
return hardcodedConfigFromWithinTheTestFile;
}),
},
},
],

Related

How would I mock the DataSource from TypeORM in NestJS Jest tests?

I am trying to write tests for a small project in NestJS. Here is the relevant code for context:
dummy.controller.ts
#Controller(UrlConstants.BASE_URL + 'dummy')
export class DummyContoller {
constructor(
private readonly sessionService: SessionService,
) { }
#Get('validateSession')
async checkValidateSession(#Query('sessionId') sessionId: string) {
const session = await this.sessionService.validateSession(sessionId);
console.log(session);
return { message: "OK" };
}
}
session.service.ts
#Injectable()
export class SessionService {
constructor(
private readonly sessionRepo: SessionRepository,
private readonly accountRepo: AccountRepository
) { }
#WithErrorBoundary(AuthCodes.UNKNOWN_LOGIN_ERROR)
async validateSession(sessionId: string) {
const session = await this.sessionRepo.findOneBy({ sessionId });
if (!session || this.isSessionExpired(session)) {
session && await this.sessionRepo.remove(session);
throw new HttpException({
code: AuthCodes.SESSION_TIMEOUT,
message: AuthMessages.SESSION_TIMEOUT
}, HttpStatus.UNAUTHORIZED)
}
return session;
}
...
}
session.repository.ts (Any repository)
#Injectable()
export class SessionRepository extends Repository<Session> {
constructor(private dataSource: DataSource) {
super(Session, dataSource.createEntityManager())
}
...
}
This is how I wrote my test (this is my first time writing a test using Jest and I am not really experienced in writing tests in general):
describe('DummyController', () => {
let dummyContoller: DummyContoller;
let sessionService: SessionService;
let sessionRepo: SessionRepository;
let accountRepo: AccountRepository;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [DummyContoller],
providers: [SessionService, SessionRepository, AccountRepository]
}).compile();
dummyContoller = module.get<DummyContoller>(DummyContoller);
sessionService = module.get<SessionService>(SessionService);
sessionRepo = module.get<SessionRepository>(SessionRepository);
accountRepo = module.get<AccountRepository>(AccountRepository);
})
describe('checkValidateSession', () => {
it('should return valid session', async () => {
const sessionId = "sessionId1";
const session = new Session();
jest.spyOn(sessionService, 'validateSession').mockImplementation(async (sessionId) => session);
expect(await dummyContoller.checkValidateSession(sessionId)).toBe(session);
})
})
})
Upon running the test, I encounter:
Nest can't resolve dependencies of the SessionRepository (?). Please make sure that the argument DataSource at index [0] is available in the RootTestModule context.
Potential solutions:
- If DataSource is a provider, is it part of the current RootTestModule?
- If DataSource is exported from a separate #Module, is that module imported within RootTestModule?
#Module({
imports: [ /* the Module containing DataSource */ ]
})
I looked this problem and I came across a number of solutions but most of them had #InjectRepository() instead of creating a separate Repository class where they would provide getRepositoryToken() and then use a mock factory [Link]. I couldn't find a way to make this work.
Another solution suggested using an in-memory database solution [Link]. But this felt more like a hack rather than a solution.
How can I test the above setup?
Based on this comment, I was able to get this working by using the following in the providers in the test:
providers: [
SessionService,
{ provide: SessionRepository, useClass: SessionMockRepository },
]
SessionMockRepository contains a mocked version of all additional functions in that particular repository:
export class SessionMockRepository extends Repository<Session> {
someFunction = async () => jest.fn();
}
Currently, this works for me so I am accepting this. I am still open to more answers if there is a better way to do this.

NestJS test cannot resolve PinoLogger dependency

On NestJS application startup I am creating few db tables (using TypeORM) and have written service file init-db.service.ts for the same. The service file implements OnApplicationBootstrap interface and onApplicationBootstrap method is called once the application has fully started and is bootstrapped.
I am trying to write a unit test for the service file, but is unable to resolve PinoLogger dependency while creating Testing Module. Please checkout below sample code:
Service file init-db.service.ts:
import {ConfigService} from '#nestjs/config';
import {PinoLogger} from 'nestjs-pino';
import {Injectable, OnApplicationBootstrap} from '#nestjs/common';
#Injectable()
export class InitDB implements OnApplicationBootstrap {
constructor(private readonly logger: PinoLogger, private readonly configService: ConfigService) {}
onApplicationBootstrap() {
this.someMethod();
}
async someMethod(): Promise<void> {
// code to create tables...
}
}
Unit test file init-db.service.spec.ts:
import {ConfigService} from '#nestjs/config';
import {Test, TestingModule} from '#nestjs/testing';
import {InitDB} from './init-db.service';
import {getLoggerToken, PinoLogger} from 'nestjs-pino';
const mockLogger = {
// mock logger functions
};
describe('InitDB', () => {
jest.useFakeTimers();
let configService: ConfigService;
let service: InitDB;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getLoggerToken(InitDB.name), // <-------- PinoLogger dependency
useValue: mockLogger,
},
InitDB,
ConfigService,
],
}).compile();
service = module.get<InitDB>(InitDB);
configService = module.get<ConfigService>(ConfigService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
Error Message:
Nest can't resolve dependencies of the BootstrapService (?, ConfigService). Please make sure that the argument PinoLogger at index [0] is available in the RootTestModule context.
Potential solutions:
- If PinoLogger is a provider, is it part of the current RootTestModule?
- If PinoLogger is exported from a separate #Module, is that module imported within RootTestModule?
#Module({
imports: [ /* the Module containing PinoLogger */ ]
})
I have already passed PinoLogger at index [0], but the unit test still fails to resolve the dependency.
Please note that you don't use #InjectPinoLogger but you are trying to mock it as if you have used it.
You could either provide PinoLogger as a dependency:
const module: TestingModule = await Test.createTestingModule({
providers: [
PinoLogger,
InitDB,
ConfigService,
],
}).compile();
or use #InjectPinoLogger in your class constructor:
export class InitDB implements OnApplicationBootstrap {
constructor(
#InjectPinoLogger(InitDB.name)
private readonly logger: PinoLogger,
private readonly configService: ConfigService
) {}
}

Testing Class with #Injectable Scope / #Inject(REQUEST) NestJS

I have set up a MongooseConfigService to allow us to dynamically switch out the connection string for certain requests and am trying to get the tests set up correctly.
#Injectable({scope: Scope.REQUEST})
export class MongooseConfigService implements MongooseOptionsFactory {
constructor(
#Inject(REQUEST) private readonly request: Request) {
}
I am however having trouble providing request to the test context.
let service: Promise<MongooseConfigService>;
beforeEach(async () => {
const req = new JestRequest();
const module: TestingModule = await Test.createTestingModule({
providers: [
MongooseConfigService,
{
provide: getModelToken(REQUEST),
inject: [REQUEST],
useFactory: () => ({
request: req,
}),
},
],
}).compile();
service = module.resolve<MongooseConfigService>(MongooseConfigService);
});
This is as far as I have gotten, have tried it without the inject and with/without useValue, however this.request remains undefined when trying to run the tests.
TIA
maybe try to instantiate the testing module with only import: [appModule] and then try to get the service. also, try to use overrideprovider and usevalue like this:
let service: Promise<MongooseConfigService>;
beforeEach(async () => {
const req = new JestRequest();
const module: TestingModule = await Test.createTestingModule({
imports: [appModule]
}).overrideProvider(REQUEST)
.useValue(req)
.compile();
service = module.resolve<MongooseConfigService>(MongooseConfigService);
});

how to unit test a class extending an abstract class reading environment variables

I want to get into unit testing and have some configuration services for my Nest API that I want to test. When starting the application I validate the environment variables with the joi package.
I have multiple configuration services for the database, the server, ... so I created a base service first. This one is able to read environment variables, parse the raw string to a desired datatype and validate the value.
import { ConfigService } from '#nestjs/config';
import { AnySchema, ValidationResult, ValidationError } from '#hapi/joi';
export abstract class BaseConfigurationService {
constructor(protected readonly configService: ConfigService) {}
protected constructValue(key: string, validator: AnySchema): string {
const rawValue: string = this.configService.get(key);
this.validateValue(rawValue, validator, key);
return rawValue;
}
protected constructAndParseValue<TResult>(key: string, validator: AnySchema, parser: (value: string) => TResult): TResult {
const rawValue: string = this.configService.get(key);
const parsedValue: TResult = parser(rawValue);
this.validateValue(parsedValue, validator, key);
return parsedValue;
}
private validateValue<TValue>(value: TValue, validator: AnySchema, label: string): void {
const validationSchema: AnySchema = validator.label(label);
const validationResult: ValidationResult = validationSchema.validate(value);
const validationError: ValidationError = validationResult.error;
if (validationError) {
throw validationError;
}
}
}
Now I can extend this service with multiple configuration services. For the sake of simplicity I will take the server configuration service for this. Currently it only holds the port the application will listen to.
import { Injectable } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import * as Joi from '#hapi/joi';
import { BaseConfigurationService } from './base.configuration.service';
#Injectable()
export class ServerConfigurationService extends BaseConfigurationService {
public readonly port: number;
constructor(protected readonly configService: ConfigService) {
super(configService);
this.port = this.constructAndParseValue<number>(
'SERVER_PORT',
Joi.number().port().required(),
Number
);
}
}
I found multiple articles out there that I should only test public methods, e.g.
https://softwareengineering.stackexchange.com/questions/100959/how-do-you-unit-test-private-methods
so I'm assuming I should not test the methods from the base configuration service. But I would like to test the classes extending the base service. I started with this
import { Test, TestingModule } from '#nestjs/testing';
import { ConfigService } from '#nestjs/config';
import { ServerConfigurationService } from './server.configuration.service';
const mockConfigService = () => ({
get: jest.fn(),
});
describe('ServerConfigurationService', () => {
let serverConfigurationService: ServerConfigurationService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ServerConfigurationService,
{
provide: ConfigService,
useFactory: mockConfigService
}
],
}).compile();
serverConfigurationService = module.get<ServerConfigurationService>(ServerConfigurationService);
});
it('should be defined', () => {
expect(serverConfigurationService).toBeDefined();
});
});
but as you can see in the second code snippet I'm calling the functions from the base service in the constructor. The test instantly fails with
ValidationError: "SERVER_PORT" must be a number
Is there a way I can unit test the configuration services although they depend on an abstract base class and an external .env file? Because I know I can create a mockConfigService but I think the base class breaks this. I don't know how to fix this test file.
The main problem boils down to this: You are using the Joi libary to parse environment variables. Whenever you call validateValue, Joi functions are called that expect actual environment variables to be set (in this case, SERVER_PORT). Now that these environment variables need to be set is a valid assumption for the running service. But in your test cases, you have no environment variables set, hence the Joi validation fails.
A primitive solution would be to set process.env.SERVER_PORT to some value in your beforeEach and delete it in afterEach. However, this is just a work-around around the actual issue.
The actual issue is: You hard-coded library calls into your BaseConfigurationService that have the assumption that environment variables are set. We already figured out earlier that this is not a valid assumption when running tests. When you stumble upon issues like this when writing tests, it often points to a problem of tight coupeling.
How can we address that?
We can separate the concerns clearly and abstract away the actual validation into its own service class that's used by BaseConfigurationService. Let's call that service class ValidationService.
We can then inject that service class into BaseConfigurationService using Nest's dependency injection.
When running tests, we can mock the ValidationService so it does not rely on actual environment variables, but, for example, just doesn't complain about anything during validation.
So here's how we can achieve that, step by step:
1. Define a ValidationService interface
The interface simply describes how a class needs to look that can validate values:
import { AnySchema } from '#hapi/joi';
export interface ValidationService {
validateValue<TValue>(value: TValue, validator: AnySchema, label: string): void;
}
2. Implement the ValidationService
Now we'll take the validation code from your BaseConfigurationService and use it to implemente ValidationService:
import { Injectable } from '#nestjs/common';
import { AnySchema, ValidationResult, ValidationError } from '#hapi/joi';
#Injectable()
export class ValidationServiceImpl implements ValidationService {
validateValue<TValue>(value: TValue, validator: AnySchema, label: string): void {
const validationSchema: AnySchema = validator.label(label);
const validationResult: ValidationResult = validationSchema.validate(value);
const validationError: ValidationError = validationResult.error;
if (validationError) {
throw validationError;
}
}
}
3. Inject ValidationServiceImpl into BaseConfigurationService
We'll now remove the validation logic from the BaseConfigurationService and instead add a call to ValidationService:
import { ConfigService } from '#nestjs/config';
import { AnySchema, ValidationResult, ValidationError } from '#hapi/joi';
import { ValidationServiceImpl } from './validation.service.impl';
export abstract class BaseConfigurationService {
constructor(protected readonly configService: ConfigService,
protected readonly validationService: ValidationServiceImpl) {}
protected constructValue(key: string, validator: AnySchema): string {
const rawValue: string = this.configService.get(key);
this.validationService.validateValue(rawValue, validator, key);
return rawValue;
}
protected constructAndParseValue<TResult>(key: string, validator: AnySchema, parser: (value: string) => TResult): TResult {
const rawValue: string = this.configService.get(key);
const parsedValue: TResult = parser(rawValue);
this.validationService.validateValue(parsedValue, validator, key);
return parsedValue;
}
}
4. Implemente a mock ValidationService
For testing purposes, we don't want to validate against actual environment variables, but just genereally accept all values. So we implement a mock service:
import { ValidationService } from './validation.service';
import { AnySchema, ValidationResult, ValidationError } from '#hapi/joi';
export class ValidationMockService implements ValidationService{
validateValue<TValue>(value: TValue, validator: AnySchema, label: string): void {
return;
}
}
5. Adapt classes extending BaseConfigurationService to have ConfigurationServiceImpl injected and pass it on to BaseConfigurationService:
import { Injectable } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import * as Joi from '#hapi/joi';
import { BaseConfigurationService } from './base.configuration.service';
import { ValidationServiceImpl } from './validation.service.impl';
#Injectable()
export class ServerConfigurationService extends BaseConfigurationService {
public readonly port: number;
constructor(protected readonly configService: ConfigService,
protected readonly validationService: ValidationServiceImpl) {
super(configService, validationService);
this.port = this.constructAndParseValue<number>(
'SERVER_PORT',
Joi.number().port().required(),
Number
);
}
}
6. use the mock service in the test
Finally, now that ValidationServiceImpl is a dependency of BaseConfigurationService, we use the mocked version in the test:
import { Test, TestingModule } from '#nestjs/testing';
import { ConfigService } from '#nestjs/config';
import { ServerConfigurationService } from './server.configuration.service';
import { ValidationServiceImpl } from './validation.service.impl';
import { ValidationMockService } from './validation.mock-service';
const mockConfigService = () => ({
get: jest.fn(),
});
describe('ServerConfigurationService', () => {
let serverConfigurationService: ServerConfigurationService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ServerConfigurationService,
{
provide: ConfigService,
useFactory: mockConfigService
},
{
provide: ValidationServiceImpl,
useClass: ValidationMockService
},
],
}).compile();
serverConfigurationService = module.get<ServerConfigurationService>(ServerConfigurationService);
});
it('should be defined', () => {
expect(serverConfigurationService).toBeDefined();
});
});
Now when running the tests, ValidationMockService will be used. Plus, apart from fixing your test, you also have a clean separation of concerns.
The refactoring I provided here is just an example how you can go ahead. I guess that, depending on your further use cases, you might cut ValidationService differently than I did, or even separate more concerns into new service classes.

How to jest.spyOn only the base class method, not the overridden method

Trying to write test scripts for my nestjs application.
I have controller/service framework, that looks like this:
Controller:
export class MyController {
constructor(
protected _svc: MyService
) {}
#Get()
async getAll(): Promise<Array<Person>> {
return await this._svc.findAll();
}
}
Service:
#Injectable()
export class MyService extends DbService < Person > {
constructor(
private _cache: CacheService
) {
super(...);
}
async findAll() {
return super.findAll().then(res => {
res.map(s => {
this._cache.setValue(`key${s.ref}`, s);
});
return res;
});
}
Base class:
#Injectable()
export abstract class DbService<T> {
constructor() {}
async findAll(): Promise<Array<T>> {
...
}
}
My controller is the entry point when calling an endpoint on the API. This calls the service, which extends the DbService, which is what communicates with my database. There are a lot of services which all extend this DbService. In this case, the MyService class overrides the DbService "findAll" method to do some cache manipulation.
My test script has this:
let myController: MyController;
let myService: MyService;
describe("MyController", async () => {
let spy_findall, spy_cacheset;
beforeAll(() => {
this._cacheService = {
// getValue, setValue, delete methods
};
myService = new MyService(this._cacheService);
myController = new MyController(myService);
spy_findall = jest.spyOn(myService, "findAll").mockImplementation(async () => {
return [testPerson];
});
spy_cacheset = jest.spyOn(this._cacheService, "setValue");
});
beforeEach(async () => {
jest.clearAllMocks();
});
describe("getAll", () => {
it("should return an array of one person", async () => {
await myController.getAll().then(r => {
expect(r).toHaveLength(1);
expect(spy_findall).toBeCalledTimes(1);
expect(spy_cacheset).toBeCalledTimes(1);
expect(r).toEqual([testPerson]);
});
});
});
});
Now, obviously the mockImplementation of findAll mocks the "findAll" on MyService, so the test fails because spy_cacheset is never called.
What I would like to do is mock only the base method "findAll" from DbService, so that I maintain the extra functionality that exists in MyService.
Is there a way of doing this without just renaming the methods in MyService, which I would rather avoid doing?
Edited to add:
Thanks to #Jonatan lenco for such a comprehensive reponse, which I have taken on board and implemented.
I have one further question. CacheService, DbService and a whole lot of other stuff (some of which I want to mock, other that I don't) is in an external library project, "shared".
cache.service.ts
export class CacheService {...}
index.ts
export * from "./shared/cache.service"
export * from "./shared/db.service"
export * from "./shared/other.stuff"
....
This is then compiled and included as a package in node_modules.
In the project where I am writing the tests:
import { CacheService, DocumentService, OtherStuff } from "shared";
Can I still use jest.mock() for just the CacheService, without mocking the whole "shared" project?
In this case since you want to spy on an abstract class (DbService), you can spy on the prototype method:
jest.spyOn(DbService.prototype, 'findAll').mockImplementation(async () => {
return [testPerson];
});
Also here some recommendations for your unit tests with NestJS and Jest:
Use jest.mock() in order to simplify your mocking (in this case for CacheService). See https://jestjs.io/docs/en/es6-class-mocks#automatic-mock.
When you do jest.spyOn(), you can assert the method execution without the need of the spy object. Instead of:
spy_findall = jest.spyOn(myService, "findAll").mockImplementation(async () => {
return [testPerson];
});
...
expect(spy_findall).toBeCalledTimes(1);
You can do:
jest.spyOn(DbService.prototype, 'findAll').mockImplementation(async () => {
return [testPerson];
});
...
expect(DbService.prototype.findAll).toBeCalledTimes(1);
If you are mocking a class properly, you do not need to spy on the method (if you do not want to mock its implementation).
Use the Testing utilities from NestJS, it will help you a lot specially when you have complex dependency injection. See https://docs.nestjs.com/fundamentals/testing#testing-utilities.
Here is an example that applies these 4 recommendations for your unit test:
import { Test } from '#nestjs/testing';
import { CacheService } from './cache.service';
import { DbService } from './db.service';
import { MyController } from './my.controller';
import { MyService } from './my.service';
import { Person } from './person';
jest.mock('./cache.service');
describe('MyController', async () => {
let myController: MyController;
let myService: MyService;
let cacheService: CacheService;
const testPerson = new Person();
beforeAll(async () => {
const module = await Test.createTestingModule({
controllers: [MyController],
providers: [
MyService,
CacheService,
],
}).compile();
myService = module.get<MyService>(MyService);
cacheService = module.get<CacheService>(CacheService);
myController = module.get<MyController>(MyController);
jest.spyOn(DbService.prototype, 'findAll').mockImplementation(async () => {
return [testPerson];
});
});
beforeEach(async () => {
jest.clearAllMocks();
});
describe('getAll', () => {
it('Should return an array of one person', async () => {
const r = await myController.getAll();
expect(r).toHaveLength(1);
expect(DbService.prototype.findAll).toBeCalledTimes(1);
expect(cacheService.setValue).toBeCalledTimes(1);
expect(r).toEqual([testPerson]);
});
});
});
NOTE: for the testing utilities to work and also for your application to work well, you will need to add the #Controller decorator on the class MyController:
import { Controller, Get } from '#nestjs/common';
...
#Controller()
export class MyController {
...
}
About mocking specific items of another package (instead of mocking the whole package) you could do this:
Create a class in your spec file (or you can create it in another file that you import, or even in your shared module) which has a different name but has the same public method names. Note that we use jest.fn() since we do not need to provide an implementation, and that already spies in the method (no need to later do jest.spyOn() unless you have to mock the implementation).
class CacheServiceMock {
setValue = jest.fn();
}
When setting up the providers of your testing module, tell it that you are "providing" the original class but actually providing the mocked one:
const module = await Test.createTestingModule({
controllers: [MyController],
providers: [
MyService,
{ provide: CacheService, useClass: CacheServiceMock },
],
}).compile();
For more info about providers see https://angular.io/guide/dependency-injection-providers (Nest follows the same idea of Angular).

Resources