非EF环境下.NET基于ADO.NET与Npgsql的单元测试咨询
问题描述
正在开发一个基于ADO.NET和Npgsql的.NET应用,为了提升SQL能力未采用Entity Framework。目前找不到相关测试文档,不清楚在不使用EF的情况下如何模拟匹配数据访问层的数据库。纠结是只写单元测试、只做集成测试,还是用EF内存数据库模拟真实库?刚接触单元测试,求相关建议与指导。
现有代码实现
UserRepository.cs(数据访问层)
using Domain; using Npgsql; using Persistence.Logger; namespace Persistence.DAL { public class UserRepository : IUserRepository { IConnections _connections; ILogger _logger; public UserRepository(IConnections connections, ILogger logger) { _connections = connections; _logger = logger; } public async Task<User> GetUserById(int userId) { var user = new User(); await using NpgsqlConnection conn = _connections.GetConnection(); { try { if (conn.State != System.Data.ConnectionState.Open) { await conn.OpenAsync(); } var sqlString = $"SELECT * FROM get_user_by_id({userId})"; await using NpgsqlCommand cmd = new NpgsqlCommand(sqlString, conn); cmd.Parameters.AddWithValue("id", userId); await using var rdr = await cmd.ExecuteReaderAsync(); if (await rdr.ReadAsync()) { user = new User(rdr); return user; } else { // No user found for the given ID throw new ArgumentException("Invalid user ID."); } } catch (NpgsqlException ex) { // Handle PostgreSQL-specific exceptions await _logger.Log(ex); throw; // Re-throw the exception to propagate it further } catch (Exception ex) { // Handle general exceptions await _logger.Log(ex); throw new Exception("An error occurred while fetching the user."); } } } } }
Logger.cs
using Persistence.DAL; namespace Persistence.Logger { public class Logger : ILogger { private IDataLogger _dataLogger; public Logger(IDataLogger dataLogger) { _dataLogger = dataLogger; } public async Task Log(Exception exception) { await _dataLogger.LogError(exception); } public async Task Log(Exception exception, string userId) { await _dataLogger.LogError(exception, userId); } } }
Connections.cs
using Npgsql; using Microsoft.Extensions.Configuration; namespace Persistence.DAL { public class Connections : IConnections { private IConfiguration _config; public Connections(IConfiguration configuration) { _config = configuration; } public NpgsqlConnection GetConnection() { return new NpgsqlConnection(_config.GetConnectionString("connectionString")); } } }
UserLogic.cs(业务逻辑层)
using Domain; using Persistence.DAL; namespace Persistence.BLL { public class UserLogic : IUserLogic { private readonly IUserRepository _repository; public UserLogic(IUserRepository repository) { _repository = repository; } public Task<User> GetUserById(int userId) { return _repository.GetUserById(userId); } } }
User.cs(领域模型)
using System.Data.Common; using System.Globalization; namespace Domain { public class User { public User() { } public User(DbDataReader rdr) { this.Id = Int32.Parse(rdr["id"].ToString()); this.UserName = rdr["user_name"].ToString(); this.Created = DateTime.Parse(rdr["created"].ToString(), CultureInfo.CurrentCulture, DateTimeStyles.None); } public int Id { get; set; } public string UserName { get; set; } public DateTime Created { get; set; } } }
测试建议与指导
1. 单元测试与集成测试需结合进行
- 单元测试:针对业务逻辑层(如
UserLogic)和数据访问层的依赖模拟,验证逻辑正确性,无需真实数据库,执行速度快,适合迭代过程中快速验证代码变更。 - 集成测试:连接测试专用的PostgreSQL实例,验证真实数据库交互逻辑,确保SQL语句、存储过程(如
get_user_by_id)和数据映射的正确性,适合验证核心数据流程的稳定性。
2. 不依赖EF的数据库模拟方案
你的代码已经做了良好的依赖抽象(IConnections、ILogger),可以基于此进行模拟:
- 使用Moq等模拟框架:模拟
IConnections返回虚构的NpgsqlConnection,再进一步模拟NpgsqlCommand的ExecuteReaderAsync方法,返回预定义的DbDataReader,覆盖以下核心场景:- 存在用户:返回包含用户数据的Reader
- 不存在用户:Reader无数据
- 数据库异常:抛出
NpgsqlException,验证日志记录和异常传播逻辑
- 注意:模拟时要覆盖所有分支逻辑,确保每个代码路径都能被测试覆盖。
3. 不建议使用EF内存数据库
EF内存数据库的行为与真实PostgreSQL差异极大(不支持PostgreSQL特有的函数、语法、类型),无法有效验证原生SQL和Npgsql交互逻辑,测试结果不具备参考性。
4. 现有代码优化建议(利于测试与稳定性)
- 修复SQL注入风险:当前代码同时使用字符串拼接和参数传递,属于错误写法,应改为纯参数化调用:
var sqlString = "SELECT * FROM get_user_by_id(@id)"; await using NpgsqlCommand cmd = new NpgsqlCommand(sqlString, conn); cmd.Parameters.AddWithValue("@id", userId); - 安全读取DataReader数据:直接
ToString()后Parse容易引发异常,建议使用强类型读取方法:public User(DbDataReader rdr) { this.Id = rdr.GetInt32(rdr.GetOrdinal("id")); this.UserName = rdr.GetString(rdr.GetOrdinal("user_name")); this.Created = rdr.GetDateTime(rdr.GetOrdinal("created")); }
5. 单元测试示例(以Moq为例)
测试UserRepository.GetUserById找到用户的场景:
[Test] public async Task GetUserById_ValidId_ReturnsUser() { // 模拟DbDataReader var mockReader = new Mock<DbDataReader>(); mockReader.Setup(r => r.ReadAsync(default)).ReturnsAsync(true); mockReader.Setup(r => r.GetInt32(rdr.GetOrdinal("id"))).Returns(1); mockReader.Setup(r => r.GetString(rdr.GetOrdinal("user_name"))).Returns("testUser"); mockReader.Setup(r => r.GetDateTime(rdr.GetOrdinal("created"))).Returns(new DateTime(2024, 1, 1)); // 模拟NpgsqlCommand var mockCommand = new Mock<NpgsqlCommand>(); mockCommand.Setup(c => c.ExecuteReaderAsync(default)).ReturnsAsync(mockReader.Object); // 模拟NpgsqlConnection var mockConn = new Mock<NpgsqlConnection>(); mockConn.Setup(c => c.OpenAsync(default)).Returns(Task.CompletedTask); mockConn.Setup(c => c.CreateCommand()).Returns(mockCommand.Object); // 模拟IConnections var mockConnections = new Mock<IConnections>(); mockConnections.Setup(c => c.GetConnection()).Returns(mockConn.Object); // 模拟ILogger var mockLogger = new Mock<ILogger>(); // 实例化Repository并测试 var repository = new UserRepository(mockConnections.Object, mockLogger.Object); var result = await repository.GetUserById(1); // 断言 Assert.AreEqual(1, result.Id); Assert.AreEqual("testUser", result.UserName); }
内容的提问来源于stack exchange,提问作者codian
相关产品推荐
相关产品推荐

