Одной из не очевидных задач, является тестирование кода, реализованного в финализаторе дотнетовского класса.
Данная заметка рассматривает один из способов решения этой задачи.
Например, есть классс 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