【问题标题】:How to mock IDataReader to test method which converts SqlDataReader to System.DataView如何模拟 IDataReader 以测试将 SqlDataReader 转换为 System.DataView 的方法
【发布时间】:2016-05-14 01:21:46
【问题描述】:

我是 Moq 的新手,我正在努力编写单元测试来测试将 SqlDataAdapter 转换为 System.DataView 的方法。这是我的方法:

private DataView ResolveDataReader(IDataReader dataReader)
{
    DataTable table = new DataTable();

    for (int count = 0; count < dataReader.FieldCount; count++)
    {
        DataColumn col = new DataColumn(dataReader.GetName(count), 
                                        dataReader.GetFieldType(count));
        table.Columns.Add(col);
    }

    while (dataReader.Read())
    {
        DataRow dr = table.NewRow();
        for (int i = 0; i < dataReader.FieldCount; i++)
        {
            dr[i] = dataReader.GetValue(dataReader.GetOrdinal(dataReader.GetName(i)));
        }
        table.Rows.Add(dr);
    }

    return table.DefaultView;
}

我正在尝试创建类似的东西:

var dataReaderMock = new Mock<IDataReader>();
var records = new Mock<IDataRecord>();
dataReaderMock.Setup(x => x.FieldCount).Returns(2);
dataReaderMock.Setup(x => x.Read()).Returns(() => records);

我想传递一些数据并验证它是否已转换。

谢谢。

【问题讨论】:

  • 你有什么问题?
  • 我无法用虚拟数据填充模拟对象。这不允许我测试我的方法中的逻辑。
  • ResolveDataReader方法是如何实现的?我需要看代码才能举个例子……
  • 获取并创建列然后填充行。
  • 用最小起订量来做这件事会很困难,并且会非常紧密地将您的测试与实施结合起来。如果您无法重构代码以将其分解,那么您最好编写一个手动存根类,该类实现 IDataReader 并从您可以在测试中配置的内部列表返回数据。

标签: c# unit-testing mocking moq idatareader


【解决方案1】:

你的模拟是在正确的轨道上,但是dataReaderMock.Setup(x =&gt; x.Read()).Returns(() =&gt; records); 是你出错的地方,因为.Read 返回一个布尔值,而不是记录本身,它们是通过你的方法读出的IDataReader


安排模拟

var dataReader = new Mock<IDataReader>();
dataReader.Setup(m => m.FieldCount).Returns(2); // the number of columns in the faked data

dataReader.Setup(m => m.GetName(0)).Returns("First"); // the first column name
dataReader.Setup(m => m.GetName(1)).Returns("Second"); // the second column name

dataReader.Setup(m => m.GetFieldType(0)).Returns(typeof(string)); // the data type of the first column
dataReader.Setup(m => m.GetFieldType(1)).Returns(typeof(string)); // the data type of the second column

您可以排列列来品尝以模拟更多的真实数据,类型等在您的系统中,只需确保第一个计数,GetNames的数量和@987654326的数量@s 是同步的。

要排列.Read(),我们可以使用SetupSequence:

dataReader.SetupSequence(m => m.Read())
    .Returns(true) // Read the first row
    .Returns(true) // Read the second row
    .Returns(false); // Done reading

要在测试中使用它,您可以将其提取到方法中:

private const string Column1 = "First";
private const string Column2 = "Second";
private const string ExpectedValue1 = "Value1";
private const string ExpectedValue2 = "Value1";

private static Mock<IDataReader> CreateDataReader()
{
    var dataReader = new Mock<IDataReader>();

    dataReader.Setup(m => m.FieldCount).Returns(2);
    dataReader.Setup(m => m.GetName(0)).Returns(Column1);
    dataReader.Setup(m => m.GetName(1)).Returns(Column2);

    dataReader.Setup(m => m.GetFieldType(0)).Returns(typeof(string));
    dataReader.Setup(m => m.GetFieldType(1)).Returns(typeof(string));

    dataReader.Setup(m => m.GetOrdinal("First")).Returns(0);
    dataReader.Setup(m => m.GetValue(0)).Returns(ExpectedValue1);
    dataReader.Setup(m => m.GetValue(1)).Returns(ExpectedValue2);

    dataReader.SetupSequence(m => m.Read())
        .Returns(true)
        .Returns(true)
        .Returns(false);
    return dataReader;
}

(或者,您可以将其安排在 Setup 上,如果这对您的测试类更有意义 - 在这种情况下,dataReader 模拟将是一个字段,而不是返回值)

示例测试。然后它可以像这样使用:

[Test]
public void ResovleDataReader_RowCount()
{
    var dataReader = CreateDateReader();
    var view = ResolveDataReader(dataReader.Object);
    Assert.AreEqual(2, view.Count);
}

[Test]
public void ResolveDataReader_NamesColumn1()
{
    var dataReader = CreateDataReader();
    var view = ResolveDataReader(dataReader.Object);
    Assert.AreEqual(Column1, view.Table.Columns[0].ColumnName);
}

[Test]
public void ResolveDataReader_PopulatesColumn1()
{
    var dataReader = CreateDataReader();
    var view = ResolveDataReader(dataReader.Object);
    Assert.AreEqual(ExpectedValue1, view.Table.Rows[0][0]);
}

// Etc..

(我使用过 NUnit,但它会很相似,只是测试方法的属性不同,断言语法不同,适用于不同的测试框架)


顺便说一句,我通过将ResolveDataReader 更改为internal 并设置InternalsVisibleTo 来实现上述操作,但我假设您已经拥有了进入这种私有方法的网关,就像您所做的那样试图测试它。

【讨论】:

    【解决方案2】:

    我的班级设置IDataReader模拟:

    public static class DataReaderMock
    {
        public static void SetupDataReader(this Mock<IDataReader> mock, ICollection<string> columns, object[,] values)
        {
            if (columns.Count != values.GetLength(1))
            {
                throw new ArgumentException($"The number of named columns must be identical to the number of columns in the 2d values array: {columns.Count} compared to {values.GetLength(1)}");
            }
            mock.Setup(reader => reader.FieldCount).Returns(columns.Count);
    
            var setupSequence = mock.SetupSequence(reader => reader.Read());
            var callbacks = new List<Action<object[]>>
            {
                vals => vals.Populate(columns.Cast<object>().ToList())
            };
            for (var row = 0; row < values.GetLength(0); row++)
            {
                var currentRow = row; // for closure
                callbacks.Add(vals => vals.Populate(values, currentRow));
                setupSequence.Returns(true);
            }
            setupSequence.Returns(false);
            mock.Setup(reader => reader.GetValues(It.IsAny<object[]>())).CallbackSequence(callbacks.ToArray());
        }
    
        private static void Populate<T>(this IList<T> target, IList<T> source)
        {
            for (var i = 0; i < target.Count; i++)
            {
                target[i] = source[i];
            }
        }
    
        private static void Populate<T>(this IList<T> target, T[,] sourceTable, int row)
        {
            for (var i = 0; i < sourceTable.GetLength(1); i++)
            {
                target[i] = sourceTable[row, i];
            }
        }
    
        private static void CallbackSequence<T, TResult, TArg>(this ISetup<T, TResult> setup, params Action<TArg>[] callbacks) where T : class
        {
            var queue = new ConcurrentQueue<Action<TArg>>(callbacks);
            setup.Callback((TArg arg) =>
            {
                Action<TArg> callback;
                if (!queue.TryDequeue(out callback))
                {
                    Assert.Fail("More callbacks were invoked than defined in sequence");
                }
                callback(arg);
            });
        }
    }
    

    用法:

    const int ItemsCount = 1000;
    var dataReaderMock = new Mock<IDataReader>();
    var values = new object[ItemsCount, 2];
    for (var i = 0; i < ItemsCount; i++)
    {
        values[i, 0] = i + 1;
        values[i, 1] = (i + 1).ToString();
    }
    dataReaderMock.SetupDataReader(new List<string> {"Col1", "Col2"}, values);
    

    【讨论】:

    • 有没有办法扩展它以允许我们使用列名从数据读取器中提取值。像 var Column1Value = dataReader["Col1"]
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-02
    • 1970-01-01
    • 2014-10-27
    • 2020-07-14
    相关资源
    最近更新 更多