Как тестировать код финализатора (c#)

в 21:54, , рубрики: .net, appdomain, dispose, finalizer, idisposable, nunit, testing, тестирование, финализатор, метки: , , , , , , , ,

Одной из не очевидных задач, является тестирование кода, реализованного в финализаторе дотнетовского класса.
Данная заметка рассматривает один из способов решения этой задачи.


Например, есть классс TemporaryFile (временный файл), который создает уникальный временный файл в конструкторе и должен удалять его в Dipose() или в финализаторе.

    public class MyTemporaryFile : IDisposable
    {
        public string FileName { private set; get; }
        public MyTemporaryFile()
        {
            FileName = Path.GetTempFileName();
        }

        public void Dispose()
        {
            Dispose(true);
        }
        ~MyTemporaryFile()
        {
            Dispose(false);
        }

        void Dispose(bool disposing)
        {
            if (disposing)
            {
                GC.SuppressFinalize(this);
            }
            DeleteFile();
        }
        void DeleteFile()
        {
            if (FileName != null)
            {
                File.Delete(FileName);
                FileName = null;
            }
        }
    }

Реализация паттерна Dispose довольно стандартная и обсуждалась на Хабре. Наверняка есть в данной реализации некоторые тонкие места, поэтому в «настоящей» программе имейте это ввиду.

Но почему то я не нашел обсуждения вопроса, а как же тестировать код, реализованный в финализаторе.

Понятно, что «очень наивная» реализация теста работать не будет.

        [Test]
        public void TestMyTemporaryFile_without_Dispose()
        {
            var temporaryFile = new MyTemporaryFile();
            string createdTemporaryFileName = temporaryFile.FileName;

            Assert.IsTrue(File.Exists(createdTemporaryFileName));

            temporaryFile = null;
            
            Assert.IsFalse(File.Exists(createdTemporaryFileName));
        }

Дело в том, что присвоение null переменной temporaryFile не вызывает финализатор.

Встречался совет вызывать GC.WaitForPendingFinalizers();, но почему то в данном тесте мне это не помогло.

offtopic: Когда то давно на какой то лекции по c# рассказывали про AppDomain. Я тогда не очень понимал зачем мне это надо. Ну вы знаете, как большинство лекторов рассказывают для "некого среднего слушателя" "некие общие вещи". Я ни разу не смог понять паттерн Dispose со слов лектора. Самое смешное, что после того, как я стал его чуть чуть понимать, я с трудом стал догадываться, что лектор таки имеет ввиду.

Так вот, оказывается, что с помощью AppDomain можно легко приготовить тест для кода финализатора:

        [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
            {
                MyTemporaryFile temporaryFile = new MyTemporaryFile();
                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)); 
        }

Как известно, AppDomain.Unload(testDomain); выгружает код и очищает память (в том числе и вызываются финализаторы).
Это и помогает «насильно вызвать» финализатор и, соответсвенно, протестировать его код.

Примечания:
1. Один из лекторов советовал в финализаторе выкидывать исключительную ситуацию (exception) со словами "вызови Dispose, идиот". Где то он может быть и прав, но если есть unmanaged ресурс, надо предусмотреть и финализатор тоже.
2. Реализация класса MyTemporaryFile очень схематична и не рекомендуется для продакшен использования.
3. Скорей всего реализация данного теста, тоже имеет всякие тонкие моменты, но многолетняя практика ни разу не зафиксировала ложное срабатывание этого теста.
4. С удовольствием почитаю, как можно решить задачу тестирования финализатора другими способами или какие есть недостатки у данного подхода.

Спасибо,
Игорь.

Автор: constructor

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js