垃圾回收器如何与单元测试一起工作?

6
最近,我在StackOverflow上提出了一个问题(并回答了这个问题),关于为什么一个单元测试在单独运行时可以正常工作,但是当与整个批次的单元测试一起运行时会偶尔失败。请参见此处:SQL Server和TransactionScope(使用MSDTC):偶尔无法获取连接 单独运行单元测试通过,而一起运行时失败,通常意味着代码存在严重问题。
我发现存在一些资源泄漏。由于微妙的错误导致与SQL服务器的连接未被释放,因此我用尽了连接并且测试失败。据我所知,这几乎与内存泄漏完全相同;连接从连接池中分配并且永远不会被释放,就像内存可以分配然后不被释放一样。
然而,这给我留下了一个令人困惑的问题?运行我的测试一次和作为一组运行之间有什么区别?如果单独运行测试时测试通过,然后一起运行时测试失败,那么必须在测试运行之间发生某种清理,只有在单独运行测试时才会发生。
我猜测这可能与.NET垃圾回收器在测试之间执行或不执行的操作有关。在一个情况下,连接在测试之间被释放;在另一种情况下,它们没有。
我该如何解释这个问题?
更新:对于那些询问代码细节的人,它非常简单。我在我的Setup方法中声明了一个新的TransactionScope对象,并在我的Teardown方法中处理它。然而,问题测试是一个数据驱动测试,其中包含100个测试用例;在测试下的代码使用SqlHelper类从select语句中填充了一个SqlDataReader对象,然后没有调用close方法关闭SqlDataReader。因为我使用SqlHelper类来获取SqlDataReader,所以我期望连接由它处理。并非如此!
但是为了澄清,我不是在问我的具体情况。我想知道的是:通常情况下,如何在测试之间释放资源?我想象这将是垃圾回收的某种应用。我想知道垃圾回收器是否仍在清理上一个测试时运行下一个测试(竞态条件?)
更新:我了解到关于单元测试的垃圾回收的信息。出于自己的好奇心,我取出了因SqlDataReader对象未关闭而失败的单元测试。我尝试在每个测试的结尾添加System.GC.Collect()。这成功地释放了连接,但会导致约50%的性能损失。

如果您的连接不够用,并且在测试之间没有释放足够的连接,我会怀疑您拆除方法存在问题。当单独运行它们时,操作系统会处理某些拆除工作,但是作为一套运行时,资源使用会持续更长时间。 - MikeD
1
也许你需要在某些地方增加一些DisposeClose的调用? - Gabe
5个回答

4

听起来是可行的,没错。单元测试框架在测试之间请求垃圾回收运行并不奇怪。

或者,不同的执行模式可能会自然地在彼此运行时触发垃圾回收。分析这种情况的问题在于它非常动态,并且每次测试运行都会有所不同。

不要忘记,它可能不必释放所有测试之间的连接 - 只需要足够使它们保持运行即可...

垃圾回收本身在单元测试中不太可能表现出任何不同,除非测试运行器进程以特定方式配置。另一方面,无论您是否在调试器中运行测试,都会影响垃圾回收的积极性等。


Jon,如果你错过了的话,Lucero提到它显然会卸载整个AppDomain,这应该可以清除任何泄漏。 - Steven Sudit

2
垃圾回收是一个定期的后台任务。具体来说,有一个线程专门终止已被标记为死亡的对象。通过一次只运行一个测试,您给了该线程完成对象终止并关闭连接的机会。

真的吗?你能指给我一些文档,证明GC通常在自己的线程上运行吗? - Vivian River
1
https://dev59.com/xkXRa4cB1Zd3GeqPvNx9 - Steven Sudit
即使这被接受为答案,它也不是真正的原因。在资源丰富的机器上,GC 可能需要很长时间才能触发,特别是对于始终存活于第一代 GC 的可终止对象而言。真正的原因是为测试运行创建了应用程序域(可能是一个测试或测试批次),当这些应用程序域被卸载时,该域中的所有对象都会被终结,从而确定性地清理任何泄漏的资源。 - Lucero
@Steven,确实,如果在测试之间运行GC并且终结器线程足够快以运行终结器,您将不会注意到资源泄漏。但仍然是不确定的,并且仅运行GC无法解决问题,因为需要在GC运行后跟随终结化才能释放句柄(例如,如果终结器在一段时间内阻塞了终结器线程,则其他终结器将不会被执行,因此在该时间段内不会释放任何资源)。 - Lucero
1
@Lucero:所有这些归结为“有时有效,有时无效”,就像楼主所说的那样。正确的答案是像Gabe建议的那样正确使用IDsposable。 - Steven Sudit
显示剩余2条评论

2
通常每次测试运行都在单独的应用程序域中执行,原因有几个。现在当应用程序域被卸载时,它将释放与之关联的资源,以便关闭打开的连接,从而防止“泄漏”显现出来。

另请参见Cbrumme关于此主题的博客


啊,好的。如果它卸载了AppDomain,那么在其间强制进行垃圾回收就没有必要了。 - Steven Sudit
没错。由于卸载程序集的唯一方法是将它们放在单独的应用程序域中并卸载该应用程序域,因此“资源泄漏清理”就是其副作用。 - Lucero
嗯,它是否是副作用取决于你的意图。我不得不使用AppDomains来清理泄漏的资源,卸载程序集就是其副作用。 :-) - Steven Sudit
由于它改变了单个测试与测试批次的行为,因此在我看来,这是一个副作用。 ;) - Lucero

1

当单独运行每个单元测试时通过,但是一起运行时失败,这通常意味着代码存在严重问题。

我认为你编写的单元测试存在严重问题。每个测试应该独立于其他测试运行。一种方法是确保你有设置和拆卸方法([SetUp][TearDown]),它们创建和清理测试运行所需的环境。

在你的设置方法中,你创建连接,在拆卸方法中,你释放它。现在,在运行每个测试之前,将调用设置方法,在每个测试之后将调用拆卸方法,这将确保你不会泄漏任何资源。


0

哇,这里有多个问题!

首先,你希望你的单元测试系列要快速。不要通过访问数据库来测试业务逻辑等。

其次,如果你的生产代码泄漏资源(?)那就是你的主要问题。不要通过更改设置/拆卸测试代码来解决该问题。现在,如果你的测试代码分配了系统资源但没有正确处理,你需要以正确的方式解决它,而不是尝试控制垃圾回收器何时运行。你不应该担心这个问题。

第三,你真的不应该在单元测试中创建TransactionScope。对我来说这毫无意义。你在测试代码中使用的编码风格有问题。单元测试不仅仅是任何自动化测试,例如集成测试或系统测试。单元测试是小型和专注的测试,测试一个独立于所有其他生产代码的小型生产代码片段的行为。

现在,关于泄漏资源的提示。一个好的编程实践是在创建可处理对象时使用using语句,以确保这些资源被正确处理。

using (SqlDataReader reader = ...)
{
   ...
}

你如何在 using 中使用 SqlDataReader。它没有 dispose() 方法。 - Vivian River
兄弟,只需打开您的对象浏览器并检查一下,它继承自可处理的DbDataReader - 请参见下面的类型声明。顺便说一句,这些与数据库连接相关的大多数类型都必须是可处理的,因为它们包装了在使用时分配的各种操作系统资源。关闭读取器实际上是将其处理。使用语句的好处是,即使抛出异常,它们也会被处理,实际上是try {...} finally {foo.Dispose();} 的简化形式。public class SqlDataReader:DbDataReader,IDataReader,IDisposable,IDataRecord - Mahol25

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