2010-05-24 6 views
16

я следующий класс, который является декоратором для 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(); 
    } 
} 

можно легко проверить, что 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

[Здесь] (http://stackoverflow.com/questions/3259456/should-dispose-methods-be-unit-tested) вы можете увидеть использование IDisposable. Это сработало для меня. –

ответ

8

При написании модульных тестов вы всегда должны проверять внешнее видимое поведение, а не детали реализации. Можно утверждать, что подавление финализации действительно является наружным видимым поведением, но, с другой стороны, вы, вероятно, не сможете (и не должны) издеваться над сборщиком габаритов.

Что вы пытаетесь убедиться в вашем случае, заключается в том, что соблюдаются «лучшие практики» или практика кодирования. Он должен быть принудительно применен с помощью инструмента, созданного для этой цели, такого как FxCop.

+1

Yup. Кроме того, вы никогда не получите 100% -ый охват своих модульных тестов. В конце концов вам просто нужно верить, что ваш код будет работать, и если вы компетентны, то он должен. –

+9

У меня на самом деле была ошибка, потому что я забыл проверить флаг 'disposing' в' Dispose() ', поэтому хотел добавить тест, прежде чем исправлять его. – GraemeF

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

+0

Так близко! Это действительно очень умно. Это действительно заставляет финализатор бежать, и он получает меня на 90% туда, где я хотел быть. Однако в моем случае мне нужно также использовать Fakes Shim, а код, запущенный в AppDomain, не видит Shim. Я не могу создать прокладку внутри DoCallback либо из-за того, что она закончится до запуска финализатора. Кто-нибудь понял, что один из них? –

+0

@SteveInCO вы можете опубликовать вопрос с источниками вашего дела? Интересно видеть пример и решение для поиска. – constructor

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"); 
    }