Skip to content

Finished test class instances stay reachable until the test session ends #7260

Closed

Description

@thomhurst

Problem

After a test finishes and its class instance has been disposed, TestContext.Metadata.TestDetails.ClassInstance still references the instance, and the session keeps every TestContext (TestSessionContext → AssemblyHookContext → ClassHookContext → its list of TestContexts). As a result, every object a finished test stored in an instance field remains reachable until the whole session ends.

That is harmless for small fields but expensive for suites where each test owns a heavy resource in a field (for example a per-test WebApplicationFactory, its host and DI container, an EF Core model, or large buffers). Disposal releases unmanaged resources, but the managed graph stays on the heap, so working set grows linearly with the number of completed tests.

Minimal repro (TUnit 1.76.0, net10.0)

public class Holder
{
    private byte[]? _payload;

    [Test]
    [MatrixDataSource]
    public async Task Allocate([MatrixRange<int>(1, 20)] int n)
    {
        _payload = new byte[50 * 1024 * 1024];
        Random.Shared.NextBytes(_payload);
        await Task.Yield();
    }

    [After(TestSession)]
    public static void Report()
    {
        GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
        Console.WriteLine($"RETAINED_MB={GC.GetTotalMemory(true) / (1024 * 1024)}");
    }
}

Output: RETAINED_MB=1000, meaning all 20 finished instances (20 x 50 MB) are still reachable at session end. Expected: close to 0, since every test finished and its instance was disposed.

A heap dump of a larger suite shows this root chain for a finished test's instance:

TestSessionContext
 -> List<AssemblyHookContext> -> AssemblyHookContext
 -> List<ClassHookContext> -> ClassHookContext
 -> List<TestContext> -> TestContext (finished test)
 -> <test class instance> -> <instance field> -> ...

Proposed fix

Once the instance has been disposed (after DisposeTestInstanceWithSpanAsync in TestCoordinator), replace TestDetails.ClassInstance with a sentinel such as PlaceholderInstance.Instance, so result reporting keeps the test metadata without keeping user state alive. Any other per-test references to user objects that are no longer needed after the test completes (for example the tracked-object sets) could be cleared at the same point.

Acceptance

  • The repro above reports roughly 0 MB retained.
  • A regression test asserts that a finished test's class instance is collectable (weak reference) before the session ends.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingperformancePerformance optimization

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions