Home > Software design >  Unit Testing WeakReference
Unit Testing WeakReference

Time:11-23

It seems the following code to unit test WeakReference does not work correctly/reliably:

object realObject = new object();
WeakReference weakReference = new WeakReference(realObject);
Assert.NotNull(weakReference.Target);
realObject = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Assert.Null(weakReference.Target);

The testing is run targeting both net461 and net5.0, for both DEBUG and RELEASE mode. The best result:

So the problem narrows down to targeting net5.0 DEBUG mode.

I also came across the following posts but it seems they're not very helpful mostly because they're out-of-date:

Worth to mention the answer from @Eric Lippert for Why C# Garbage Collection behavior differs for Release and Debug executables? says: You absolutely cannot rely on the garbage collector having any particular behaviour whatsoever with respect to the lifetime of a local, which suggests testing a WeakReference cannot be done reliably.

Edit: More information about the context of why using a WeakReference:

I'm developing a framework providing class Entity<T> and Record<T> similar to System.Data.DataRow and System.Data.DataRowView, with the most significance that the prior one is strongly typed. Entity is the model provides its own event to notify changes, Record<T> is a viewmodel that wraps Entity<T> to translate the change event into INotifyPropertyChanged plus other things such as databinding, validations, etc. Entity<T> should not aware the existence of Record<T>. To avoid memory leak, Record<T> should be GC reclaimed when only Entity is still referenced.

The example code for the event hookup:

IEntityListener Implemented by Record<T> class:

internal interface IEntityListener
{
    void OnFieldUpdated(FieldUpdatedEventArgs e);
}

WeakEntityListner to do the event hookup:

internal sealed class WeakEntityListener
{
    private readonly WeakReference<IEntityListener> _weakListener;
    private readonly Entity _entity;

    internal WeakEntityListener(Entity entity, IEntityListener listener)
    {
        _entity = entity;
        _weakListener = new (listener);
        _entity.FieldUpdated  = OnFieldUpdated;
    }

    private void OnFieldUpdated(object? sender, FieldUpdatedEventArgs e)
    {
        if (_weakListener.TryGetTarget(out var listener))
            listener.OnFieldUpdated(e);
        else
            CleanUp();
    }

    private void CleanUp()
    {
        _entity.FieldUpdated -= OnFieldUpdated;
    }
}

I would like to have method OnFieldUpdated and CleanUp fully covered by unit test.

CodePudding user response:

Now that you've explained the reason you are trying to use WeakReference. The solution you really need is to implement IDisposable. This gives the caller precise control of the lifetime of your classes, allowing the garbage collector to release resources as soon as they are unused.

I would consider the use of WeakReference to avoid IDisposable an anti-pattern. Garbage collection is an expensive operation. Each version of the .net runtime may tweak how the garbage collector works, how often it occurs, and what garbage is removed. Until that collection occurs, every instance of IEntityListener will continue to receive events after it's lifetime has been completed. Forcing any implementation to require extra guard code to prevent mis-behaving.

Though implementing IDisposable has an annoying tendency to spread throughout every class. The tradeoff is worth it. Even if you are building a framework for others to reuse. I don't believe that using IDisposable as part of the contract between client code and framework would dissuade anyone from using your library. However, attempting to use WeakReference to invert this contract, might give your library a negative reputation.

CodePudding user response:

I ended up with the following solution, inspired by runtime WeakReference unit test source code suggested by @Jeremy Lakeman (https://github.com/dotnet/runtime/blob/main/src/libraries/System.Runtime/tests/System/WeakReferenceTests.cs)

using System;
using Xunit;
using Xunit.Abstractions;

public class WeakReferenceTests
{
    private sealed class Latch
    {
        public bool FinalizerRan { get; set; }
    }

    private sealed class TestObject
    {
        ~TestObject()
        {
            Latch.FinalizerRan = true;
        }

        public Latch Latch { get; } = new();
    }

    private readonly ITestOutputHelper output;

    public WeakReferenceTests(ITestOutputHelper output)
    {
        this.output = output;
    }

    [Fact]
    public void Test1()
    {
        TestObject realObject = new TestObject();
        Latch l = realObject.Latch;
        Assert.False(l.FinalizerRan);
        var weakReference = new WeakReference(realObject);
        Assert.NotNull(weakReference.Target);
        GC.KeepAlive(realObject);
        realObject = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        if (!l.FinalizerRan)
            output.WriteLine("Attempted GC but could not force test object to finalize. Test skipped.");
        else
            Assert.Null(weakReference.Target);
    }
}
  • Related