【问题标题】:NestJS TypeORM Unit Tests mocking getConnection.transactionNestJS TypeORM 单元测试模拟 getConnection.transaction
【发布时间】:2022-03-13 19:37:26
【问题描述】:

我正在为 NestJS 应用程序编写 UT。 我在模拟甚至测试事务内部函数的逻辑时遇到问题。

这是来自服务的一段代码,它获取连接并开始使用 transactionEntityManager 与 DB 交互。

我希望像这个例子一样测试一个函数的内部逻辑(我有几个这样的案例)。

import { getConnection } from 'typeorm';
@Injectable()
export class UsersService {

    async getUsers(user: User): Promise<{ users: User[], organizations: MinimalOrg[] }> {
        const result: { users: User[], organizations: MinimalOrg[] } = { users: [], organizations: [] };
        // getConnection() is imported from 'typeorm'
        await getConnection().transaction(async (transactionEntityManager) => {
            if (!user || !Utils.isAdmin(user)) {
                throw new UnauthorizedException('You have no permission');
            }

            result.users = await transactionEntityManager
                .createQueryBuilder(User, 'user')
                .leftJoin('user.image', 'image')
                .leftJoin('user.organization', 'organization')
                .select([
                    'user.id',
                    'user.email',
                ])
                .addSelect(['organization.id', 'organization.name'])
                .addSelect(['image.id', 'image.fileName'])
                .getMany();

            result.organizations = (await transactionEntityManager
                .createQueryBuilder(Organization, 'org')
                .select(['org.id', 'org.name'])
                .getMany()) as MinimalOrg[];
        });
        return result;
    }
}

我已经尝试在单元测试中使用这种方法,但无法成功。

jest.mock('typeorm',()=>({
    transaction:jest.fn()
}));

任何帮助将不胜感激

【问题讨论】:

  • 如果这个问题有更多的重点,会更容易回答;也许选择一个您正在尝试编写的测试并编辑问题以询问这个问题?
  • 乍一看,您是从 typeorm 模拟 transaction,而您只是在代码中导入 getConnection。
  • 至少,如果你想模拟交易,你需​​要类似:({ getConnection: () =&gt; ({ transaction: jest.fn() }) }) in your jest.mock
  • 关于焦点:如果我模拟geConnection().transaction(,我如何才能访问最简单的行if (!user || !Utils.isAdmin(user)) {以及如何模拟像getConnection()这样的全局函数来实现这一点?
  • 出于好奇,为什么使用typeorm 中的getConnection() 而不是@nestjs/typeorm 中的@InjectConnection() 来注入连接并能够在(嵌套)约定中模拟它?

标签: typescript unit-testing jestjs nestjs typeorm


【解决方案1】:

您需要先模拟 getConnection(),然后再模拟 transction() 本身。

const queryBuilderMock = {
  leftJoin: jest.fn().mockReturnThis(),
  select: jest.fn().mockReturnThis(),
  addSelect: jest.fn().mockReturnThis(),
  getMany: jest.fn().mockResolvedValue(['your_mocked_object_here']),
};

const entityManagerMock = { createQueryBuilder: () => queryBuilderMock };
const transactionMock = jest.fn(async passedFunction => await passedFunction(entityManagerMock));

jest.mock('typeorm', () => ({
  getConnection: () => ({ transaction: transactionMock })
}));

不要忘记在每次测试之前清理/重置所需的模拟。

  beforeEach(async () => {
    queryBuilderMock.addSelect.mockClear();
    queryBuilderMock.select.mockClear();
    queryBuilderMock.getMany.mockClear();
    queryBuilderMock.leftJoin.mockClear();
    const module: TestingModule = await Test.createTestingModule({
      providers: [UsersService],
    }).compile();

    service = module.get<UsersService>(UsersService);
  });

如果你想测试(!user || !Utils.isAdmin(user)) 异常行,你可以试试这个:

it('should throw exception if passed user is null', async () => {
  await expect(service.getUsers(null as any)).rejects.toThrow(UnauthorizedException);
});

【讨论】:

    【解决方案2】:

    经过几天的调查和您的反馈,我通过以下步骤得出了这个解决方案:

    1. 在 devDependencies 中安装 sqlite3(将仅用于内存数据库 用于连接)。
    2. 注入到服务的构造函数中 @InjectConnection() private readonly connection: Connection。
    3. 用@Transaction()装饰方法,并添加服务方法@TransactionManager() _manager?的新参数。

    服务方法现在看起来像这样:

        @Transaction()
        async getUsers(
            user: User,
            @TransactionManager() _manager?
        ): Promise<{ users: User[], organizations: MinimalOrg[] }> {
            const result: { users: User[], organizations: MinimalOrg[] } = { users: [], organizations: [] };
            const transactionEntityManager = this.connection.manager;
            if (!user || !Utils.isAdmin(user)) {
                throw new UnauthorizedException('You have no permission');
            }
            result.users = await transactionEntityManager
                .createQueryBuilder(User, 'user')
                .leftJoin('user.image', 'image')
                .leftJoin('user.organization', 'organization')
                .select([
                    'user.id',
                    'user.email',
                ])
                .addSelect(['organization.id', 'organization.name'])
                .addSelect(['image.id', 'image.fileName'])
                .getMany();
            result.organizations = (await transactionEntityManager
                .createQueryBuilder(Organization, 'org')
                .select(['org.id', 'org.name'])
                .getMany()) as MinimalOrg[];
            return result;
        }
    

    在规范文件中,重要的是使用数据库配置设置导入,如下所示:

    /* eslint-disable import/no-extraneous-dependencies */
    import { Logger } from '@nestjs/common';
    import { Test } from '@nestjs/testing';
    import { getConnectionToken, TypeOrmModule, } from '@nestjs/typeorm';
    import { UserRole } from 'gn-models';
    import { ConnectionOptions, getConnection } from 'typeorm';
    
    import { User } from '../models/user.entity';
    import { UsersService } from './users.service';
    
    // #region Mock
    const createQueryBuilder: any = {
        createQueryBuilder: jest.fn().mockImplementation(() => createQueryBuilder),
        select: jest.fn().mockImplementation(() => createQueryBuilder),
        andWhere: jest.fn().mockImplementation(() => createQueryBuilder),
        addSelect: jest.fn().mockImplementation(() => createQueryBuilder),
        leftJoinAndSelect: jest.fn().mockImplementation(() => createQueryBuilder),
        innerJoin: jest.fn().mockImplementation(() => createQueryBuilder),
        leftJoin: jest.fn().mockImplementation(() => createQueryBuilder),
        groupBy: jest.fn().mockImplementation(() => createQueryBuilder),
        where: jest.fn().mockImplementation(() => createQueryBuilder),
        findOne: jest.fn().mockImplementation(() => null),
        save: jest.fn().mockImplementation(() => null),
        getOne: jest.fn().mockImplementation(() => null),
        getMany: jest.fn().mockImplementation(() => null),
        getRawMany: jest.fn().mockImplementation(() => null)
    }
    // #endregion
    
    describe('UsersService', () => {
        let srv: UsersService;
        beforeEach(async () => {
            const moduleRef = await Test.createTestingModule({
                imports: [
                    TypeOrmModule.forRoot({
                        type: "sqlite",
                        database: ":memory:",
                        dropSchema: true,
                        synchronize: true,
                        logging: false,
                        name: 'default'
                    } as ConnectionOptions)
                ],
                providers: [
                    UsersService,
                    {
                        provide: getConnectionToken(),
                        useValue: {
                            manager: createQueryBuilder,
                            ...createQueryBuilder
                        }
                    },
                ],
            }).compile();
    
            // Disable log error prints 
            jest.spyOn(Logger, 'error').mockReturnValue();
    
            srv = moduleRef.get<UsersService>(UsersService);
        });
    
        afterEach(async () => {
            jest.clearAllMocks();
            await getConnection().close();
        });
    
    
        it('should test getUsers', async () => {
            const userRecord = { rdp: '1', userName: 'aaa', roles: [UserRole.SuperAdmin] } as User;
    
            const res = await srv.getUsers(userRecord);
            expect(res).toBeTruthy();
            expect(res).toEqual({ users: null, organizations: null })
        });
    
        it('should test getUsers if the user is not admin', async () => {
            const userRecord = { rdp: '1', userName: 'aaa', roles: [] } as User;
            expect.assertions(1);
            const res = srv.getUsers(userRecord);
            await expect(res).rejects
                .toThrowError('You have no permission');
        });
    });
    
    

    请注意,ma​​nager 设置为 beforeEach,因为连接允许在测试中模拟实体管理器,并且仍然在服务方法 const transactionEntityManager = this.connection.manager; 中使用它。

    我已将await getConnection().close(); 设置为afterEach,因此每个测试将彼此独立运行。如果连接已经打开,jest 将无法运行具有相同连接名称的其他测试(默认)。

    我并不是说这是最好的解决方案,但在投入大量时间和精力来完成这项工作后,它对我有用。

    我希望它可以帮助您为您的项目编写 UT!

    【讨论】:

      猜你喜欢
      • 2019-09-14
      • 2020-03-22
      • 2022-01-26
      • 2021-06-07
      • 2019-12-04
      • 2022-01-02
      • 1970-01-01
      • 2019-11-27
      • 2020-10-30
      相关资源
      最近更新 更多