Home > Blockchain >  Jest tests leaking a lot of memory
Jest tests leaking a lot of memory

Time:09-08

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()

  • Related