关于c#:集成测试数据库,我做对了吗?

Integration testing database, am I doing it right?

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

我发现了集成测试的想法,它将测试的数据库操作逻辑package在一个 TransactionScope 对象中,该对象在完成时回滚更改。

不幸的是,这首先不是从一个空数据库开始,它还使主键依赖(即,当数据库中已经有一些项目具有主键 1 和 2 时,然后在我运行测试之后依靠 4),我不想要这个。

这是我想出的"集成测试",只是为了测试是否实际添加了产品(例如,我想创建更困难的测试,在我拥有正确的基础架构后检查方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    [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

        }

    }

这引发了很多问题,这里有一个选择:如何从一个空数据库开始?我应该使用自己的上下文和连接字符串将另一个数据库附加到项目吗?最重要的是,如何在不破坏旧数据的情况下正确地在实际数据库上测试方法?

我整天都在忙于弄清楚如何对我的数据库逻辑进行单元/集成测试。希望这里有经验的开发者可以提供一些帮助!

/edit 确实会影响/更改我的数据库的 NDbUnit 测试...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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(@"..\\..\
DbUnitTestDatabase\
DbUnitTestDatabase.xsd"
);
        mySqlDatabase.ReadXml(@"..\\..\
DbUnitTestDatabase\\DatabaseSeeding.xml"
); // The data
        mySqlDatabase.PerformDbOperation(NDbUnit.Core.DbOperationFlag.CleanInsertIdentity);
}

I do not want to use mock methods / objects because the queries can be complicated and creating test objects for that is too much of an effort.

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

How can I start with an empty database?

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

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

And most importantly, how do I properly test methods on an actual database without ruining my old data?

算了。除了可以根据需要丢弃的开发数据库之外,切勿在任何东西上运行此类测试。迟早你会提交一些你不打算做的事情,或者持有比生产中可接受的更长时间的锁(例如,通过在调试器中命中断点),或者以不兼容的方式修改模式,或者只是用负载测试来锤击它否则会影响真实用户的工作效率...


我发现自己需要编写集成测试,但我没有针对开发数据库执行测试,因为它是一个变化的主题。由于我们在持续两周的 sprint 中使用了 scrum 方法,因此我们能够采用以下方法:

  • 在每个 sprint 结束时,我们将创建与开发数据库模式匹配的测试数据库。在大多数情况下,该数据库将在每次测试执行之前在测试数据库服务器上恢复,并在测试完成后将其删除。
  • 用可预测的数据集填充测试数据库,这不会是更改的主题,除了需要更改数据的测试。
  • 配置测试项目以针对测试数据库执行。
  • 我们编写的测试分为两部分。

  • 仅对数据库执行选择查询的测试。
  • 对数据库执行插入、更新、删除查询的测试。
  • 上述方法让我们始终知道每次测试执行后会发生什么。我们使用 MSTest 框架来编写我们的测试,并利用它的能力在每次测试之前和之后,或者在每组测试之前和之后执行逻辑。下面的代码适用于只执行选择查询的测试。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    [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.
        }
    }

    这样,测试将针对可预测的数据集执行,我们始终知道会发生什么。每个测试类将执行一次数据库的恢复和删除,这将加快测试的执行。

    对于在数据库中执行更改的测试,在每个测试执行之前恢复和删除数据库是强制性的,因为我们不希望我们的下一个测试针对具有未知状态的数据库执行,因为我们不会知道会发生什么。这是该场景的代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    [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.
        }
    }

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


    您应该检查您的函数创建的特定案例。将断言视为您在此测试中专门检查的内容。现在,您的测试正在检查,数据库中是否正好有 1 条记录。而已。更有可能的是,您希望您的断言意味着,A)我实际上只是将一个项目添加到数据库中吗?或者,B)我是否只是将刚??刚创建的特定项目添加到数据库中。

    对于 A,你应该做类似...

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
     [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。