简体   繁体   English

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

[英]Integration testing database, am I doing it right?

I want to test methods in my MVC4 application that rely on and work with a database. 我想测试依赖于数据库并使用数据库的MVC4应用程序中的方法。 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. 我不想使用模拟方法/对象,因为查询可能很复杂,为此创建测试对象太费劲了。

I found the idea of integration testing that wraps your test's database manipulating logic in a TransactionScope object that rolls back the changes when done. 我发现了集成测试的想法,它将测试的数据库操作逻辑包装在TransactionScope对象中,该对象在完成后回滚更改。

Unfortunately, this does not start with an empty database at first and it also makes the primary keys count on (ie, when there are already a few items in the database with Primary keys 1 and 2 then after I run the test it counts on with 4), I do not want this. 不幸的是,这不是从一开始就是一个空的数据库开始,它也使得主键计数(即,当数据库中已经存在一些具有主键1和2的项时,然后在我运行测试之后,它依赖于4),我不想要这个。

This is an "integration test" I came up with just to test if products are actually added (an example, I want to create more difficult test that check the methods once I have the infrastructure right). 这是一个“集成测试”我想出来测试产品是否实际添加(例如,我想创建更难的测试,一旦我拥有正确的基础设施就检查方法)。

    [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

        }

    }

This raises a lot of questions, here's a selection: How can I start with an empty database? 这提出了很多问题,这里有一个选择:我如何从空数据库开始? Should I attach another database to the project with its own context and connection string? 我应该使用自己的上下文和连接字符串将另一个数据库附加到项目吗? And most importantly, how do I properly test methods on an actual database without ruining my old data? 最重要的是,如何在不破坏旧数据的情况下在实际数据库上正确测试方法?

I have been busy all day trying to figure out how to unit/integration test my database logic. 我整天忙着试图弄清楚如何对我的数据库逻辑进行单元/集成测试。 I hope some experienced developers here can provide some help! 我希望这里有经验丰富的开发人员能提供一些帮助

/edit The NDbUnit test that DOES affect/change my 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);
}

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. 我不想使用模拟方法/对象,因为查询可能很复杂,为此创建测试对象太费劲了。

This is the right strategy. 这是正确的策略。 Most "interesting" bugs tend to happen at the "boundary" between client code and the (real) database. 大多数“有趣”的错误往往发生在客户端代码和(真实)数据库之间的“边界”。

How can I start with an empty database? 如何从空数据库开始?

Purge the database programmatically before each test. 在每次测试之前以编程方式清除数据库。 You can automate that by putting the purging code in a method marked with [TestInitialize] attribute. 您可以通过将清除代码放在标有[TestInitialize]属性的方法中来自动执行此操作。 If your database happens to use ON DELETE CASCADE, deleting all data might be as simple as deleting few "top" tables. 如果您的数据库碰巧使用ON DELETE CASCADE,删除所有数据可能就像删除一些“顶部”表一样简单。

Alternatively, just write your tests to be resilient in case there is already some data in the database. 或者,只要数据库中已有一些数据,就可以将测试编写为具有弹性。 For example, each test would generate its own test data and use the specific IDs of the generated data only . 例如,每个测试将产生其自己的测试数据,并使用将所生成的数据的特定的ID。 This allows you better performance as you don't need to run any extra purging code. 这样可以提高性能,因为您不需要运行任何额外的清除代码。

And most importantly, how do I properly test methods on an actual database without ruining my old data? 最重要的是,如何在不破坏旧数据的情况下在实际数据库上正确测试方法?

Forget about it. 忘掉它。 Never run such tests on anything but a development database that you can throw-away as needed. 除了可以根据需要丢弃的开发数据库之外,不要对任何东西运行此类测试。 Sooner or later you will commit something you did not intend to, or hold some lock longer than acceptable in production (eg by hitting a breakpoint in the debugger), or modify the schema in an incompatible manner, or just hammer it with load tests that would otherwise affect the productivity of real users... 迟早你会提交你不想要的东西,或者在生产中持有一些比生产中可接受的更长的锁(例如,通过在调试器中点击一个断点),或者以不兼容的方式修改模式,或者只是用负载测试来敲定它。否则会影响真实用户的生产力......

I have found myself in a situation to write integration tests, but I didn't execute the tests against the development database since it was a subject of change. 我发现自己处于编写集成测试的情况,但我没有对开发数据库执行测试,因为它是一个变化的主题。 Since we used scrum methodology with sprints that lasted for two weeks, we were able to adopt the following approach: 由于我们使用scrum方法进行了持续两周的冲刺,因此我们采用了以下方法:

  1. At the end of each sprint we would make test database that matches the schema of the development database. 在每个sprint结束时,我们将使测试数据库与开发数据库的模式匹配。 In most of the cases this database would be restored on the test db server before each test executed, and it would be dropped after the test has finished. 在大多数情况下,在执行每个测试之前,将在测试数据库服务器上恢复此数据库,并且在测试完成后将丢弃该数据库。
  2. Fill the test database with predictable set of data, that would not be a subject of change, except for the tests that need to change the data. 使用可预测的数据集填充测试数据库,除了需要更改数据的测试之外,这些数据不会成为变更的主题。
  3. Configure out test projects to execute against the test database. 配置测试项目以针对测试数据库执行。

The tests that we wrote were separated in two parts. 我们编写的测试分为两部分。

  1. Tests that only perform select queries against the database. 仅对数据库执行选择查询的测试。
  2. Tests that perform insert, update, delete queries against the database. 对数据库执行插入,更新,删除查询的测试。

Above mentioned approach allowed us to always know what to expect after each test has executed. 上述方法使我们始终知道每次测试执行后会发生什么。 We used MSTest framework to write our test and used its abilities to execute logic before and after each test, or before and after each set of tests. 我们使用MSTest框架编写测试,并使用其能力在每次测试之前和之后,或每组测试之前和之后执行逻辑。 Below code applies to the tests that perform only select queries. 以下代码适用于仅执行选择查询的测试。

[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.
    }
}

This way the tests would execute against predictable set of data and we would always know what to expect. 这样,测试将针对可预测的数据集执行,我们总是知道会发生什么。 Restoring and dropping of the database would be performed once per test class which would speed up executing of the tests. 每个测试类将执行一次数据库的恢复和删除,这将加速测试的执行。

For the tests that perform changes in the database, restoring and dropping of the database would be mandatory before each test executes, since we didn't want our next test to execute against a database that have unknown state because we wouldn't know what to expect. 对于在数据库中执行更改的测试,在每次测试执行之前必须恢复和删除数据库,因为我们不希望我们的下一个测试针对具有未知状态的数据库执行,因为我们不知道该怎么做期望。 Here is a code sample for that scenario: 以下是该场景的代码示例:

[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.
    }
}

In this scenario the test database is restored and dropped before and after each test. 在此方案中,将在每次测试之前和之后恢复和删除测试数据库。

You should be checking for the specific case created by your function. 您应该检查您的函数创建的特定案例。 Think of the Assertion as what you are specifically checking in this test. 将断言想象为您在此测试中具体检查的内容。 Right now, your Test is checking, is there exactly 1 record in the database. 现在,您的测试正在检查,数据库中是否有1条记录。 That's it. 而已。 More likely, you want your assert to mean, A) Did I actually just add an item to the database? 更可能的是,你希望你的断言意味着,A)我实际上只是将一个项目添加到数据库中吗? Or, B) Did I just add the SPECIFIC item I just created to the database. 或者,B)我刚刚将刚刚创建的SPECIFIC项添加到数据库中。

For A, you should do something like... 对于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

        }

    }

For B), I'll let you figure that out on your own, but really, you should check for the specific ID assigned to your object. 对于B),我会让你自己解决这个问题,但实际上,你应该检查分配给你对象的特定ID。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM