17

I'm new to Moq and I'm struggling to write Unit Test to test a method which converts SqlDataAdapter to System.DataView. This is my method:

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

I'm trying to create to create something like:

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

I would like to pass some data and verify that it is converted.

Thanks.

Old Fox
  • 8,629
  • 4
  • 34
  • 52
Hristo
  • 859
  • 1
  • 8
  • 14
  • 1
    What problem do you have? – Old Fox Feb 04 '16 at 14:45
  • I can't populate the mocked object with dummy data. This doesn't allow me to test the logic in my method. – Hristo Feb 04 '16 at 14:51
  • How `ResolveDataReader` method is implemented? I need to see the code to be able to give an example... – Old Fox Feb 04 '16 at 15:01
  • Get and create columns then populating rows. – Hristo Feb 04 '16 at 15:11
  • 2
    Doing this with moq is going to be hard and would very tightly couple your test to the implementation. If you can't refactor the code to break it up a bit then you would probably be better off writing a hand rolled stub class that implements IDataReader and returns data from an internal list which you could configure in your test. – forsvarir Feb 04 '16 at 16:14
  • I completely agree with @forsvarir, I'd like to extend his comment with the remark that creating a hand rolled stub class for this code won't have receive the ROI, moreover it look like one of the cases where the code will never change and creating a UT for it might be a waste of time... IMO you should't test it at all... – Old Fox Feb 04 '16 at 20:20

2 Answers2

21

You were on the right track with your mocks, but dataReaderMock.Setup(x => x.Read()).Returns(() => records); is where you went wrong as .Read returns a bool, not the records themselves, which are read out the IDataReader by your method.


To arrange the mocks:

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

You can arrange the columns to taste to simulate more real data, types etc.. in your system, just ensure the first count, the number of GetNames and the number of GetFieldTypes are in sync.

To arrange the .Read(), we can use SetupSequence:

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

To use this in tests you can extract it into a method:

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

(Alternatively, you could arrange this on a Setup, if that makes more sense for your test class - in that case the dataReader mock would be a field, not a returned value)

Example Tests. Then it can be used like:

[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..

(I've used NUnit, but it'll be similar with just a different attribute on the test method and a different assert syntax, for different test frameworks)


As an aside, I got the above to work by changing ResolveDataReader to internal and setting InternalsVisibleTo, but I assume you have a gateway into this private method as you've got as far as you did with trying to test it.

NikolaiDante
  • 18,469
  • 14
  • 77
  • 117
4

My class to setup IDataReader mock:

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

Usage:

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);
Mugen
  • 8,301
  • 10
  • 62
  • 140
  • Is there a way to expand this to allow us to pull values from the data reader using the column name. like var Column1Value = dataReader["Col1"] – russelrillema Aug 31 '20 at 05:38
  • 1
    _iDataReaderMock.Setup(x => x.GetOrdinal("ColumnName")).Returns(0); _iDataReaderMock.Setup(x => x[0]).Returns(12647); – Rash Apr 04 '22 at 17:18