使用Jest测试TypeORM项目时遭遇EntityMetadataNotFoundError
问题:Jest测试登录接口时抛出EntityMetadataNotFoundError,生产环境正常
我已经排查这个问题超过两天,目前的情况是:在nodemon中运行服务时,注册和登录API都能正常工作,但执行Jest测试时,只有登录测试失败,抛出EntityMetadataNotFoundError错误。
相关代码文件
src/data-source.ts
import { DataSource } from 'typeorm'; import { User } from '@src/entities/User'; import 'reflect-metadata'; export const AppDataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'admin', password: 'password', database: 'users', synchronize: true, logging: true, entities: [User], migrationsRun: true, migrations: ['@src/migrations/*'], }); export const TestDataSource = new DataSource({ name: 'test', type: 'postgres', host: 'localhost', port: 5433, username: 'test', password: 'password', database: 'tests', dropSchema: true, synchronize: true, entities: ["@src/entities/*.ts"], });
src/services/user.service.ts
import 'reflect-metadata'; import { User } from '@src/entities/User'; import { validateUserInput } from '@src/utils/validateUserInput'; import argon2 from 'argon2'; import { AppDataSource } from '@src/data-source'; import { EntityManager, QueryFailedError, Repository } from 'typeorm'; export interface UserInputParameters { name: string; username: string; email: string; password: string; } export interface UserResponse { user?: User; errors?: FieldError[]; } export interface SecureUserResponse { user?: { name: string; email: string; username: string; }; errors?: FieldError[]; } export interface FieldError { field: string; message: string; } export const queryFailedGuard = (err: unknown): err is QueryFailedError & { code: string } => err instanceof QueryFailedError; const UserRepository: Repository<User> = AppDataSource.getRepository(User); export const registerService = async (inputParameters: UserInputParameters): Promise<UserResponse> => { const errors = validateUserInput(inputParameters); if (errors) { return { errors }; } const { name, username, email, password } = inputParameters; const hashedPassword = await argon2.hash(password); let user = new User(); try { UserRepository.merge(user,{ name, username, email, password: hashedPassword, }) await UserRepository.save(user); } catch (err) { if (queryFailedGuard(err)) { if (err.code === '23505') { console.log(err); return { errors: [ { field: 'username', message: 'Username already exists', }, { field: 'email', message: 'Email already exists', }, ], }; } } } return { user }; }; export const loginService = async (usernameOrEmail: string, password: string): Promise<SecureUserResponse> => { let user; try { user = await UserRepository.findOneBy( usernameOrEmail.includes('@') ? { email: usernameOrEmail } : { username: usernameOrEmail } ); console.log(user); if (!user) { return { errors: [ { field: 'usernameOrEmail', message: 'Invalid Username or Email', }, ], }; } const valid = await argon2.verify(user.password, password); if (!valid) { return { errors: [ { field: 'password', message: 'Invalid password', }, ], }; } return { user: { username: user.username, email: user.email, name: user.name, }, }; } catch (error) { console.error(error); return { errors: [ { field: 'usernameOrEmail', message: 'Error in logging in', }, ], }; } }; //@TODO: update user, forget password, deleteAccount, view tasks.
src/tests/user.test.ts
import { createServer } from '@src/config/createServer'; import { testConnection } from '@src/utils/connections'; import supertest from 'supertest'; let app = createServer(); beforeAll(async () => { await testConnection.create(); // server = await app.listen(4000); }); afterAll(async () => { await testConnection.close(); // server.close(); }); let registeredUser = { usernameOrEmail: 'shravan26', password: 'shravan98', }; let testUser = { name: 'shravan', username: 'shravan26', email: 'shravan@gmail.com', password: 'shravan98', }; describe('POST /register', () => { it('should register user if all fields exist', async () => { let testUser = { name: 'shravan', username: 'shravan26', email: 'shravan@gmail.com', password: 'shravan98', }; const response = await supertest(app).post('/api/users/register').send(testUser); expect(response.statusCode).toBe(200); expect(response.body.message).toBe('Successfully registered user'); }); it('should throw error if email field is missing', async () => { let testUser = { name: 'shravan', username: 'shravan26', email: 'email', password: 'shravan98', }; const response = await supertest(app).post('/api/users/register').send(testUser); expect(response.statusCode).toBe(401); expect(response.body[0].field).toBe('email'); expect(response.body[0].message).toBe('Please enter a valid email'); }); it('should throw error if email is invalid', async () => { let testUser = { name: 'shravan', username: 'shravan26', email: '@', password: 'shravan98', }; const response = await supertest(app).post('/api/users/register').send(testUser); expect(response.statusCode).toBe(401); expect(response.body[0].field).toBe('email'); expect(response.body[0].message).toBe('Please enter a valid email'); }); it('should throw error if name is empty', async () => { let testUser = { name: '', username: 'shravan26', email: 'shravan@gmail.com', password: 'shravan98', }; const response = await supertest(app).post('/api/users/register').send(testUser); expect(response.statusCode).toBe(401); expect(response.body[0].field).toBe('name'); expect(response.body[0].message).toBe('Please enter a valid name'); }); it('should throw error if username is less than 4 characters', async () => { let testUser = { name: 'shravan', username: '', email: 'shravan@gmail.com', password: 'shravan98', }; const response = await supertest(app).post('/api/users/register').send(testUser); expect(response.statusCode).toBe(401); expect(response.body[0].field).toBe('username'); expect(response.body[0].message).toBe('Username should be more than 4 characters'); }); it('should throw error if password is less than 8 characters', async () => { let testUser = { name: 'shravan', username: 'shravan26', email: 'shravan@gmail.com', password: 'shr', }; const response = await supertest(app).post('/api/users/register').send(testUser); expect(response.statusCode).toBe(401); expect(response.body[0].field).toBe('password'); expect(response.body[0].message).toBe('Password should be more than 8 characters'); }); it('should login a registered user', async () => { const loginResponse = await supertest(app).post('/api/users/login').send(registeredUser); console.log(loginResponse); expect(loginResponse.statusCode).toBe(200); }); });
错误日志
EntityMetadataNotFoundError: No metadata for "User" was found. at DataSource.getMetadata (/Users/shravan/Documents/todolist/server/src/data-source/DataSource.ts:423:30) at Repository.get metadata [as metadata] (/Users/shravan/Documents/todolist/server/src/repository/Repository.ts:52:40) at Repository.findOneBy (/Users/shravan/Documents/todolist/server/src/repository/Repository.ts:539:44) at /Users/shravan/Documents/todolist/server/src/services/user.service.ts:78:37 at Generator.next (<anonymous>) at /Users/shravan/Documents/todolist/server/src/services/user.service.ts:8:71 at new Promise (<anonymous>) at Object.<anonymous>.__awaiter (/Users/shravan/Documents/todolist/server/src/services/user.service.ts:4:12) at loginService (/Users/shravan/Documents/todolist/server/src/services/user.service.ts:75:110) at /Users/shravan/Documents/todolist/server/src/controllers/user.controller.ts:17:48 109 | }; 110 | } catch (error) { > 111 | console.error(error); | ^ 112 | return { 113 | errors: [ 114 | { at src/services/user.service.ts:111:17 at Generator.throw (<anonymous>) at rejected (src/services/user.service.ts:6:65) FAIL src/__tests__/user.test.ts POST /register ✓ should register user if all fields exist (44 ms) ✓ should throw error if email field is missing (11 ms) ✓ should throw error if email is invalid (3 ms) ✓ should throw error if name is empty (3 ms) ✓ should throw error if username is less than 4 characters (3 ms) ✓ should throw error if password is less than 8 characters (2 ms) ✕ should login a registered user (63 ms) ● POST /register › should login a registered user expect(received).toBe(expected) // Object.is equality Expected: 200 Received: 401 97 | it('should login a registered user', async () => { 98 | const loginResponse = await supertest(app).post('/api/users/login').send(registeredUser); > 99 | expect(loginResponse.statusCode).toBe(200); | ^ 100 | }); 101 | }); 102 | at src/__tests__/user.test.ts:99:42 at fulfilled (src/__tests__/user.test.ts:5:58) Test Suites: 1 failed, 1 total Tests: 1 failed, 6 passed, 7 total Snapshots: 0 total Time: 5.028 s Ran all test suites.
问题分析
核心问题在于:测试环境初始化的是TestDataSource,但业务代码中的UserRepository始终从AppDataSource获取,而AppDataSource在测试时并没有被初始化,导致TypeORM无法找到User实体的元数据,从而抛出EntityMetadataNotFoundError。
另外,TestDataSource的entities配置使用字符串路径"@src/entities/*.ts",可能存在路径解析问题,不如直接传入实体类[User]可靠。
解决方案
1. 动态切换数据源
修改src/services/user.service.ts,根据环境变量选择对应的数据源:
// 替换原有的UserRepository定义部分 import { AppDataSource, TestDataSource } from '@src/data-source'; // 根据环境变量切换数据源 const activeDataSource = process.env.NODE_ENV === 'test' ? TestDataSource : AppDataSource; const UserRepository: Repository<User> = activeDataSource.getRepository(User);
2. 统一TestDataSource的entities配置
修改src/data-source.ts中的TestDataSource,让entities配置和AppDataSource保持一致:
export const TestDataSource = new DataSource({ name: 'test', type: 'postgres', host: 'localhost', port: 5433, username: 'test', password: 'password', database: 'tests', dropSchema: true, synchronize: true, entities: [User], // 替换原字符串路径,直接传入实体类 });
3. 测试脚本中设置环境变量
在package.json的test脚本中添加NODE_ENV=test,确保测试时环境变量正确:
{ "scripts": { "test": "NODE_ENV=test jest" } }
4. 确保测试数据源初始化完成
检查testConnection.create()的实现,确保它确实初始化的是TestDataSource,并且等待初始化完成后再执行测试。
内容的提问来源于stack exchange,提问作者Shravan
相关产品推荐
相关产品推荐

