How to mock an SqlDataReader using Moq - Update

后端 未结 5 1416
温柔的废话
温柔的废话 2021-01-30 17:12

I\'m new to moq and setting up mocks so i could do with a little help. How do I mock up an SqlDataReader using Moq?

Update

After further testing this is what I h

相关标签:
5条回答
  • 2021-01-30 17:19

    Inspired by @mikesigs answer and another question: SetupSequence in Moq I've came up with the following extension method that does the work for you:

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

    And the usage example:

            var foundTargetIds = new[] { 1, 2, 3 };
            var dataReaderMock = new Mock<IDataReader>();
            dataReaderMock.SetupDataReader(new[] { "TargetId" }, foundTargetIds.Select(x => new { TargetId = x }).ToList());
    
    0 讨论(0)
  • 2021-01-30 17:23

    This does not let you mock a SqlDataReader but if your function is returning a DbDataReader (The base class of SqlDataReader) or a IDataReader the easist way to mock it is just use a DataTable or a DataSet and call its CreateDataReader() function and return that.

    First, in a separate project, run your query like normal to produce some test data and use the WriteXmlSchema to generate a .xsd file and the WriteXml functions to hold the test data.

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

    In your test project add TestDataSet.xsd to the project and make sure it has the custom tool of MSDataSetGenerator (it should have it by default). This will cause a DataTable derived class named TestDataSet to be generated that has the schema of your query.

    Then add TestDataSetData.xml as a resource to your test project. Finally in your test create the TestDataSet and call ReadXml using the text from the xml file you generated.

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

    This will create a data reader that will act just like the data reader that would have been returned from the sql query and even supports things like multiple result sets returned.

    0 讨论(0)
  • 2021-01-30 17:28

    I was just trying to figure this out myself. Not sure if this is new functionality in Moq, but it appears there is a simpler way than @Monsignor's answer.

    Use Moq's SetupSequence method. Your code simply becomes:

    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; 
    }
    
    0 讨论(0)
  • 2021-01-30 17:39

    After some testing the problem is trying to set the DataReader.Read() to true for one loop and then setting it to false. Rhino Mock has the Repeat.Once() option but I could not find a similar method in Moq (I might be wrong here).

    The main reason for testing this was the extension methods to convert the reader to the relevant datatype so in the end I removed the while loop and just accessed the values that had been setup in my mock. The code looks as follows:

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

    Not an ideal solution but it works. If anyone knows better leave a comment thanks.

    0 讨论(0)
  • 2021-01-30 17:42

    Moq has an ability to run some code after the method is executed. It is called "Callback". Modify your code this way and it will work:

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

    But what if it will be required IDataReader to contain not only single row, but several? Well, here is a sample:

    // 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;
    }
    
    0 讨论(0)
提交回复
热议问题