如何模拟 IDataReader 来测试将 SqlDataReader 转换为 System.DataView 的方法

17

我是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);

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

谢谢。


1
你有什么问题? - Old Fox
我无法使用虚拟对象填充虚拟数据。这使我无法测试我的方法中的逻辑。 - Hristo
ResolveDataReader 方法是如何实现的? 我需要查看代码才能给出示例... - Old Fox
获取并创建列,然后填充行。 - Hristo
2
使用Moq实现这个功能会很困难,并且会让你的测试与实现非常紧密地耦合在一起。如果你无法重构代码以将其分解一些,那么最好编写一个手动编写的存根类来实现IDataReader,并从内部列表返回数据,你可以在测试中进行配置。 - forsvarir
我完全同意@forsvarir的观点,我想补充他的评论,即为这段代码创建一个手动编写的存根类不会获得投资回报率,而且看起来它是永远不会改变的情况之一,为其创建单元测试可能是浪费时间...在我看来,你根本不应该对其进行测试... - Old Fox
2个回答

21

你在模拟测试方面走得很顺利,但是dataReaderMock.Setup(x => x.Read()).Returns(() => 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

您可以根据自己的口味来排列列,以模拟系统中更多的真实数据类型等。只需确保第一个计数、GetName数量和GetFieldType数量同步。

要排列.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来实现的,但我假设你已经可以进入这个私有方法了,因为你试图测试它而且已经有了一定的进展。


4

我的类用于设置 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"] - russelrillema
1
_iDataReaderMock.Setup(x => x.GetOrdinal("ColumnName")).Returns(0); _iDataReaderMock.Setup(x => x[0]).Returns(12647); _iDataReaderMock.Setup(x => x.GetOrdinal("ColumnName")).返回(0); _iDataReaderMock.Setup(x => x[0]).返回(12647); - Rash

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