TransactionScope会导致阻塞吗?

4
我正在编写一些针对数据库的单元测试,我们使用事务来确保在结束时删除测试数据。
我遇到了一个问题,那就是我要测试的方法正在使用它们自己的TransactionScope对象,当访问数据库时似乎会被阻塞。
以下是我的测试基类中的代码:
BaseScope = new CommittableTransaction(new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUnCommitted, Timeout = new System.TimeSpan(0, 5, 0) });

然后在我正在测试的方法内部,它执行以下操作:

using (TransactionScope scope = new TransactionScope())

当第二个作用域内的代码首次触及数据库时,它会挂起。我有什么解决这个问题的方法吗?

3个回答

6
如果您正在使用数据库,那么您并没有进行单元测试,而您所遇到的问题正是真正的单元测试使用模拟和存根的原因之一。
现在,您所做的测试非常有价值,在某些情况下,我实际上会选择它们而不是单元测试。我将其标记为早期集成测试(EIT)。关键点在于,当我们与真实环境而不是单元测试模拟一起工作时,我们会发现一整个新类的错误。而这里的关键是“真实环境”。一旦您使用人工事务范围等虚拟环境,您就会失去EIT的许多好处,因为您无法捕捉微妙的交互错误,或者(就像您的情况一样)引入人为问题。
我建议找到一种快速填充数据库的方法,并在测试之外将其恢复到该状态。对于这些类型的测试,"重置为已知状态"脚本非常有帮助。

我认为这是我最终会选择的选项。我编写了一些代码来创建一个在测试开始时运行的新/空数据库,而在结束时删除该数据库。 - Jonas

5
当你嵌套TransactionScope实例时,可能会出现分布式事务而不是简单的本地事务。这种行为在使用的数据库之间有所不同。例如,SQLServer 2008不会升级到DTX,除非实际涉及多个数据库。另一方面,Oracle总是会升级到分布式事务,因为它不支持共享连接以进行单个本地事务。
根据你使用的数据库和TransactionScopeOption,你可能会遇到死锁。这是因为DTX通常需要表锁来确保它们可以被原子提交。例如,在Oracle中,如果你启动了一个DTX并在完成之前崩溃或失去连接,你可能会得到一个“In Doubt Distributed Transaction”。这个“In Doubt”事务可能会锁定一个或多个表,阻止其他会话修改它们,直到DBA对挂起的事务ID执行ROLLBACK FORCE命令。一些数据库(如SQLServer)试图检测这样的死锁并终止其中一个有问题的事务...但这并不保证发生。
我建议你有两种选择: 1. 决定是否真的需要编写与数据库交互的测试。通常情况下,你可以使用模拟对象或存根来避免编写修改然后回滚数据库的测试的需要。避免这样的问题是有意义的,因为它既加快了你的测试速度,又消除了潜在的依赖关系。 2. 如果你确实需要针对数据库测试你的逻辑,请考虑修改代码,使所有方法都使用相同的数据库连接来执行它们的SQL。这将消除分布式事务的创建,并希望解决你的问题。
你还可以查看数据库的挂起事务视图(在Oracle中称为PENDING_TRANS$...在SQLServer中有XACT_STATE()函数)。

3
你需要提交基本事务以解除测试方法的阻塞,我认为这不是你想要的行为。你需要让测试方法的事务加入到在基类中创建的外部“环境”(伞/父/基础/外部)事务中,而不是尝试创建自己的事务。
从MSDN CommittableTransactionClass备注中(重点是我的):
建议使用TransactionScope类创建隐式事务,以便自动管理环境事务上下文。对于需要跨多个函数调用或多个线程调用使用相同事务的应用程序,还应使用TransactionScope和DependentTransaction类。有关此模型的更多信息,请参见“使用Transaction Scope实现隐式事务”主题。创建CommittableTransaction不会自动设置环境事务,即代码执行的事务。您可以通过调用全局Transaction对象的静态Current属性来获取或设置环境事务。有关环境事务的更多信息,请参见“使用Transaction Scope Option管理事务流”部分的“使用Transaction Scope实现隐式事务”主题。如果未设置环境事务,则资源管理器上的任何操作都不属于该事务。您需要明确设置和重置环境事务,以确保资源管理器在正确的事务上下文中运行。在提交CommittableTransaction之前,所有涉及到的资源仍然被锁定。
正如djna所指出的那样,使用事务来回滚测试期间所做的更改是相当滥用的。你的测试应该是一个良好的公民,并在finally子句中撤消它自己对数据库所做的任何更改,以便在它之后可能运行的其他测试不会受到影响。如果你已经有很多不规范的测试,那么现在你可能不会选择这条路。在这种情况下,将你的基础更改为使用隐式事务,范围设置为RequiresNew,并在你的测试方法中使用Required

我只想指出,我对此的否决与其完全无关,并且似乎是一种“报复”投票,因为我同时收到了五个针对较早回答的负分 :/ - Mike Atlas
谢谢Damian :) 我非常确定我的答案是非常好的。 - Mike Atlas

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