如何用C#结合Moq在不影响真实数据库时单元测试SQL查询
单元测试中执行SQL脚本但不影响真实数据库的几种方案
方案一:重构代码+Mock IDbConnection(推荐,纯单元测试)
原代码直接在方法内实例化SqlConnection,导致无法替换为测试用的模拟对象。先重构代码,把数据库连接的创建逻辑抽离为抽象依赖:
重构后的业务代码
// 定义连接工厂接口 public interface IDbConnectionFactory { IDbConnection CreateConnection(string connectionString); } // 生产环境用的SQL连接工厂实现 public class SqlConnectionFactory : IDbConnectionFactory { public IDbConnection CreateConnection(string connectionString) { return new SqlConnection(connectionString); } } // 修改业务类,通过构造函数注入工厂 public class ContentService { private readonly IDbConnectionFactory _dbConnectionFactory; private readonly Content _content; public ContentService(IDbConnectionFactory dbConnectionFactory, Content content) { _dbConnectionFactory = dbConnectionFactory; _content = content; } public void DeleteContentData(string connectionString) { using IDbConnection db = _dbConnectionFactory.CreateConnection(connectionString); DynamicParameters dynParameters1 = new DynamicParameters(); dynParameters1.Add("@contentId", _content.content_id); db.Execute("DELETE FROM [content] WHERE content_id=@contentId", dynParameters1); db.Execute("INSERT INTO [content] (content_id) VALUES (@contentId);", dynParameters1); } }
单元测试代码(用Moq)
现在可以Mock工厂和连接对象,验证SQL语句、参数是否被正确调用,完全不需要操作真实数据库:
[Test] public void DeleteContentData_ShouldExecuteExpectedSql() { // 准备测试数据 var testContentId = 123; var mockContent = new Content { content_id = testContentId }; // Mock连接工厂和连接对象 var mockConnectionFactory = new Mock<IDbConnectionFactory>(); var mockConnection = new Mock<IDbConnection>(); mockConnectionFactory.Setup(f => f.CreateConnection(It.IsAny<string>())).Returns(mockConnection.Object); // 验证DELETE语句的调用参数 mockConnection.Setup(c => c.Execute( "DELETE FROM [content] WHERE content_id=@contentId", It.Is<DynamicParameters>(p => p.Get<int>("@contentId") == testContentId), null, null, null)) .Verifiable(); // 验证INSERT语句的调用参数 mockConnection.Setup(c => c.Execute( "INSERT INTO [content] (content_id) VALUES (@contentId);", It.Is<DynamicParameters>(p => p.Get<int>("@contentId") == testContentId), null, null, null)) .Verifiable(); var service = new ContentService(mockConnectionFactory.Object, mockContent); // 执行测试方法 service.DeleteContentData("fake_connection_string"); // 验证所有预期调用都已执行 mockConnection.VerifyAll(); }
方案二:使用内存测试数据库(集成测试,真执行SQL)
如果需要验证SQL语法的正确性,可以用内存数据库,比如SQLite内存模式,数据在连接关闭后自动销毁:
[Test] public void DeleteContentData_ShouldPerformCorrectOperations() { // 创建SQLite内存数据库连接 var connectionString = "Data Source=:memory:"; using var connection = new SqliteConnection(connectionString); connection.Open(); // 初始化和真实库一致的表结构 connection.Execute("CREATE TABLE [content] (content_id INT PRIMARY KEY);"); // 插入测试数据 connection.Execute("INSERT INTO [content] (content_id) VALUES (123);"); // 执行业务方法 var service = new ContentService(new SqlConnectionFactory(), new Content { content_id = 123 }); service.DeleteContentData(connectionString); // 验证操作结果:删除后插入,记录数不变 var count = connection.QueryFirst<int>("SELECT COUNT(*) FROM [content] WHERE content_id=123"); Assert.AreEqual(1, count); }
如果需要更贴近SQL Server环境,也可以用LocalDB创建临时测试库,测试前建表、测试后删库。
方案三:事务回滚(适合依赖真实库结构的场景)
如果必须使用和生产一致的数据库结构,但不想留存测试数据,可以用事务包裹测试流程,结束后回滚:
[Test] public void DeleteContentData_ShouldRollbackAfterTest() { var connectionString = "your_test_db_connection_string"; using var connection = new SqlConnection(connectionString); connection.Open(); using var transaction = connection.BeginTransaction(); try { // 这里需要调整业务方法,允许传入已打开的连接(而非自己创建) var service = new ContentService(new SqlConnectionFactory(), new Content { content_id = 456 }); service.DeleteContentData(connection); // 修改方法参数为IDbConnection // 验证操作是否生效 var count = connection.QueryFirst<int>("SELECT COUNT(*) FROM [content] WHERE content_id=456", transaction: transaction); Assert.AreEqual(1, count); // 手动回滚事务,所有操作不会持久化 transaction.Rollback(); } catch { transaction.Rollback(); throw; } }
内容的提问来源于stack exchange,提问作者Aman Tiwari
相关产品推荐
相关产品推荐

