Как выполнить модульное тестирование финализатора?

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

person GraemeF    schedule 24.05.2010    source источник
comment
Здесь вы можете увидеть использование IDisposable. Это сработало для меня.   -  person Rogério Silva    schedule 22.07.2016


Ответы (4)


Я могу ошибаться, но:

GC.WaitForPendingFinalizers();

Может помочь - http://msdn.microsoft.com/en-us/library/system.gc.waitforpendingfinalizers.aspx

person Steven Robbins    schedule 24.05.2010
comment
Пример в контексте модульного теста был бы полезен - person Sinaesthetic; 10.09.2016

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

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

person Johannes Rudolph    schedule 24.05.2010
comment
Ага. Кроме того, вы никогда не получите 100% покрытие модульными тестами. В конце концов вам просто нужно верить, что ваш код будет работать, и если вы компетентны, то он должен работать. - person Mike Hanson; 24.05.2010
comment
На самом деле у меня была ошибка, потому что я забыл проверить флаг disposing в Dispose(), поэтому хотел добавить тест, прежде чем исправлять его. - person GraemeF; 24.05.2010

Я использую Appdomain (см. пример ниже). Класс TemporaryFile создает временный файл в конструкторе и удаляет его в Dispose или в финализаторе ~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));
    }
person constructor    schedule 09.06.2013
comment
Я не думаю, что есть какой-либо способ должным образом протестировать финализатор, учитывая, что существует безграничное количество сценариев многопоточности, в которых может работать финализатор. Финализаторы могут в конечном итоге работать на частично созданных объектах, и поэтому их обычно следует использовать только в классах, которые достаточно просты, чтобы можно было проверить все сценарии с помощью проверки. Если класс, использующий неуправляемые ресурсы, слишком сложен, чтобы его можно было легко проверить, ресурсы должны быть инкапсулированы в их собственном меньшем классе, чтобы класс, который содержит ссылку на объект, содержащий ресурсы, не нуждался в финализаторе. - person supercat; 11.06.2013
comment
Так близко! Это действительно очень умно. Это действительно заставляет работать финализатор и дает мне 90% того, чего я хотел. Однако в моем случае мне также нужно иметь возможность использовать прокладку Fakes, а код, работающий в AppDomain, не видит прокладку. Я также не могу создать прокладку внутри DoCallback, потому что она выйдет за рамки до запуска финализатора. Кто-нибудь разобрался с этим? - person Steve In CO; 24.04.2015
comment
@SteveInCO, не могли бы вы опубликовать вопрос с источниками вашего дела? Интересно посмотреть пример и поиск решения. - person constructor; 27.04.2015
comment
@constructor: в итоге я просто отказался от утверждения, которое хотел. Моя основная цель состояла в том, чтобы получить покрытие кода. (Я всегда стремлюсь к 100%, поэтому я могу легко определить, чего не хватает с первого взгляда, когда пишу новый код) Я полагаю, что мог бы обойти это с помощью временного файла, но для этого потребовался бы некоторый код, специфичный для теста, в моем классе. под тестом, который казался грязным. Может быть, если у меня будет время, я попытаюсь собрать что-нибудь, чтобы продемонстрировать и посмотреть, сможет ли кто-нибудь придумать лучшее решение. - person Steve In CO; 27.04.2015

Нелегко протестировать завершение, но может быть проще проверить, подлежит ли объект сборке мусора.

Это можно сделать со слабыми ссылками.

В тесте важно, чтобы локальные переменные вышли за рамки до вызова 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");
    }
person George Polevoy    schedule 20.01.2016