Подтвердить что ты не робот

Как издеваться над SqlDataReader с помощью Moq - Update

Я новичок в moq и настраиваю mocks, поэтому я мог бы сделать с небольшой помощью. Как сделать макет SqlDataReader с помощью Moq?

Update

После дальнейшего тестирования это то, что у меня есть до сих пор:

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.Stub( x => x.Read() ).Repeat.Once().Return( true ) 

в соответствии с примером игры rhino: Издевательство над DataReader и получение Rhino.Mocks.Exceptions.ExpectationViolationException: IDisposable.Dispose(); Ожидаемый # 0, фактический номер 1

4b9b3361

Ответ 1

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

Ответ 2

Я просто пытался понять это сам. Не уверен, что это новая функциональность в 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; 
}

Ответ 3

Это не позволяет вам издеваться над SqlDataReader, но если ваша функция возвращает DbDataReader (базовый класс SqlDataReader) или IDataReader, легкий способ издеваться над ним просто использует DataTable или DataSet и вызовите функцию 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 (он должен иметь его по умолчанию). Это приведет к генерации производного класса DataTable с именем TestDataSet, который имеет схему вашего запроса.

Затем добавьте TestDataSetData.xml в качестве ресурса в ваш тестовый проект. Наконец, в вашем тесте создайте TestDataSet и вызовите ReadXml, используя текст из созданного вами XML файла.

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

После некоторого тестирования проблема пытается установить для 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;
}

Не идеальное решение, но оно работает. Если кто-то знает, лучше оставить комментарий спасибо.

Ответ 5

Вдохновленный ответом @mikesigs и другим вопросом: SetupSequence в 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());