NestJS TypeORM 单元测试模拟 getConnection.transaction
NestJS TypeORM Unit Tests mocking getConnection.transaction
我正在为 NestJS 应用程序编写 UT。
我在模拟甚至测试事务内部函数的逻辑时遇到问题。
这是一段来自服务的代码,它获取连接并开始使用 transactionEntityManager 与数据库交互。
我想像这个例子一样测试函数的内部逻辑(我有几个这样的案例)。
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()
}));
任何帮助将不胜感激
您需要先模拟 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 })
}));
另外不要忘记在每次测试前clean/reset需要模拟。
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);
});
经过几天的调查和您的反馈,我通过以下步骤找到了这个解决方案:
- 在 devDependencies 中安装 sqlite3(将使用 in-memory 数据库
用于连接)。
- 注入到服务的构造函数中
@InjectConnection() private readonly connection: Connection
.
- 用@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');
});
});
请注意 manager 设置为 beforeEach 因为连接允许在测试中模拟 entity-manager 并且仍然在服务方法 const transactionEntityManager = this.connection.manager;
.
我已将 await getConnection().close();
设置为 afterEach,因此每个测试将 运行 彼此独立。如果连接已经打开,jest 将无法 运行 具有相同连接名称(默认)的其他测试。
我并不是说这是最好的解决方案,但在我投入大量时间和精力后它对我有用。
希望对你的项目编写UT有所帮助!
我正在为 NestJS 应用程序编写 UT。 我在模拟甚至测试事务内部函数的逻辑时遇到问题。
这是一段来自服务的代码,它获取连接并开始使用 transactionEntityManager 与数据库交互。
我想像这个例子一样测试函数的内部逻辑(我有几个这样的案例)。
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()
}));
任何帮助将不胜感激
您需要先模拟 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 })
}));
另外不要忘记在每次测试前clean/reset需要模拟。
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);
});
经过几天的调查和您的反馈,我通过以下步骤找到了这个解决方案:
- 在 devDependencies 中安装 sqlite3(将使用 in-memory 数据库 用于连接)。
- 注入到服务的构造函数中
@InjectConnection() private readonly connection: Connection
. - 用@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');
});
});
请注意 manager 设置为 beforeEach 因为连接允许在测试中模拟 entity-manager 并且仍然在服务方法 const transactionEntityManager = this.connection.manager;
.
我已将 await getConnection().close();
设置为 afterEach,因此每个测试将 运行 彼此独立。如果连接已经打开,jest 将无法 运行 具有相同连接名称(默认)的其他测试。
我并不是说这是最好的解决方案,但在我投入大量时间和精力后它对我有用。
希望对你的项目编写UT有所帮助!