Данная заметка рассматривает один из способов решения этой задачи.
Например, есть классс 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. С удовольствием почитаю, как можно решить задачу тестирования финализатора другими способами или какие есть недостатки у данного подхода.
Спасибо,
Игорь.
ссылка на оригинал статьи http://habrahabr.ru/post/182702/
Добавить комментарий