Course contentsShow
← C#
Lesson 1659 of 1,90815. Software Quality and TestingFree lesson

Transactional Rollback Strategies

Ensure database cleanup by running tests within transactions that always rollback.

Transactional Rollback Strategies

In this lesson, you will learn how to maintain a pristine database state during integration testing by wrapping your test logic within a transaction that is always rolled back.

The Problem: Pollution

When writing integration tests that interact with a real database (e.g., using Entity Framework Core), each test often inserts, updates, or deletes records. If test A inserts a "User" and test B attempts to insert a user with the same unique email, test B will fail, not because of a bug, but because of test pollution. Manually deleting data between tests is error-prone and slows down your suite.

The Solution: The Transactional Wrapper

The most efficient strategy is to wrap the entire test execution within a database transaction. Because you explicitly roll back the transaction at the end of the test, the database remains in its original state, regardless of whether the test passed or failed.

Here is how you might structure this in a test class:

[Fact]
public async Task RegisterUser_ShouldSaveToDatabase()
{
    using var transaction = await _dbContext.Database.BeginTransactionAsync();
    
    // Arrange: Create service using the same context
    var service = new UserService(_dbContext);

    // Act: Perform operation
    await service.RegisterAsync("test@example.com");

    // Assert: Verify state
    var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.Email == "test@example.com");
    Assert.NotNull(user);

    // Rollback ensures no changes are committed to the DB
    await transaction.RollbackAsync();
}

Implementation Nuance

For this strategy to be effective, your production code must use the same DbContext instance as your test. If the service creates its own context internally, it will ignore the ambient transaction you started in your test. Use Dependency Injection to ensure the test injects the exact DbContext instance that holds the open transaction.

By utilizing IDbContextTransaction, you guarantee that your integration tests remain independent, fast, and deterministic, avoiding the "works on my machine" syndrome caused by lingering database artifacts.

Key Takeaway: Wrap integration tests in a database transaction and force a rollback after verification to ensure each test starts from a clean, predictable state without requiring manual cleanup.