I've been struggling with slow performance in Jest and followed some advice to run it using this command:
node --expose-gc ./node_modules/.bin/jest --runInBand --logHeapUsage
It clearly shows that my tests are leaking memory (about 70Mb)
PASS src/test/events/events.controller.spec.ts (8.185 s, 174 MB heap size)
PASS src/test/accounts/accounts.queries.spec.ts (260 MB heap size)
PASS src/test/accounts/apikeys.controller.spec.ts (328 MB heap size)
PASS src/test/events/events.repo.spec.ts (395 MB heap size)
PASS src/test/accounts/projects.commands.spec.ts (463 MB heap size)
PASS src/test/mailer/mailer.service.spec.ts (530 MB heap size)
PASS src/test/groups/groups.queue.spec.ts (597 MB heap size)
PASS src/test/mailer/mailer.command.spec.ts (664 MB heap size)
PASS src/test/accounts/apikeys.queries.spec.ts (731 MB heap size)
PASS src/test/groups/groups.command.spec.ts (799 MB heap size)
Looking at the code myself, I couldn't find what is causing it. Can you guys help me figure this out. I am unable to run this on the Chrome Inspector for some reason (it's failing to process the heap).
Here is my Basetest from which all my tests import:
import * as supertest from 'supertest';
import { ExecutionContext, INestApplication, Type, ValidationPipe } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { AppModule } from '../app.module';
import { AuthGuard } from '@nestjs/passport';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Account } from '../accounts/entities/account.entity';
import { Session } from '../accounts/entities/session.entity';
import { Repository } from 'typeorm';
import { ApiKey } from '../accounts/entities/apikey.entity';
import { Project } from '../accounts/entities/project.entity';
import { Operator } from '../accounts/entities/operator.entity';
import { ProjectRole } from '../accounts/entities/project-roles.entity';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { GlobalGuard } from '../auth/global.guard';
import { ProjectGuard } from '../auth/project.guard';
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
const createMockRepository = <T = any>(): MockRepository<T> => ({
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
find: jest.fn(),
preload: jest.fn(),
remove: jest.fn(),
});
export abstract class BaseTest {
static app: INestApplication;
static httpServer: any;
static accountRepository: MockRepository;
static projectRepository: MockRepository;
static apikeyRepository: MockRepository;
static operatorRepository: MockRepository;
static projectRoleRepository: MockRepository;
static sessionRepository: MockRepository;
static fakeRequest: any;
static fakeApiRequest: any;
// runs before all tests
static async before(): Promise<void> {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideGuard(JwtAuthGuard)
.useValue({
canActivate: (ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest();
req.user = { operatorId: 'opid', accountId: '123', email: '[email protected]' };
return true;
},
})
.overrideGuard(GlobalGuard)
.useValue({
canActivate: (ctx: ExecutionContext) => {
return true;
},
})
.overrideGuard(ProjectGuard)
.useValue({
canActivate: (ctx: ExecutionContext) => {
return true;
},
})
.overrideGuard(AuthGuard('api-key'))
.useValue({
canActivate: (ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest();
req['account'] = { id: '123', name: 'test', slug: 'test' };
return true;
},
})
.overrideProvider(getRepositoryToken(Account))
.useValue(createMockRepository())
.overrideProvider(getRepositoryToken(Project))
.useValue(createMockRepository())
.overrideProvider(getRepositoryToken(ApiKey))
.useValue(createMockRepository())
.overrideProvider(getRepositoryToken(Operator))
.useValue(createMockRepository())
.overrideProvider(getRepositoryToken(ProjectRole))
.useValue(createMockRepository())
.overrideProvider(getRepositoryToken(Session))
.useValue(createMockRepository())
.overrideProvider('database')
.useValue({
insert: jest.fn().mockReturnValue({
toPromise: jest.fn().mockResolvedValue(true),
}),
query: jest.fn().mockReturnValue({
toPromise: jest.fn().mockResolvedValue(true),
}),
queryPromise: jest.fn(),
})
.compile();
BaseTest.app = moduleRef.createNestApplication();
BaseTest.app.useGlobalPipes(new ValidationPipe({ transform: true }));
BaseTest.app = await BaseTest.app.init();
BaseTest.httpServer = this.app.getHttpServer();
BaseTest.accountRepository = moduleRef.get<MockRepository>(getRepositoryToken(Account));
BaseTest.projectRepository = moduleRef.get<MockRepository>(getRepositoryToken(Project));
BaseTest.apikeyRepository = moduleRef.get<MockRepository>(getRepositoryToken(ApiKey));
BaseTest.operatorRepository = moduleRef.get<MockRepository>(getRepositoryToken(Operator));
BaseTest.projectRoleRepository = moduleRef.get<MockRepository>(getRepositoryToken(ProjectRole));
BaseTest.sessionRepository = moduleRef.get<MockRepository>(getRepositoryToken(Session));
BaseTest.fakeRequest = { user: { sessionId: 'aaa', operatorId: 'opid', email: '[email protected]', accountId: '123' } };
BaseTest.fakeApiRequest = { projectId: 'projectid' };
}
static async after(): Promise<void> {
expect.hasAssertions();
BaseTest.app = null;
}
get<TInput = any, TResult = TInput>(type: Type<TInput> | string | symbol): TResult {
return BaseTest.app.get(type);
}
server(): supertest.SuperTest<supertest.Test> {
return supertest(BaseTest.httpServer);
}
}
And here is a sample test that imports it:
import { suite, test } from '@testdeck/jest';
import { BaseTest } from '../base-test';
import { UserListQuery } from '../../users/queries/user-list.query';
import { UserRepository } from '../../users/repositories/user.repository';
import { UserListQueryHandler } from '../../users/queries/user-list.handler';
import { GetUserByInternalIdQuery } from '../../users/queries/get-user-by-internal-id.query';
import { GetUserByInternalIdQueryHandler } from '../../users/queries/get-user-by-internal-id.handler';
@suite
export class UserQueryTest extends BaseTest {
@test
async '[UserListHandler] Should call the right service'() {
const query: UserListQuery = {
page: 0,
sortBy: 'createdAt',
sortOrder: '-1',
filters: {},
projectId: 'lala',
};
const repo = jest.spyOn(super.get(UserRepository), 'list').mockReturnThis();
await super.get(UserListQueryHandler).execute(query);
expect(repo).toHaveBeenCalledWith(query.page, query.sortBy, query.sortOrder, query.filters, query.projectId);
}
@test
async '[GetUserByInternalId] Should call the right service'() {
const query: GetUserByInternalIdQuery = {
internalId: 'lala',
projectId: 'lala',
};
const repo = jest.spyOn(super.get(UserRepository), 'getByInternalId').mockReturnThis();
await super.get(GetUserByInternalIdQueryHandler).execute(query);
expect(repo).toHaveBeenCalledWith(query.internalId, query.projectId);
}
}
Any hints on where this leak may be?
CodePudding user response:
This is a wild guess, but I see in your after()
in BaseTest
you are setting the BaseTest.app = null;
, could you try doing it for all the other properties on BaseTest?
BaseTest.httpServer = null;
BaseTest.accountRepository = null;
BaseTest.projectRepository = null;
BaseTest.apikeyRepository = null;
BaseTest.operatorRepository = null;
BaseTest.projectRoleRepository = null;
BaseTest.sessionRepository = null;
CodePudding user response:
Another hunch:
Could you try using this on your afterEach()
jest.resetAllMocks()