Как издеваться над 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
Ответы
Ответ 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());