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


我想测试依赖于数据库并使用数据库的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);
}

4144
2017-12-02 17:50


起源



答案:


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

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

如何从空数据库开始?

在每次测试之前以编程方式清除数据库。您可以通过将清除代码放在标有的方法中来自动执行此操作 [TestInitialize] 属性。如果您的数据库碰巧使用ON DELETE CASCADE,删除所有数据可能就像删除一些“顶部”表一样简单。

或者,只要数据库中已有一些数据,就可以将测试编写为具有弹性。例如,每个测试都会生成自己的测试数据,并使用生成数据的特定ID 只要。这样可以提高性能,因为您不需要运行任何额外的清除代码。

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

忘掉它。除了可以根据需要丢弃的开发数据库之外,不要对任何东西进行此类测试。迟早你会提交你不想要的东西,或者在生产中持有一些比生产中可接受的更长的锁(例如,通过在调试器中点击一个断点),或者以不兼容的方式修改模式,或者只是用负载测试来修改它。否则会影响真实用户的生产力......


11
2017-12-03 01:51



所以这也意味着我应该使用额外的数据库。我在一本关于持续集成的书中读过的NDbUnit怎么样? - Erwin Rooijakkers
@ user2609980抱歉,我没有使用NDbUnit的经验。 - Branko Dimitrijevic
@ user2609980 NDbUnit是一个合适的替代方案,因为它允许您在xml文件中定义测试数据 - 参见例如 codeproject.com/Articles/529830/...。但是请注意,当您经常更改架构时,它会变得非常麻烦。 - Thomas Weller
@ThomasWeller谢谢。从现在开始,数据库几乎无法改变,因此这不是问题。关于NDbUnit我没有得到的是它的工作原理。当我向数据库添加一些内容然后运行测试(它与“原始”数据库具有相同的连接字符串...)时,数据库被清除...我应该用另一个数据库创建一个新的数据库实例在同一台服务器上的名称? - Erwin Rooijakkers
@ user2609980使用NDbUnit时,根本没有“真正的”数据库,而是一组驻留在测试项目中的本地xml文件。基于这些,您可以将数据库设置为基于每个测试所需的状态(空或某些数据等)。通过示例项目比任何解释都更好地证明了这一点...... - Thomas Weller


答案:


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

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

如何从空数据库开始?

在每次测试之前以编程方式清除数据库。您可以通过将清除代码放在标有的方法中来自动执行此操作 [TestInitialize] 属性。如果您的数据库碰巧使用ON DELETE CASCADE,删除所有数据可能就像删除一些“顶部”表一样简单。

或者,只要数据库中已有一些数据,就可以将测试编写为具有弹性。例如,每个测试都会生成自己的测试数据,并使用生成数据的特定ID 只要。这样可以提高性能,因为您不需要运行任何额外的清除代码。

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

忘掉它。除了可以根据需要丢弃的开发数据库之外,不要对任何东西进行此类测试。迟早你会提交你不想要的东西,或者在生产中持有一些比生产中可接受的更长的锁(例如,通过在调试器中点击一个断点),或者以不兼容的方式修改模式,或者只是用负载测试来修改它。否则会影响真实用户的生产力......


11
2017-12-03 01:51



所以这也意味着我应该使用额外的数据库。我在一本关于持续集成的书中读过的NDbUnit怎么样? - Erwin Rooijakkers
@ user2609980抱歉,我没有使用NDbUnit的经验。 - Branko Dimitrijevic
@ user2609980 NDbUnit是一个合适的替代方案,因为它允许您在xml文件中定义测试数据 - 参见例如 codeproject.com/Articles/529830/...。但是请注意,当您经常更改架构时,它会变得非常麻烦。 - Thomas Weller
@ThomasWeller谢谢。从现在开始,数据库几乎无法改变,因此这不是问题。关于NDbUnit我没有得到的是它的工作原理。当我向数据库添加一些内容然后运行测试(它与“原始”数据库具有相同的连接字符串...)时,数据库被清除...我应该用另一个数据库创建一个新的数据库实例在同一台服务器上的名称? - Erwin Rooijakkers
@ user2609980使用NDbUnit时,根本没有“真正的”数据库,而是一组驻留在测试项目中的本地xml文件。基于这些,您可以将数据库设置为基于每个测试所需的状态(空或某些数据等)。通过示例项目比任何解释都更好地证明了这一点...... - Thomas Weller


我发现自己处于编写集成测试的情况,但我没有对开发数据库执行测试,因为它是一个变化的主题。由于我们使用scrum方法进行了持续两周的冲刺,因此我们采用了以下方法:

  1. 在每个sprint结束时,我们将生成与开发数据库的模式匹配的测试数据库。在大多数情况下,在执行每个测试之前,将在测试数据库服务器上恢复此数据库,并且在测试完成后将丢弃该数据库。
  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.
    }
}

在此方案中,将在每次测试之前和之后恢复和删除测试数据库。


3
2017-12-03 01:25



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


您应该检查由您的函数创建的特定案例。将断言想象为您在此测试中专门检查的内容。现在,您的测试正在检查,数据库中是否有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。


2
2017-12-02 21:03



谢谢。这仍将继续向主键添加值。我将去NdbUnit或Test数据库。 - Erwin Rooijakkers