【问题标题】:How to mock an SqlDataReader using Moq - Update如何使用 Moq 模拟 SqlDataReader - 更新
【发布时间】:2011-02-08 06:59:04
【问题描述】:

我是最小起订量和设置模拟的新手,所以我可以在一些帮助下完成。如何使用 Moq 模拟 SqlDataReader?

更新

经过进一步测试,这是我目前所拥有的:

private IDataReader MockIDataReader()
{
    var moq = new Mock<IDataReader>();
    moq.Setup( x => x.Read() ).Returns( true );
    moq.Setup( x => x.Read() ).Returns( false );
    moq.SetupGet<object>( x => x["Char"] ).Returns( 'C' );

    return moq.Object;
}

private class TestData
{
    public char ValidChar { get; set; }
}

private TestData GetTestData()
{
   var testData = new TestData();

   using ( var reader = MockIDataReader() )
   {
       while ( reader.Read() )
       {
           testData = new TestData
           {
               ValidChar = reader.GetChar( "Char" ).Value
           };
       }
   }

   return testData;
}

当我在 GetTestData() 方法中执行 reader.Read 时,您遇到的问题始终是空的。我需要知道如何做类似的事情

reader.Stub( x => x.Read() ).Repeat.Once().Return( true ) 

根据犀牛模拟示例:Mocking a DataReader and getting a Rhino.Mocks.Exceptions.ExpectationViolationException: IDisposable.Dispose(); Expected #0, Actual #1

【问题讨论】:

  • 我没有模拟 SqlDataReader 的经验,但如果可以的话,你应该模拟接口。我已经为您查找了它,也许这篇文章可以帮助您:] stackoverflow.com/questions/1792984/… 它使用 Rhinomocks 但想法是一样的。建议在那里,你应该模拟 IDataReader。当您模拟了它时,在模拟上执行 .Setups() 应该不会有问题 ^^ 如果您已经尝试模拟界面,也许您可​​以通过发布一些示例代码向我们展示您卡在哪里:]

标签: c# unit-testing mocking moq


【解决方案1】:

我只是想自己解决这个问题。不确定这是否是 Moq 中的新功能,但似乎有比@Monsignor 的答案更简单的方法。

使用 Moq 的 SetupSequence 方法。你的代码就变成了:

private IDataReader MockIDataReader()
{
    var moq = new Mock<IDataReader>();
    moq.SetupSequence( x => x.Read() )
       .Returns( true )
       .Returns( false );
    moq.SetupGet<object>( x => x["Char"] ).Returns( 'C' );

    return moq.Object; 
}

【讨论】:

  • 你可以使用这种方法返回多个对象吗?
【解决方案2】:

受@mikesigs 回答和另一个问题的启发:SetupSequence in Moq 我想出了以下可以为您工作的扩展方法:

    public static void SetupDataReader(this Mock<IDataReader> dataReaderMock, IList<string> columnNames, ICollection collection)
    {
        var queue = new Queue(collection);

        dataReaderMock
            .Setup(x => x.Read())
            .Returns(() => queue.Count > 0)
            .Callback(() =>
            {
                if (queue.Count > 0)
                {
                    var row = queue.Dequeue();
                    foreach (var columnName in columnNames)
                    {
                        var columnValue = row.GetType().GetProperty(columnName).GetValue(row);
                        dataReaderMock
                            .Setup(x => x[columnNames.IndexOf(columnName)])
                            .Returns(columnValue);
                        dataReaderMock
                            .Setup(x => x[columnName])
                            .Returns(columnValue);
                    }
                }
            });
    }

以及用法示例:

        var foundTargetIds = new[] { 1, 2, 3 };
        var dataReaderMock = new Mock<IDataReader>();
        dataReaderMock.SetupDataReader(new[] { "TargetId" }, foundTargetIds.Select(x => new { TargetId = x }).ToList());

【讨论】:

    【解决方案3】:

    这不允许您模拟 SqlDataReader,但如果您的函数返回 DbDataReaderSqlDataReader 的基类)或 IDataReader,模拟它的最简单方法就是使用 DataTableDataSet 并调用其 CreateDataReader() 函数并返回它。

    首先,在一个单独的项目中,像往常一样运行查询以生成一些测试数据,并使用WriteXmlSchema 生成一个 .xsd 文件,并使用WriteXml 函数来保存测试数据。

    using (var con = new SqlConnection(connectionString))
    {
        con.Open();
        using (var cmd = new SqlCommand("Some query", con))
        {
    
            DataSet ds = new DataSet("TestDataSet");
            DataTable dt = new DataTable("FirstSet");
            ds.Tables.Add(dt);
            using (var reader = cmd.ExecuteReader())
            {
                dt.Load(reader);
            }
    
            ds.WriteXmlSchema(@"C:\Temp\TestDataSet.xsd");
            ds.WriteXml(@"C:\Temp\TestDataSetData.xml");
        }
    }
    

    在您的测试项目中,将TestDataSet.xsd 添加到项目中,并确保它具有MSDataSetGenerator 的自定义工具(默认情况下应该有)。这将导致生成一个名为 TestDataSetDataTable 派生类,该类具有您的查询架构。

    然后将TestDataSetData.xml 作为资源添加到您的测试项目中。最后在您的测试中创建TestDataSet 并使用您生成的xml 文件中的文本调用ReadXml

    var resultSet = new TestData.TestDataSet();
    using (var reader = new StringReader(Resources.TestDataSetData))
    {
        resultSet.ReadXml(reader);
    }
    
    var testMock = new Mock<DbCommand>();
    
    testMock.Setup(x => x.ExecuteReader())
        .Returns(resultSet.CreateDataReader);
    
    testMock.Setup(x => x.ExecuteReaderAsync())
        .ReturnsAsync(resultSet.CreateDataReader);
    

    这将创建一个数据读取器,其行为就像从 sql 查询返回的数据读取器一样,甚至支持返回多个结果集。

    【讨论】:

      【解决方案4】:

      Moq 能够在方法执行后运行一些代码。它被称为“回调”。 以这种方式修改您的代码,它将起作用:

      private IDataReader MockIDataReader()
      {
          var moq = new Mock<IDataReader>();
      
          bool readToggle = true;
      
          moq.Setup(x => x.Read())
               // Returns value of local variable 'readToggle' (note that 
               // you must use lambda and not just .Returns(readToggle) 
               // because it will not be lazy initialized then)
              .Returns(() => readToggle) 
              // After 'Read()' is executed - we change 'readToggle' value 
              // so it will return false on next calls of 'Read()'
              .Callback(() => readToggle = false); 
      
          moq.Setup(x => x["Char"])
              .Returns('C');
      
          return moq.Object;
      }
      
      private class TestData
      {
          public char ValidChar { get; set; }
      }
      
      private TestData GetTestData()
      {
          var testData = new TestData();
      
          using ( var reader = MockIDataReader() )
          {
             testData = new TestData
             {
                 ValidChar = (Char)reader["Char"]
             };
         }
      
         return testData;
      }
      

      但是,如果需要 IDataReader 不仅包含单行,而且包含多行,该怎么办?好吧,这是一个示例:

      // You should pass here a list of test items, their data
      // will be returned by IDataReader
      private IDataReader MockIDataReader(List<TestData> ojectsToEmulate)
      {
          var moq = new Mock<IDataReader>();
      
          // This var stores current position in 'ojectsToEmulate' list
          int count = -1;
      
          moq.Setup(x => x.Read())
              // Return 'True' while list still has an item
              .Returns(() => count < ojectsToEmulate.Count - 1)
              // Go to next position
              .Callback(() => count++);
      
          moq.Setup(x => x["Char"])
              // Again, use lazy initialization via lambda expression
              .Returns(() => ojectsToEmulate[count].ValidChar);
      
          return moq.Object;
      }
      

      【讨论】:

      • 以防万一这对其他人有帮助,在 Returns 方法中,() => 中的括号很关键......如果你不使用它们,你会发现自己处于无限循环中跨度>
      【解决方案5】:

      经过一些测试,问题是尝试将 DataReader.Read() 设置为 true 进行一个循环,然后将其设置为 false。 Rhino Mock 有 Repeat.Once() 选项,但我在 Moq 中找不到类似的方法(我可能在这里错了)。

      对此进行测试的主要原因是将阅读器转换为相关数据类型的扩展方法,所以最后我删除了 while 循环,只访问了在我的模拟中设置的值。代码如下:

      private IDataReader MockIDataReader()
      {
          var moq = new Mock<IDataReader>();
          moq.SetupGet<object>( x => x["Char"] ).Returns( 'C' );
      
          return moq.Object;
      }
      
      private class TestData
      {
          public char ValidChar { get; set; }
      }
      
      private TestData GetTestData()
      {
          var testData = new TestData();
      
          using ( var reader = MockIDataReader() )
          {
             testData = new TestData
             {
                 ValidChar = reader.GetChar( "Char" ).Value
             };
         }
      
         return testData;
      }
      

      不是一个理想的解决方案,但它有效。如果有人知道最好发表评论谢谢。

      【讨论】:

        猜你喜欢
        • 2012-03-18
        • 2010-10-19
        • 2016-09-30
        • 2019-10-06
        • 1970-01-01
        • 2020-10-27
        • 2012-06-01
        • 2018-12-27
        • 2019-06-16
        相关资源
        最近更新 更多