如何使用Moq模拟SqlDataReader - 更新

41

我对moq和设置模拟很陌生,需要一些帮助。如何使用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 ) 

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


我没有模拟SqlDataReader的经验,但如果可以的话,你应该模拟接口。我已经为你查找了相关信息,也许这篇文章能帮到你:] https://dev59.com/XXI-5IYBdhLWcg3wj5JG 它使用了Rhinomocks,但思路是相同的。建议你模拟IDataReader。当你模拟了它之后,你就不会在模拟上出现问题了^^ 如果你已经尝试过模拟接口,也许你可以通过发布一些示例代码来展示你遇到的问题:] - Bas
5个回答

68

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;
}

3
如果有人需要帮助的话,我想指出,在Returns方法中,() => 中的括号是至关重要的...如果你不使用它们,你会发现自己陷入无限循环。 - Liath

16

我只是在尝试自己弄清楚这个问题。不确定这是否是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; 
}

使用这种方法,您能够返回多个对象吗? - Hoppe

7

这并不能让你mock一个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查询返回的数据读取器一样工作,甚至支持返回多个结果集。

2
经过一些测试,问题在于尝试将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;
}

这不是一个理想的解决方案,但它有效。如果有更好的方法,请在评论中留言,谢谢。


0
受@mikesigs的回答和另一个问题的启发:在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());

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接