【问题标题】:How does the garbage collector work with unit tests?垃圾收集器如何与单元测试一起工作?
【发布时间】:2011-03-06 12:28:28
【问题描述】:

最近,我在 StackOverflow 上询问(并回答)了一个问题,即为什么一个单元测试在单独运行时会起作用,然后在与整批单元测试一起运行时偶尔会失败。见这里:SQL Server and TransactionScope (with MSDTC): Sporadically can't get connection

单元测试在一次运行一个时通过,然后在一起运行时失败,这是代码出现严重问题的典型迹象。

我发现有一点资源泄漏。由于一个微妙的错误导致无法释放与 SQL 服务器的连接,我的连接用完了并且测试失败了。 AFAIK,这几乎就像内存泄漏一样工作;连接是从连接池中分配的,永远不会被释放,就像内存可以分配然后不被释放一样。

但是,这确实给我留下了一个令人费解的问题?一次运行一个测试和将它们作为一个套件运行有什么区别?如果测试在一次运行一个时通过,然后在一起运行时失败,则必须在测试运行之间进行某种清理,只有在一次运行一个测试时才会发生。

我猜想这可能与 .net 垃圾收集器在测试之间做什么或不做什么有关。在一种情况下,测试之间的连接被释放;在另一种情况下,它们不是。

我该如何解释?

更新:对于那些询问代码细节的人来说,这很简单。我在我的 Setup 方法中声明了一个新的 TransactionScope 对象,并在我的 Teardown 方法中处理它。然而,问题测试是一个包含 100 个测试用例的数据驱动测试;被测代码使用 SqlHelper 类从 select 语句中填充了一个 SqlDataReader 对象,然后没有在 SqlDataReader 上调用 close 方法。因为我使用 SqlHelper 类来获取 SqlDataReader,所以我希望为我处理连接。不是这样!

但为了澄清,我不是在问我的具体情况。我想知道的是:一般来说,测试之间的资源是如何释放的?我想这将是垃圾收集器的一些应用。我想知道垃圾收集器是否仍然可以在下一个测试运行时清理上一个测试(竞争条件?)

更新:我对单元测试垃圾收集的了解。 出于好奇,我删除了由于SqlDataReader 对象保持打开连接而失败的单元测试。我尝试将System.GC.Collect() 添加到每个测试的末尾。这成功地释放了连接,但确实会造成约 50% 的性能损失。

【问题讨论】:

  • 如果您因为在测试之间没有释放足够多的连接而导致连接用完,我怀疑您的拆卸方法有问题。当一次运行它们时,操作系统会负责一些拆除,但作为一个套件运行,您的资源使用会持续更长时间。
  • 也许您只是需要更多DisposeClose 来电?

标签: .net sql unit-testing garbage-collection


【解决方案1】:

哇,这里有多个问题!

首先,您希望您的单元测试系列速度快。不要打数据库测试业务逻辑等。

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

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

现在,关于泄漏资源的提示。一个好的编程习惯是在创建一次性对象时使用 using 语句来保证这些资源被正确处理。

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

【讨论】:

  • 如何将SqlDataReaderusing 一起使用。它没有dispose() 方法。
  • 伙计,它只是打开您的对象浏览器并检查它,它继承了一次性 DbDataReader - 请参阅下面的类型声明。顺便说一句,这些数据库连接相关类型中的大多数必须是一次性的,因为它们包装了在使用时分配的各种操作系统资源。关闭读者实际上是在处置它。 using-statements 的一个好处是即使抛出异常它们也会处理,实际上是 try { ... } finally { foo.Dispose();} public class SqlDataReader : DbDataReader, IDataReader, IDisposable, IDataRecord
【解决方案2】:

在运行单元测试时通过 一次,然后在运行时失败 在一起是一个经典的标志 出现严重问题 代码。

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

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

【讨论】:

    【解决方案3】:

    出于多种原因,通常每个测试运行都在单独的应用程序域中执行。现在,当 appdomain 被卸载时,它将释放与其关联的资源,从而关闭打开的连接,从而防止“泄漏”出现。

    另见Cbrumme's blog on this topic

    【讨论】:

    • 啊,好的。如果它卸载 AppDomain,则无需在两者之间强制进行垃圾收集。
    • 没错。由于卸载程序集的唯一方法是将它们放在单独的 appdomain 中并卸载 appdomain,因此“资源泄漏清理”是它的副作用。
    • 好吧,这是否是副作用取决于您的意图。我不得不使用 AppDomains 来清理泄漏的资源,卸载程序集是一个副作用。 :-)
    • 由于它改变了一个测试与一个测试批次的行为,在这种情况下,恕我直言,这是一个副作用。 ;)
    【解决方案4】:

    这听起来可行,是的。单元测试框架要求垃圾收集器在测试之间运行一点也不奇怪。

    或者,当它们一个接一个地运行时,不同的执行模式可能会自然地触发垃圾收集。分析这类事情的麻烦在于它是非常动态的 - 并且会因测试运行而异。

    不要忘记它可能不必释放 所有 测试之间的连接 - 足以让它们保持运行......

    垃圾收集器本身在单元测试中的行为不太可能有任何不同,除非测试运行程序进程以特定方式配置。另一方面,是否在调试器中运行测试影响垃圾收集器的热切程度,等等。

    【讨论】:

    • 乔恩,万一你错过了,Lucero 提到它显然会卸载整个 AppDomain,这应该可以消除任何泄漏。
    【解决方案5】:

    垃圾收集是一个周期性的后台任务。具体来说,有一个线程除了终结已经标记为死的对象之外什么都不做。通过一次运行一个测试,您可以让该线程有机会最终确定对象以关闭连接。

    【讨论】:

    • 真的吗?您能否指出一些说明 GC 通常在其自己的线程上运行的文档?
    • GC 实际上可以暂时停止所有线程,但它也会在线程中运行终结器,如链接中所述。
    • 即使这被接受为答案,这也不是真正的原因。在拥有大量资源的机器上,触发 GC 可能需要很长时间,尤其是对于始终在第一代 GC 中存活的可终结对象。真正的原因是为测试运行(可能是一个测试或一批测试)创建了应用程序域,当卸载这些应用程序域中的所有对象时,该应用程序域中的所有对象都已完成,这会确定性地立即清除所有泄漏的资源。
    • @Lucero:正如 OP 所说,所有这些都转化为“有时有效,有时无效”。正如 Gabe 建议的那样,正确的答案是正确使用 IDsposable。
    猜你喜欢
    • 2014-05-03
    • 2011-09-30
    • 1970-01-01
    • 2014-02-05
    • 1970-01-01
    • 1970-01-01
    • 2017-11-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多