【问题标题】:Mocking the database layer, Oracle refCursor, should I refactor this?模拟数据库层,Oracle refCursor,我应该重构这个吗?
【发布时间】:2021-02-11 06:28:44
【问题描述】:

对于冗长的帖子,我深表歉意。我自己不喜欢看到那些,但我的问题是关于结构的,我认为所有的部分都需要问它。 不过,有趣的部分在底部,所以请随意向下滚动到问题。

这是一个控制器。我正在注入一个上下文和一个命令工厂。控制器返回从 Oracle 数据库读取的对象列表。

public class aController : ControllerBase
{
    protected readonly IDB db;

    public aController(IContext context, ICommandFactory factory)
    {
        db = IDB.dbFactory(context, factory);
    }

    [HttpGet]
    public ActionResult<s> GetS()
    {
        return Ok(db.DbGetS());
    }
}

目前没有注入的是持久化类。将有可管理数量的存储过程映射到模型,全部手动编码为规范。这个接口有一个工厂来构造一个实现(这样我可以在需要时模拟它),以及我们的数据检索方法。

public interface IDB
{
    public static IDB dbFactory(
        IContext context,
        ICommandFactory factory)
    { 
        return new DB(context, factory); 
    }

    public S DbGetS();
}

这个类实现了接口。它有一个构造函数,将注入的项目传递给基构造函数,否则通过调用基类中的通用访问方法进行 Oracle 交互。

public class DB: dbBase, IDB
{
    public DB(
        IContext context,
        ICommandFactory factory)
            : base(context, factory)
    { }

    public S DbGetS()
    {
        S s = new S();
        IEnumerable<S> ss = GetData("proc-name");
        return ss.SingleOrDefault();
    }
}

然后,所有模型类都有一个基类,它使用泛型并完成繁重的工作。这非常简化。

public abstract class dbBase
{
    private readonly IContext _context;
    private readonly ICommandFactory _commandFactory;

    protected delegate IEnumerable<T> ParseResult<T>(IDbCommand cmd);

    protected dbBase(IContext context, ICommandFactory factory)
    {
        _context = context;
        _commandFactory = factory;
    }

    protected IEnumerable<T> GetData<T>(string sproc)
    {
        IEnumerable<T> results = null;
        var cmd = this._commandFactory.GetDbCommand(sproc, this._context);
        
        // boilerplate code omitted that sets up the command and executes the query

        results = parseResult<T>(cmd);  // this method will read from the refCursor      
        return results;
    }

    private IEnumerable<T> parseResult<T>(IDbCommand cmd) where T : ModelBase, new()
    {
       // This cast is the problem:
        OracleRefCursor rc = (OracleRefCursor)cmd.Parameters["aCursor"];
        using (OracleDataReader reader = rc.GetDataReader())
        {
            while (reader.Read())
            {
              // code omitted that reads the data and returns it

这是应该测试控制器的单元测试:

 public void S_ReturnsObject()
    {
        // Arrange
        var mockFactory = new Mock<ICommandFactory>();
        var mockContext = new Mock<IContext>();
        var mockCommand = new Mock<IDbCommand>();
        var mockCommandParameters = new Mock<IDataParameterCollection>();

        mockCommandParameters.SetupGet(p => p[It.IsAny<string>()]).Returns(mockParameter.Object);

        // Set up the command and parameters
        mockCommand.SetupGet(x => x.Parameters)
            .Returns(mockCommandParameters.Object);

        mockCommand.Setup(x => x.ExecuteNonQuery()).Verifiable();

        // Set up the command factory
        mockFactory.Setup(x => x.GetDbCommand(
                It.IsAny<string>(),
                mockContext.Object))
            .Returns(mockCommand.Object)
            .Verifiable();

        var controller = new aController(mockContext.Object, mockFactory.Object);

        // Act
        var result = controller.GetS();
     
        // omitted verification

所有存储过程都有包含结果的refCursor 输出参数。为此获得OracleDataReader 的唯一方法是将查询输出参数强制转换为OracleRefCursor。因此无法模拟阅读器,因为即使我可以获得模拟参数,测试也会因 ParseResult 方法中的强制转换异常而失败。除非我错过了什么。

我担心我需要删除 Oracle API 交互,尽管至少输入 parseResults() 作为测试的一部分会很好。

我可以注入 IDB 并将 DbGetS() 替换为模拟版本,但是我的测试不会覆盖太多代码,我将无法模拟任何数据库连接问题等。此外,大约有十几个 IDB 级别的接口都必须注入。

我应该如何重组它才能编写有意义的测试?


(免责声明:我在这里粘贴的代码sn-ps是为了说明目的,经过大量编辑。结果未经测试,不会编译或运行。)

【问题讨论】:

    标签: c# oracle unit-testing moq ref-cursor


    【解决方案1】:

    我并没有真正期待答案,或者至少,我没有收到答案我很好。当我提出关于 SO 的问题时,经常会发生这种情况,只需以其他人可以理解的方式提出问题,就足以让我看透。

    在这种情况下,问题的本质归结为无法在 RefCursor 上使用 GetDataReader()。换句话说,如果您有带有输出游标(即不是 SELECT 结果集)的 Oracle 存储过程,则您无法模拟数据库交互,除非您设法编写自己的 RefCursor 和 OracleDataReader。如果您认为这是错误的,请详细说明。 OracleCommand、OracleParameter 和命令上的操作 (ExecuteNonQuery) 可以替换为可以模拟的 System.Data 等效项。

    那我做了什么?我用 System.Data 东西恢复了 Oracle.ManageDataAccess 类型的替换(因为前者不那么冗长),而是注入了 IDB。

    这是生成的单元测试:

            // Arrange
            var mockDbS = new Mock<IDB>();
            Model.S expected = new Model.S() { var1 = 1, var2 = 2, var3 = 3 };
            mockDbS.Setup(d => d.DbGetS().Returns(expected);
            var controller = new aController(mockDbS.Object);
    
            // Act
            ActionResult<Model.Summary> actionResult = controller.GetS();
            Model.S actual = ((ObjectResult)actionResult.Result).Value as Model.S;
    
            // Assert
            mockDbS.Verify();
            Assert.Equal(200, ((ObjectResult)actionResult.Result).StatusCode);
            Assert.Equal(expected, actual);
    

    这并没有提供我想要的覆盖范围,但它是对控制器操作的基本测试。

    【讨论】:

      猜你喜欢
      • 2020-05-08
      • 2011-10-13
      • 2014-03-26
      • 1970-01-01
      • 2013-12-21
      • 2011-03-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多