【问题标题】:Rollback of nested transactions while unit testing stored procedures在单元测试存储过程时回滚嵌套事务
【发布时间】:2010-03-11 03:40:22
【问题描述】:

我正在尝试为一些 SQL 服务器存储过程和函数编写一些集成测试。我想要一个数据库,其中包含一组已知的测试数据,然后将每个测试包装在一个事务中,完成后将其回滚,以便测试有效地独立。

存储过程/函数可以做任何事情,从相当简单的连接查询到具有多层连接的复杂过滤,再到将数据插入多个表。

有几个实际使用事务的存储过程 - 因此这些更难测试。我将展示一个执行的整体代码示例,但请记住,这通常位于两个不同的位置(测试设置/拆卸和实际的存储过程)。对于这个示例,我还使用了一个非常简单的临时表:

CREATE TABLE #test (
  val nvarchar(500)
)

例子:

-- for this example, just ensuring that the table is empty
delete from #test
go


-- begin of test setup code --
begin transaction 
go
-- end of test setup code --

    -- begin of code under test --
    insert into #test values('aaaa')

    begin transaction 
    go

        insert into #test values('bbbbb')

    rollback transaction 
    go

    insert into #test values('ccccc')

    -- Example select #1:
    select * from #test

    -- end of code under test --

-- begin of test teardown --
rollback transaction 
go
-- end of test teardown

-- checking that #temp is still empty, like it was before test  
-- Example select #2:
select * from #test

这里的问题是,在“Example select #1”处,我希望“aaaa”和“cccc”在表中,但实际上只有“cccc”在表中,因为 SQL Server 实际上回滚了 ALL交易(见http://abdulaleemkhan.blogspot.com/2006/07/nested-t-sql-transactions.html)。此外,第二次回滚会导致错误,尽管可以通过以下方式避免这种情况:

-- begin of test teardown --
if @@trancount > 0 
begin
    rollback transaction 
end
go
-- end of test teardown

它并没有解决真正的问题:在“Example select #2”处,我们仍然在表中得到“cccc”——它不再被回滚,因为没有事务处于活动状态。

有没有办法解决这个问题?这种类型的测试有更好的策略吗?

注意:我不确定代码库在回滚后是否做过任何事情(插入“cccc”部分)——但如果它做过,无论是有意还是无意,测试可能会中断以奇怪的方式,因为意外数据可能会从另一个测试中遗留下来。


有点类似于Nested stored procedures containing TRY CATCH ROLLBACK pattern?,但这里提出的问题没有真正的解决方案。

【问题讨论】:

    标签: sql-server unit-testing integration-testing


    【解决方案1】:

    代码中的回滚不会嵌套。他们将所有内容回滚到第一个 BEGIN TRANSACTION。

    对于每个 BEGIN TRANSACTION,@@trancount 都会增加 1,但是,任何 ROLLBACK 都会将 @@trancount 设置回零。

    如果要回滚事务的一部分,则需要使用 TRANSACTION 保存点。您可以在 BOL 中查找它们,以获得比我在此处输入的更多信息。

    http://msdn.microsoft.com/en-us/library/ms188378.aspx

    【讨论】:

    • 这样做的一个问题是它需要修改内部异常——即被测试代码内部的异常(存储过程)。如果有人在代码库中编写事务时不使用这种模式,它仍然可能以不可预知的方式破坏测试。如果可以修改外部事务(测试框架的一部分)以使其适用于被测试代码中编写的“常规”事务,那就更好了。不过谢谢,否则这是一件好事。
    • @gregmac,TSQL 就是这样。我已经正确解释了为什么您的代码有问题,以及您有哪些选择。
    【解决方案2】:

    我想要一个数据库 其中有一组已知的测试数据,以及 然后将每个测试包装在一个事务中, 完成后将其回滚,以便 测试实际上是独立的。

    不要。一方面,您不会实际测试功能,因为在现实世界中,程序将提交。其次,这一点更为重要,您将收到大量错误错误,并且需要实施变通方法来读取脏数据,因为您实际上并没有提交,而且您无法进行任何适当的验证。

    而是使用众所周知的集合备份数据库,然后在测试前快速恢复它。将测试分组到套件中,这些套件都可以在新的数据库还原上运行而不会相互影响,因此您可以减少所需的还原次数。

    您还可以使用数据库快照,在套件启动时拍摄快照,然后在每次测试前从快照中恢复数据库,请参阅How to: Revert a Database to a Database Snapshot (Transact-SQL)

    或者结合这两种方法:套件设置(即单元测试@class方法)从.bak文件中恢复数据库并创建快照,然后每个测试从快照中恢复数据库。

    【讨论】:

    • 我应该更清楚地了解设置:实际的测试断言将在测试结束和拆卸之前完成——因此,此时数据将被提交。
    • 数据无法提交,稍后回滚。此时未提交数据。
    【解决方案3】:

    我对这种设置也有类似的问题,我的看法是创建一个“SetupTest”脚本和一个“ClearTest”脚本,以便在测试执行之前和之后运行。除非您在这里谈论大量数据——这会使测试执行太慢,否则这应该可以很好地工作并使测试可重复,因为您知道每次运行该测试套件时,您都会得到正确的等待执行的数据。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-22
      • 1970-01-01
      相关资源
      最近更新 更多