集成测试数据库,我做得对吗?

19

我想测试我的MVC4应用程序中依赖并使用数据库的方法,我不想使用模拟方法/对象,因为查询可能很复杂,并且为此创建测试对象太费力了。

我发现了一种集成测试的思路,它将您的测试数据库操作逻辑包装在一个TransactionScope对象中,在完成时回滚更改。

不幸的是,这不会从一开始就使用空数据库,它还会使主键计数(例如,当数据库中已经有一些带有主键1和2的项目时,运行测试后它会继续计数到4),我不想这样。

这是我想出来的“集成测试”,只是为了测试产品是否真正添加(这是一个例子,我想创建更难的测试,以检查在正确的基础设施下的方法)。

    [TestMethod]
    public void ProductTest()
    {
        // Arrange
        using (new TransactionScope())
        {
            myContext db = new myContext();
            Product testProduct = new Product
            {
                ProductId = 999999,
                CategoryId = 3,
                ShopId = 2,
                Price = 1.00M,
                Name = "Test Product",
                Visible = true
            };

            // Act
            db.Products.Add(testProduct);
            db.SaveChanges();

            // Assert
            Assert.AreEqual(1, db.Products.ToList().Count());
            // Fails since there are already items in database

        }

    }

这引起了许多问题,以下是其中一些:如何从空数据库开始?是否应该将另一个带有自己上下文和连接字符串的数据库附加到项目中?最重要的是,如何在实际数据库上正确测试方法而不破坏我的旧数据?

我一整天都忙于尝试解决如何对数据库逻辑进行单元/集成测试的问题。希望这里有一些有经验的开发人员能够提供一些帮助!

/编辑:NDbUnit 测试确实影响/更改了我的数据库...

public class IntegrationTests
{
    [TestMethod]
    public void Test()
    {
        string connectionString = "Data Source=(LocalDb)\\v11.0;Initial Catalog=Database_Nieuw;
            Integrated Security=false;"; 
        //The above is the only connectionstring that works... And is the "real" local database
        //This is not used on Jenkins but I can perhaps attach it???
        NDbUnit.Core.INDbUnitTest mySqlDatabase = new 
        NDbUnit.Core.SqlClient.SqlDbUnitTest(connectionString);
        mySqlDatabase.ReadXmlSchema(@"..\..\NDbUnitTestDatabase\NDbUnitTestDatabase.xsd");
        mySqlDatabase.ReadXml(@"..\..\NDbUnitTestDatabase\DatabaseSeeding.xml"); // The data
        mySqlDatabase.PerformDbOperation(NDbUnit.Core.DbOperationFlag.CleanInsertIdentity);
}
3个回答

20

我不想使用模拟方法/对象,因为查询可能很复杂,并且为此创建测试对象太费力。

这是正确的策略。大多数“有趣”的错误往往发生在客户端代码和(真实的)数据库之间的“边界”处。

如何使用空数据库开始?

在每个测试之前以编程方式清除数据库。您可以通过将清除代码放入标记有[TestInitialize]属性的方法中来自动化此过程。如果您的数据库恰好使用ON DELETE CASCADE,则删除所有数据可能只需删除几个“顶级”表。

或者,只需编写测试,以便在数据库中已经存在一些数据的情况下具有弹性。例如,每个测试都会生成自己的测试数据,并仅使用生成的数据的特定ID。这使您能够获得更好的性能,因为您不需要运行任何额外的清除代码。

最重要的是,如何在实际数据库上正确测试方法而不破坏旧数据?

忘了它。永远不要在除开发数据库外的任何地方运行这样的测试,因为您可能会不经意地提交某些不打算提交的内容,或者在生产环境中比预期更长时间地持有某个锁定(例如通过调试器中断点),或以不兼容的方式修改模式,或只是使用负载测试对其进行严重压力测试,否则会影响真实用户的生产力...


这也意味着我应该使用额外的数据库。那么关于我在一本关于持续集成的书中读到的NDbUnit呢? - user2609980
1
@user2609980 NDbUnit是一个合适的替代方案,因为它允许您在xml文件中定义测试数据-例如,请参见http://www.codeproject.com/Articles/529830/TestingplusEntityplusFrameworkplusapplications-2cp。但请注意,当您频繁更改模式时,它会变得非常麻烦。 - Thomas Weller
1
@user2609980 当使用NDbUnit时,您实际上没有一个“真正”的数据库,而是一组驻留在测试项目中的本地XML文件。基于这些文件,您可以根据每个测试的需要设置数据库状态(空或某些数据等)。通过示例项目更好地演示了这一点,胜过任何解释... - Thomas Weller
@ThomasWeller 嗯,为什么我从NDbUnit运行测试后查看我的数据库时数据会受到影响呢?这是因为使用了错误的连接字符串吗?我在真实数据库和虚拟数据库中使用相同的连接字符串。请参见我对帖子的/edit以获取我正在使用的测试方法。我期待您的评论。 - user2609980
@user2609980 哎呀!我之前的回答部分是错误的。对此我感到很抱歉(当时我很匆忙)!当然你确实需要一个数据库 - 只是它应该是你现有数据库的副本,然后将连接字符串指向它(你可以轻松地使用免费的 SQL Server Express)。哦,然后你可以完全删除所有事务(这有助于提高性能)。 - Thomas Weller
显示剩余3条评论

3
我发现自己需要编写集成测试,但由于开发数据库经常变化,我没有对其进行测试。我们采用了每两周一个迭代的Scrum方法,因此我们能够采取以下方法:
  1. 在每个迭代结束时,我们会创建一个与开发数据库架构匹配的测试数据库。在大多数情况下,每次执行测试之前都会将该数据库还原到测试数据库服务器上,并在测试完成后删除它。
  2. 使用可预测的数据填充测试数据库,这些数据不会改变,除非是需要更改数据的测试。
  3. 配置我们的测试项目以针对测试数据库运行。
我们编写的测试分为两部分。
  1. 只对数据库执行选择查询的测试。
  2. 对数据库执行插入、更新、删除查询的测试。
上述方法使我们能够始终知道每次测试执行后会发生什么。我们使用MSTest框架编写测试,并利用其在每个测试之前和之后,或在每组测试之前和之后执行逻辑的能力。下面的代码适用于仅执行选择查询的测试。
[TestClass]
public class Tests_That_Perform_Only_Select   
{
    [ClassInitialize]
    public static void MyClassInitialize()
    {
        //Here would go the code to restore the test database.
    }

    [TestMethod]
    public void Test1()
    {
        //Perform logic for retrieving some result set.
        //Make assertions.
    }

    [TestMethod]
    public void Test2()
    {
        //Perform logic for retrieving some result set.
        //Make assertions.
    }

    [ClassCleanup]
    public static void MyClassCleanup()
    {
        //Here would go logic to drop the database.
    }
}

这样测试就可以针对可预测的数据集执行,并且我们总是知道期望的结果。恢复和删除数据库将在每个测试类中执行一次,这将加快测试的执行。

对于对数据库进行更改的测试,每次执行测试之前都必须还原和删除数据库,因为我们不希望下一个测试执行在数据库处于未知状态下,从而不知道期望的结果。以下是该场景的代码示例:

[TestClass]
public class Tests_That_Perform_Insert_Update_Or_Delete
{
    [TestInitialize]
    public void MyTestInitialize()
    {
        //Here would go the code to restore the test database.
    }

    [TestMethod]
    public void Test1()
    {
        //Perform logic.
        //Make assertions.
    }

    [TestMethod]
    public void Test2()
    {
        //Perform some logic.
        //Make assertions.
    }

    [TestCleanup]
    public void MyClassCleanup()
    {
        //Here would go logic to drop the database.
    }
}

在这种情况下,测试数据库在每次测试之前和之后都会被恢复和删除。

感谢您的评论。这意味着我应该创建一个额外的测试数据库。不确定如何将其与运行Jenkins的构建服务器结合起来。 - user2609980
在构建服务器上,我们使用MSTest配置了测试的执行。数据库还原和删除的逻辑是通过执行两个SQL脚本来实现的,这些脚本是测试项目的一部分。这些脚本在测试数据库服务器上执行,该服务器上存储了测试数据库备份。我们使用的是Hudson CI而不是Jenkins,但这并不重要。 - Ilija Dimov
你把数据库放在哪里了? - user2609980
测试数据库的备份位于测试数据库服务器上。测试由Hudson CI服务器执行。测试项目包含App.Config文件,其中包含指向数据库服务器的连接字符串。在ClassInitialize/TestInitialize上执行的脚本将备份还原到测试数据库服务器上。然后实际测试针对测试数据库执行。之后,在ClassCleanup/TestCleanup上,执行删除测试数据库的脚本。 - Ilija Dimov

3
您应该检查函数创建的具体情况。将Assertion视为您在此测试中要特别检查的内容。目前,您的测试正在检查数据库中是否恰好有1条记录。就是这样。更有可能的是,您想要断言表示A)我是否实际上刚刚向数据库添加了一项?或B)我是否刚刚将SPECIFIC项目添加到了数据库中。
对于A,您应该执行以下操作...
 [TestMethod]
    public void ProductTest()
    {
        // Arrange
        using (new TransactionScope())
        {
            myContext db = new myContext();
            var originalCount = db.Products.ToList().Count();

            Product testProduct = new Product
            {
                ProductId = 999999,
                CategoryId = 3,
                ShopId = 2,
                Price = 1.00M,
                Name = "Test Product",
                Visible = true
            };

            // Act
            db.Products.Add(testProduct);
            db.SaveChanges();

            // Assert
            Assert.AreEqual(originalCount + 1, db.Products.ToList().Count());
            // Fails since there are already items in database

        }

    }

对于B),我会让你自己解决,但是你应该检查分配给对象的特定ID。


谢谢。虽然这样仍会向主键添加值,但我会选择NdbUnit或测试数据库。 - user2609980

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