You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 15:06:21