2010-05-24 61 views
16

我有以下类,这是用于IDisposable对象一个装饰(I省略其添加的东西),其本身使用一个共同的模式实现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(); 
    } 
} 

我可以很容易地测试innerDisposable是当Dispose()设置正所谓:

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

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

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

但我怎么写一个测试,以确保innerDisposable被终结得到安置?我想写这样的事情,但它失败了,大概是因为终结还没有被调用由GC线程:

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

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

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

[Here](http://stackoverflow.com/questions/3259456/should-dispose-methods-be-unit-tested)你可以看到使用IDisposable。这对我来说很好。 – 2016-07-22 15:24:25

回答

8

在编写单元测试时,应该总是尝试测试外部可见行为,而不是实现细节。有人可能会说,压制完成确实是外部可见的行为,但另一方面,你可能无法(也不应该)嘲笑garabage收集器。

你试图确定你的情况是遵循“最佳实践”或编码惯例。它应该通过一个为此目的而制作的工具来执行,例如FxCop

+1

是。另外,你的单元测试永远不会获得100%的覆盖率。最终,你必须相信你的代码能够工作,如果你能胜任,那就应该。 – 2010-05-24 09:15:36

+9

我实际上有一个错误,因为我忘记检查'Dispose()'中的'disposing'标志,所以想在修复它之前添加一个测试。 – GraemeF 2010-05-24 09:21:19

2

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

不幸的是,GC.WaitForPendingFinalizers();并不能帮助我测试终结者。

[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)); 
    } 
+0

我不认为有任何方法可以正确地测试终结器,因为有一个无限数量的终结器可能运行的线程场景。终结器最终可能会运行在部分构建的对象上,因此通常只能用于足够简单的类,以允许通过检查验证所有场景。如果一个使用非托管资源的类太复杂而无法轻松检查,那么这些资源应该封装在它们自己的较小类中,以便持有该资源的对象的引用的类不需要终结器。 – supercat 2013-06-11 00:01:41

+0

太近了!这真的很聪明。它确实迫使终结者跑步,并让我90%到达我想成为的地方。但在我的情况下,我需要能够使用Fakes Shim,并且在AppDomain中运行的代码不会看到Shim。我无法在DoCallback中创建填充程序,因为在终结程序运行之前它将超出范围。有没有人想过那个? – 2015-04-24 00:36:35

+0

@SteveInCO你可以发布你的案例来源的问题吗?有趣的是看到样本和搜索解决方案。 – constructor 2015-04-27 11:15:45

0

测试终结并不容易,但测试对象是否是垃圾收集的主题可能会更容易。

这可以用弱引用来完成。

在测试中,在调用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"); 
    }