You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

非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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 06:25:24