Rails的ActiveRecord事务测试机制能否在TypeORM中实现?
Absolutely! You can mirror Rails' approach of wrapping each test in an ActiveRecord transaction (and rolling it back afterward to preserve your seed data) in TypeORM—even working around the cross-method transaction sharing issue you've noticed. Here's how to make it work:
Core Idea
The goal is to start a single transaction at the beginning of each test, route all database operations in your test and application code to use this transaction's context, then roll it back once the test finishes. This keeps your database in its original state without truncating tables or messing with seed data.
Step 1: Set Up a Shared Transaction in Your Test Hooks
First, use your testing framework's (Jest, Mocha, etc.) setup/teardown hooks to create and manage the transaction. You'll need to keep track of the QueryRunner (which manages the transaction) and its associated EntityManager (which all your operations should use).
For example, with Jest:
import { getConnection, QueryRunner, EntityManager } from "typeorm"; let queryRunner: QueryRunner; let transactionalEntityManager: EntityManager; beforeEach(async () => { // Get your existing connection (or create a test connection first) const connection = getConnection(); queryRunner = connection.createQueryRunner(); // Start the transaction await queryRunner.startTransaction(); // Store the EntityManager tied to this transaction—this is what we'll share transactionalEntityManager = queryRunner.manager; }); afterEach(async () => { // Roll back the transaction to undo all test operations await queryRunner.rollbackTransaction(); // Clean up the query runner await queryRunner.release(); });
Step 2: Make Your Application Code Accept a Shared EntityManager
The key pain point you mentioned—transactions not being shared across methods—happens because methods that use getRepository(Entity) or getManager() by default use the connection's root EntityManager, which isn't part of your test transaction.
Fix this by modifying your services/repositories to accept an optional EntityManager parameter. This lets you inject the transactional manager from your tests:
Example Service Adjustment
// src/services/user.service.ts import { EntityManager, getRepository } from "typeorm"; import User from "../entities/user.entity"; class UserService { // Add an optional manager parameter, defaulting to the global one for non-test use async createUser(userData: Partial<User>, manager: EntityManager = getRepository(User).manager) { // Use the provided manager to get the repository const userRepository = manager.getRepository(User); return userRepository.save(userData); } async getUserById(userId: number, manager: EntityManager = getRepository(User).manager) { return manager.getRepository(User).findOneBy({ id: userId }); } } export default UserService;
Step 3: Use the Transactional EntityManager in Tests
Now, when you call your service methods in tests, pass the transactionalEntityManager you created in the setup hook. All operations will run inside the same transaction, even across method calls:
test("creates a user and rolls back changes after test", async () => { const userService = new UserService(); // Create a user using the transactional manager const newUser = await userService.createUser({ name: "Ezequiel" }, transactionalEntityManager); // Verify the user exists within the transaction const retrievedUser = await userService.getUserById(newUser.id, transactionalEntityManager); expect(retrievedUser?.name).toBe("Ezequiel"); }); // After the test finishes, the afterEach hook rolls back the transaction— // the user won't exist in the actual database, preserving your seed data
Additional Tips
- Dependency Injection Integration: If you're using a framework like NestJS, you can override the default
EntityManagerprovider in your test module to use the transactional one, so you don't have to pass it manually to every method. - Database Support: Ensure your database supports transactions (most modern databases like PostgreSQL, MySQL, SQLite do). Note that SQLite has some limitations with nested transactions, so avoid starting additional transactions inside your tests.
- Test Isolation: Make sure your test database is separate from your development/production database to avoid accidental data changes.
内容的提问来源于stack exchange,提问作者Ezequiel

