单元测试内存泄漏

22
我有一个应用程序存在很多内存泄漏。例如,如果我打开并关闭视图10次,我的内存消耗会上升,因为视图没有完全清理干净。这些就是我的内存泄漏。从测试驱动的角度,我想编写一个测试来证明我的内存泄漏,并在修复了内存泄漏后进行断言。这样,我的代码不会在以后出现问题。简而言之:

是否有一种方法可以通过单元测试来断言我的代码没有内存泄漏?

例如,我可以像这样做:

objectsThatShouldNotBeThereCount = MemAssertion.GetObjects<MyView>().Count;
Assert.AreEqual(0, objectsThatShouldNotBeThereCount);

我对性能调优不感兴趣。我使用Ants分析器(我非常喜欢)但也想编写测试来确保“泄漏”不会再次出现。

我正在使用C# / Nunit,但对任何人在这方面的理念都很感兴趣...

6个回答

12

通常情况下,当托管类型在使用非托管资源时没有注意时会导致内存泄漏。

这种情况的经典示例是 System.Threading.Timer,它将回调方法作为参数。由于计时器最终使用了非托管资源,因此引入了新的GC根,只能通过调用计时器的Dispose方法来释放。在这种情况下,您的类型还应实现IDisposable接口,否则该对象永远不能被垃圾回收(即发生了内存泄漏)。

您可以编写一个单元测试来对此情况进行测试,例如:

var instance = new MyType();

// ...
// Use your instance in all the ways that
// may trigger creation of new GC roots
// ...

var weakRef = new WeakReference(instance);

instance.Dispose();
instance = null;

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Assert.IsFalse(weakRef.IsAlive);

请注意,这将在DotNet框架中工作,但由于Mono中GC实现的怪癖,它无法正常工作。要使其在mono中正常工作,请在单独的方法中创建WeakReference。请参见此处:https://dev59.com/gGgu5IYBdhLWcg3wUlft - tzachs

5
记忆消耗增加并不一定表示资源泄漏,因为垃圾回收是非确定性的,可能尚未启动。即使您“放弃”了对象,CLR 也可以在系统上有足够的资源的情况下继续保留它们。
如果您知道实际上存在资源泄漏,您可以使用具有显式 Close/Dispose 的对象,作为其合同的一部分(用于“using…”构造)。在这种情况下,如果您控制类型,则可以从其 Dispose 实现中标记对对象的处理,以验证它们实际上已被处理,如果您可以接受生命周期管理泄漏到类型的接口中。
如果您执行后者,就可以单元测试合同处置是否发生。我做过几次,使用特定于应用程序的相当于 IDisposable(扩展该接口),添加了查询对象是否已被处理的选项。如果您在类型上明确实现该接口,则不会过度污染其界面。
如果您无法控制相关类型,则需要内存分析器,如其他地方建议的那样。(例如 Jetbrains 的 dotTrace。)

你的意思是我可以测试我的Dispose方法是否针对特定对象被调用。虽然这是一个非常具体的测试,但这将是一个很好的开始。 - Gluip
在事后进行契约处理的测试很难。这些测试应该属于应用程序本身的单元测试。我使用的另一种方法是,在类型的析构函数中使用System.Diagnostics.Assert失败(但要注意!),如果未设置IsDisposed标志,则会发生违反契约的Dispose。这告诉你(在垃圾收集时)它发生了,但不知道如何发生。然而,如果与对象实例化时间的StackTrace快照保持结合使用,您可以找到实例化它的人并回溯为什么它没有被处理。 - Cumbayah

1

dotMemory Unit 框架具有编程能力,可检查分配的特定对象数量、内存流量,制作和比较内存快照。


1

你不需要单元测试,你需要内存分析器。你可以从CLR Profiler开始。


8
我已经在使用性能分析器,但我希望“固定”我的结果,这样针对相同情景就能轻松地创建新的泄漏。 - Gluip

0

你可能可以钩入分析 API,但看起来你需要启动启用了分析器的单元测试。

对象是如何创建的?是直接创建还是通过某种可控制的方式创建。如果可控制,请返回扩展版本,并注册终结器以表明它们已被处理。

GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(HasAllOfTypeXBeenFinalized());

好主意。不幸的是,我直接创建我的对象,所以无法为测试而包装它们添加额外的功能。 - Gluip

0

可以考虑这样做:

long originalByteCount = GC.GetTotalMemory(true);
SomeOperationThatMayLeakMemory();
long finalByteCount = GC.GetTotalMemory(true);
Assert.AreEqual(originalByteCount, finalByteCount);

我刚试了一下,这个不起作用。据我所知,nunit的跟踪会给它添加噪音,因此本应是中性的操作突然变得不中性了。 - Johannes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接