我该如何对终结器进行单元测试?

21
我有一个类,它是一个 IDisposable 对象的装饰器(我省略了它添加的内容),自己使用了一种常见的模式实现了 IDisposable
public class DisposableDecorator : IDisposable
{
    private readonly IDisposable _innerDisposable;

    public DisposableDecorator(IDisposable innerDisposable)
    {
        _innerDisposable = innerDisposable;
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion

    ~DisposableDecorator()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
            _innerDisposable.Dispose();
    }
}

当调用Dispose()时,我可以轻松测试innerDisposable是否被处理:

[Test]
public void Dispose__DisposesInnerDisposable()
{
    var mockInnerDisposable = new Mock<IDisposable>();

    new DisposableDecorator(mockInnerDisposable.Object).Dispose();

    mockInnerDisposable.Verify(x => x.Dispose());
}

但是我该如何编写一个测试来确保 innerDisposable 不会被 finalizer 销毁?我想编写下面的代码,但它失败了,可能是因为 GC 线程还没有调用 finalizer:

[Test]
public void Finalizer__DoesNotDisposeInnerDisposable()
{
    var mockInnerDisposable = new Mock<IDisposable>();

    new DisposableDecorator(mockInnerDisposable.Object);
    GC.Collect();

    mockInnerDisposable.Verify(x => x.Dispose(), Times.Never());
}

在这里,你可以看到IDisposable的使用。对我来说这很有效。 - Rogério Silva
4个回答

16

1
在单元测试环境中提供一个示例会很有帮助。 - Sinaesthetic

7
在编写单元测试时,您应该始终尝试测试可见行为而不是实现细节。有人可能会认为抑制终结确实是可见行为之外的行为,但另一方面,您可能无法(也不应该)模拟垃圾收集器。
您在这种情况下要确保的是遵循“最佳实践”或编码实践。它应该通过专门用于此目的的工具来强制执行,例如FxCop

1
没错。此外,您永远无法为单元测试获得100%的覆盖率。最终,您只需要相信您的代码将正常工作,如果您很有能力,它应该会。 - Mike Hanson
12
我实际上遇到了一个 bug,因为我在 Dispose() 方法中忘记检查 disposing 标志,所以想在修复它之前先添加一个测试。 - GraemeF

2

我使用Appdomain(见下面的示例)。 TemporaryFile类在构造函数中创建临时文件,在Dispose或finalizer ~TemporaryFile()中删除它。

不幸的是,GC.WaitForPendingFinalizers()无法帮助我测试finalizer。

    [Test]
    public void TestTemporaryFile_without_Dispose()
    {
        const string DOMAIN_NAME = "testDomain";
        const string FILENAME_KEY = "fileName";

        string testRoot = Directory.GetCurrentDirectory();

        AppDomainSetup info = new AppDomainSetup
                                  {
                                      ApplicationBase = testRoot
        };
        AppDomain testDomain = AppDomain.CreateDomain(DOMAIN_NAME, null, info);
        testDomain.DoCallBack(delegate
        {
            TemporaryFile temporaryFile = new TemporaryFile();
            Assert.IsTrue(File.Exists(temporaryFile.FileName));
            AppDomain.CurrentDomain.SetData(FILENAME_KEY, temporaryFile.FileName);
        });
        string createdTemporaryFileName = (string)testDomain.GetData(FILENAME_KEY);
        Assert.IsTrue(File.Exists(createdTemporaryFileName));
        AppDomain.Unload(testDomain);

        Assert.IsFalse(File.Exists(createdTemporaryFileName));
    }

我认为没有任何方法可以正确地测试终结器,因为在终结器可能运行的无限数量的线程场景下。终结器可能会在部分构造对象上运行,因此通常只应在足够简单以允许通过检查验证所有情况的类上使用它们。如果使用非托管资源的类过于复杂以至于无法轻松检查,则应将资源封装在自己的较小类中,以便持有资源对象引用的类不需要终结器。 - supercat
太接近了!这真的非常聪明。它确实强制执行终结器,并让我达到了90%的目标。但是在我的情况下,我还需要能够使用Fakes Shim,而在AppDomain中运行的代码看不到Shim。我也不能在DoCallback内部创建shim,因为在终结器运行之前,它将超出范围。有人解决了这个问题吗? - Steve In CO
@SteveInCO能否发布你的案例和源代码?很有趣并且可以寻找解决方案。 - constructor
@constructor:最終我決定不使用我想要的斷言。我的主要目標是獲得代碼覆蓋率。(我總是追求100%,這樣我可以在寫新代碼時一眼看出缺少什麼)我想我本可以通過使用臨時文件來解決問題,但那需要在我正在測試的類中添加一些特定於測試的代碼,這似乎有些“不潔”。也許如果我有時間,我會嘗試做一些示例,看看是否有人能夠提出更好的解決方案。 - Steve In CO

1

测试终结并不容易,但如果一个对象是垃圾回收的主题,那么测试可能会更容易。

可以使用弱引用来实现这一点。

在测试中,重要的是让局部变量在调用GC.Collect()之前超出作用域。最简单的方法是使用函数作用域。

    class Stuff
    {
        ~Stuff()
        {
        }
    }

    WeakReference CreateWithWeakReference<T>(Func<T> factory)
    {
        return new WeakReference(factory());
    }

    [Test]
    public void TestEverythingOutOfScopeIsReleased()
    {
        var tracked = new List<WeakReference>();

        var referer = new List<Stuff>();

        tracked.Add(CreateWithWeakReference(() => { var stuff = new Stuff(); referer.Add(stuff); return stuff; }));

        // Run some code that is expected to release the references
        referer.Clear();

        GC.Collect();

        Assert.IsFalse(tracked.Any(o => o.IsAlive), "All objects should have been released");
    }

    [Test]
    public void TestLocalVariableIsStillInScope()
    {
        var tracked = new List<WeakReference>();

        var referer = new List<Stuff>();

        for (var i = 0; i < 10; i++)
        {
            var stuff = new Stuff();
            tracked.Add(CreateWithWeakReference(() => { referer.Add(stuff); return stuff; }));
        }

        // Run some code that is expected to release the references
        referer.Clear();

        GC.Collect();

        // Following holds because of the stuff variable is still on stack!
        Assert.IsTrue(tracked.Count(o => o.IsAlive) == 1, "Should still have a reference to the last one from the for loop");
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接