【问题标题】:Unit Test Database Commands in C#C# 中的单元测试数据库命令
【发布时间】:2021-11-28 16:39:35
【问题描述】:

我目前正在尝试为将数据插入数据库的方法创建单元测试。我知道我需要模拟或创建一个假类,以便我的测试数据不会添加到我的数据库中,但我不知道如何去做。有人有想法吗?

这是我的控制台“用户界面”涉及的代码。这是在我的“商业”C# 项目中:

        /// <summary>
        /// Gets the connection string.
        /// </summary>
        private static readonly string _connectionString = ConfigurationManager.ConnectionStrings["Database"].ConnectionString;

        /// <summary>
        /// Inserts a new admin into the Admins database table.
        /// </summary>
        /// <param name="admin">Admin record.</param>
        public static void InsertNewAdmin(Admin admin)
        {
            var adminDatabaseWriter = new AdminDatabaseWriter(_connectionString);
            adminDatabaseWriter.Insert(admin);
        }

从那里它进入我的 AdminDatabaseWriter 类并在方法“Insert”中执行以下操作:

        /// <summary>
        /// Inserts a new admin into the Admins database table.
        /// </summary>
        /// <param name="admin">Admin record.</param>
        public void Insert(Admin admin)
        {
            using SqlConnection sqlConnection = new(_connectionString);
            sqlConnection.Open();

            using SqlCommand sqlCommand = DatabaseHelper.CreateNewSqlCommandWithStoredProcedure("InsertNewAdmin", sqlConnection);
            sqlCommand.Parameters.AddWithValue("@PIN", admin.PIN);
            sqlCommand.Parameters.AddWithValue("@AdminType", admin.AdminTypeCode);
            sqlCommand.Parameters.AddWithValue("@FirstName", admin.FirstName);
            sqlCommand.Parameters.AddWithValue("@LastName", admin.LastName);
            sqlCommand.Parameters.AddWithValue("@EmailAddress", admin.EmailAddress);
            sqlCommand.Parameters.AddWithValue("@Password", admin.Password);
            sqlCommand.Parameters.AddWithValue("@AssessmentScore", admin.AssessmentScore);

            var userAddedSuccessfully = sqlCommand.ExecuteNonQuery();
            sqlConnection.Close();

            if (userAddedSuccessfully < 0)
            {
                throw new AdminNotAddedToDatabaseException("User was unsuccessful at being uploaded to the database for an unknown reason.");
            }
        }

再次,我正在尝试对我附加的最后一段代码进行单元测试。测试我的代码是否将对象实际添加到数据库中而不实际将其添加到数据库中的最佳方法是什么。

【问题讨论】:

  • 不要使用 SqlConnection,SqlCommand 直接尝试针对它们的通用接口(如 IDbConnection 、IDBCommand 等)进行编程,并为您的测试提供 Mocks。
  • 你使用什么测试框架?单元? XUnit? MSTest?
  • 除了你当前的问题,还要考虑not use AddWithValue和SqlConnection对象应该是enclosed in using statement
  • @Steve 他正在使用 using。
  • @Ralf 我刚刚学习如何做到这一点并且不熟悉这些界面。你有一个潜在的例子吗?

标签: c# sql .net unit-testing mocking


【解决方案1】:

在为方法Insert 编写单元测试之前,您应该问问自己您究竟希望“单元测试”什么。 Insert 不返回任何内容(void),因此没有返回值可供测试。但是,它在满足某些条件时会抛出异常,并且它还有一个副作用(调用ExecuteNonQuery 将一些数据放入数据库)。

无论您决定测试什么场景(无论是抛出异常还是发生副作用),您都应该告诉您的测试框架,当它到达发生副作用的行时,它应该用另一个实际的操作替换它什么都不做(不仅这样实际的数据库不会变得“脏”,而且因为外部依赖项可能会消耗时间甚至更糟,使您的测试失败,因为它们自己失败了,基本上我们不关心在我们测试的对象的上下文)。

所有标准测试框架/库都提供了对异常和副作用进行断言的工具,但请注意,模拟副作用(通常是模拟依赖项)不仅在针对返回值或异常进行测试时必不可少,而且当您希望验证副作用本身是否发生时。因为无论哪种方式,我们都希望防止外部依赖项干扰 out 测试。

无论您使用什么测试框架,通常首选在您的产品代码中使用接口,以提高应用程序的灵活性和可测试性,并且它还可以让您模拟“副作用”方法,例如 ExecuteNonQuery (那是ISqlCommand 而不是SqlCommand)。这是因为当您指示模拟接口方法时,框架会创建一个新的虚拟类来实现相同的接口。

总而言之,可以通过以下方式测试该对象是否“添加”到数据库中:

  1. 更改代码以使用接口(ISqlConnection、ISqlCommand)。
  2. 模拟所需接口的方法(OpenExecuteNonQueryClose)。
  3. 正在执行Insert
  4. 验证ExecuteNonQuery 已被调用。

(对象是否已正确添加是另一回事,这可能需要集成测试。)

【讨论】:

  • 感谢您的回复。我最终使用了 Dapper 并学习了如何使用它。我为 dapper 和用于读取数据和写入数据的方法创建了一个接口。这似乎工作得很好,并且能够学到新的东西。
【解决方案2】:

问题中呈现的这段代码具有三个职责:

  1. 创建SqlCommand并填写参数;
  2. 执行SqlCommand最终产生一些副作用;
  3. 检查执行结果,如果值不是预期值则抛出。

显然,这些职责中的每一个都需要进行测试,问题是对每个职责都使用相同的测试方法毫无意义。虽然您可以对第 1 点和第 3 点进行单元测试(更多内容见下文),但尝试对第 2 点进行单元测试并没有好处。 SqlCommand 执行涉及与外部系统的交互,即数据库,即您需要实现某种数据库组件隔离。而且您没有那么多选择可以这样做,而每一个都远非理想。

选项是(至少是我知道的):

  1. 针对抽象编写代码(如另一个回复中建议的ISqlCommand)并模拟它们(使用NSubstitute 之类的东西),这样您就无需在attemtp 上执行任何操作来访问真正的数据库。您只需要在模拟上断言 then 即可看到正确的方法以正确的顺序调用并带有预期的参数。这种方法在几个方面不好:
    • 数据库交互未经测试,虽然它可能隐藏错误(如错误的存储过程名称或实现或无效参数);
    • 测试变得脆弱,因为通过使用模拟可以揭示方法实现细节。例如,方法调用重新排序或添加在测试方法 api 级别不可见的附加参数等简单的事情可能会使测试失败,而更改本身就业务逻辑而言可能是绝对正确的,因此不应导致任何测试失败。
  2. 使用一些数据库替换,例如内存中的SqliteEF Core
    • 尽管这种方法在 imo 中比前一种方法更好,因为您拥有的数据库或多或少与真实数据库相似,但缺少功能和行为差异仍然导致数据库交互未得到正确测试。

解决该问题的另一种方法是针对真实数据库(不是生产数据库,但专门为测试准备)编写不是单元测试而是集成测试。您只需要在测试执行之前/之后清理它。 我个人认为这种方法最适合用于测试数据库交互,因为您正在最接近生产环境的环境中测试应用程序行为。缺点是测试设置有点复杂,测试运行很可能需要更多时间。 Reseed(我正在编写它)或 Respawn 之类的库可能有助于简化设置并优化执行速度。

现在回到职责 1 和 3 的单元测试。应该可以通过稍微改变方法设计来为它们编写单元测试。您可能会提取一些额外的方法,每个方法都将显式处理每个职责。所以最初的Insert 现在可以看作是三个方法的组合:

  1. CreateParameters 准备 SqlCommand 参数 - 您将能够断言参数已通过纯单元测试正确填充;
  2. Execute 使用真实数据库执行命令——这可以在集成测试中进行测试;
  3. AssertUserCreated 在用户创建失败时抛出异常——再次纯单元测试就足够了。

这将使您达到 100% 的测试覆盖率,而这并不是必需的。如果参数创建逻辑和存储过程结果验证逻辑像现在这样简单,则可以不测试第 1 点和第 3 点。

当然,设计还可以进一步改进,使其不仅更易于测试,而且更具可读性,这只是一个想法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多