在单元测试之间恢复数据库:数据库仍在使用中

Posted

技术标签:

【中文标题】在单元测试之间恢复数据库:数据库仍在使用中【英文标题】:Recover database between unit tests: Database is still in use 【发布时间】:2016-01-05 17:29:28 【问题描述】:

我们得到了一些使用 SQL Server 数据库的单元测试。为了至少使每个测试夹具独特且独立于其他测试夹具,我尝试在每个测试夹具开始之前恢复数据库。每个测试都在其例程中打开和关闭连接和数据库。

在第一个测试夹具之前恢复数据库效果很好,但在第二个测试夹具之前恢复数据库(在打开和关闭连接和数据库之后)就不行了。

我封装并隔离了问题。下面是两个示例测试(NewUnitTest1 将首先执行):

using NUnit.Framework;
using System.Data;
using System.Data.SqlClient;

    [TestFixture]
    class UnitTest1 : BaseTest
    
        [Test]
        public void NewUnitTest1()
        
            string conString = ConnectionStringHelper.GetConnectionString(SCADADatabases.ConfigurationDatabase); // Helper method to optain connection string, is correct
            using (SqlConnection dbConn = new SqlConnection(conString))
            
                SqlCommand cmd = dbConn.CreateCommand();
                cmd.CommandText = "SELECT * FROM TB_PV";
                SqlDataAdapter da = new SqlDataAdapter(cmd);
                DataSet ds = new DataSet();
                da.Fill(ds);
             // Dispose should close connection and database ...
            Assert.IsTrue(true);
        
    

    [TestFixture]
    class UnitTest2 : BaseTest
    
        [Test]
        public void NewUnitTest2()
        
            Assert.IsTrue(true);
        
    

基础测试类将在每个测试夹具之前完成恢复工作:

using My.Core.Helper;
using My.Core.UnitTest.Properties;
using My.DBRestore.Core;
using My.Domain;
using Microsoft.SqlServer.Management.Smo;
using NUnit.Framework;
using System;
using System.Data;
using System.IO;

    /// <summary>
    /// Base test class any test class can inherit from. 
    /// </summary>
    public abstract partial class BaseTest
    
        /// <summary>
        /// Executes before all tests of this class start. 
        /// </summary>
        [TestFixtureSetUp]
        public virtual void FixtureSetUp()
        
            Console.WriteLine("Recover database ... ");
            restoreDatabase("MYDATABASE", @"D:\MYBACKUP.BAK", "");
            Console.WriteLine("Run tests in " + this.GetType() + " ...");
        

        private void restoreDatabase(string destinationDatabase, string backupFile, string dbPath)
        
            Microsoft.SqlServer.Management.Smo.Server sqlServer = new Microsoft.SqlServer.Management.Smo.Server(Properties.Settings.Default.SQLInstance);

            Microsoft.SqlServer.Management.Smo.Restore restore = new Microsoft.SqlServer.Management.Smo.Restore();
            restore.Action = Microsoft.SqlServer.Management.Smo.RestoreActionType.Database;
            restore.Devices.Add(new Microsoft.SqlServer.Management.Smo.BackupDeviceItem(backupFile, Microsoft.SqlServer.Management.Smo.DeviceType.File));

            System.Data.DataTable dt = restore.ReadBackupHeader(sqlServer);
            restore.FileNumber = Convert.ToInt32(dt.Rows[dt.Rows.Count - 1]["Position"]);

            dt = restore.ReadFileList(sqlServer);
            int indexMdf = dt.Rows.Count - 2;
            int indexLdf = dt.Rows.Count - 1;
            Microsoft.SqlServer.Management.Smo.RelocateFile dataFile = new Microsoft.SqlServer.Management.Smo.RelocateFile();
            string mdf = dt.Rows[indexMdf][1].ToString();
            dataFile.LogicalFileName = dt.Rows[indexMdf][0].ToString();
            dataFile.PhysicalFileName = Path.Combine(dbPath, destinationDatabase + Path.GetExtension(mdf));


            Microsoft.SqlServer.Management.Smo.RelocateFile logFile = new Microsoft.SqlServer.Management.Smo.RelocateFile();
            string ldf = dt.Rows[indexLdf][1].ToString();
            logFile.LogicalFileName = dt.Rows[indexLdf][0].ToString();
            logFile.PhysicalFileName = Path.Combine(dbPath, destinationDatabase + Path.GetExtension(ldf));

            restore.RelocateFiles.Add(dataFile);
            restore.RelocateFiles.Add(logFile);

            restore.Database = destinationDatabase;
            restore.ReplaceDatabase = true;
            restore.SqlRestore(sqlServer); // <- this is where the FailedOperationException is thrown on the second execution
        

如前所述,第一次恢复效果很好。第二次 FailedOperationException 声明:无法独占访问数据库,因为数据库当前正在使用中。 RESTORE DATABASE 将因错误而停止。 (由我手动翻译)

我们使用的是最新的 NUnit 2 版本 (2.6.4)。为什么数据库仍在使用中,如何正确关闭它?

【问题讨论】:

实际上你应该重构你的代码(抽象),这样你就不需要数据库来测试你的代码。现在您不仅要测试“被测单元”,还要测试数据库内容。 这是相关的:Error - Exclusive access could not be obtained because the database is in use @MikkoViitala 感谢您的建议。在我们的单元测试中,我们通过 BLL 和 DAL 显式测试插入、更新和删除操作。因此,测试功能的关键功能是与数据库一起使用。正如您所建议的,我很困惑如何以抽象的重构方式测试那些以 SQL 语句结尾的逻辑(也需要测试)。 @DMason 它可能会解决同样的问题,但情节完全不同。在我的第一个想法中,我怀疑进度“vstest.executionengine.x86.exe”在关闭连接之前存在问题,直到它自行关闭。也许它也可能是 nunit 中的一个错误。 短期内的高生产力是的,但在这里我们正在解决一个本可以避免的问题:) 就我个人而言,我更喜欢一些 ORM 而不是数据库,例如衣冠楚楚,英孚。 Moq 是一个不错且非常简单的工具,可以即时创建模拟。 【参考方案1】:

你需要在恢复之前杀死所有连接到数据库的进程:

sqlServer.KillAllProcesses(destinationDatabase)

查看documentation了解更多详情。

【讨论】:

好的,这行得通。但是为什么需要这个。我的意思是,一旦SqlConnection 对象调用Dispose(),它不应该自动执行此操作吗?我想这是某种缓存,可以更快地重新打开它,不是吗? 您对restoreDatabase 数据库的第一次调用可能会使连接保持打开状态。 刚刚测试过,没有。 如果您运行原始代码,您是否会在 SQL Management Studio 的活动监视器中看到任何打开的会话。

以上是关于在单元测试之间恢复数据库:数据库仍在使用中的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot DataJpaTest 单元测试恢复到 H2 而不是 mySql

从损坏的 h2 数据库文件中恢复所有可能的信息

为啥当我在同一行中编辑另一个单元格时,此数据表单元格会恢复为旧值?

oracle异机恢复测试

如何从 HDFS 中的 BP 文件夹中恢复数据

使用 ASP.NET Core 和 Entity Framework Core 进行集成测试 - 如何在每次测试时恢复数据库中的测试数据?