Mocking a StreamReader

11,227

It's not clear why you want to use mocking here.

Yes, using TextReader instead of requiring StreamReader would give you more flexibility. There are very few cases where it's explicitly specifying StreamReader as a parameter or return type.

If you want to provide test data for a StreamReader, simply create a StreamReader wrapping a MemoryStream

When you return a StringReader, that will indeed cause an exception when it's cast (or in the mocking framework itself). Unfortunately your exception handling is over-broad, so it's harder to see that problem. Your catch block should probably only catch IOException - if indeed anything. (If the resource can't be read, do you really want to return -1? Why not just let the exception bubble up?) Catching Exception should be very rare - basically only at the top level of a grand operation (e.g. a web service request) to avoid killing a process when it can continue with other operations.

Share:
11,227
Hans Rudel
Author by

Hans Rudel

Updated on June 26, 2022

Comments

  • Hans Rudel
    Hans Rudel almost 2 years

    I have a method which uses StreamReader which i would like to unit test. Ive split the creation of the StreamReader into a separate class and tried to mock that class but my unit test is still giving me errors.

    Class/Interface used to abstract the StreamReader

    public interface IPathReader
    {
        TextReader CreateReader(string path);
    }
    
    public class PathReader : IPathReader
    {
        public TextReader CreateReader(string filePath)
        {
            return new StreamReader(filePath);
        }
    }
    

    Class containing GetLastRowInFile (method im trying to unit test).

    public interface IIOManager
    {
        int GetLastRowInFile(string filePath, List<String> errorMessageList);
    }
    
    
    public class IOManager : IIOManager
    {
        private IPathReader _reader;
    
        public IOManager(IPathReader reader)
        {
            this._reader = reader;
        }
    
        //...
    
        public int GetLastRowInFile(string filePath, List<String> errorMessage)
        {
            int numberOfRows = 0;
            string dataRow;
    
            try
            {
                using (StreamReader rowReader = (StreamReader)_reader.CreateReader(filePath))
                {
                    while ((rowReader.Peek()) > -1)
                    {
                        dataRow = rowReader.ReadLine();
                        numberOfRows++;
                    }
                    return numberOfRows;
                }
            }
            catch (Exception ex)
            {
                errorMessage.Add(ex.Message);
                return -1;
            }
        }
    }
    

    StreamReader doesnt contain a default constructor so i dont believe i can mock it directly, hence the need to take the creation of StreamReader out of GetLastRowInFile.

    Questions

    1. Should the return type of CreateReader be TextReader?
    2. Do i need to explicitly cast the returned TextReader back to a StringReader before assigning it to rowReader?
    3. When i create a mock of the IPathReader interface and set up CreateReader to return a StringReader instead, what happens when it gets assigned to rowReader. I thought it wasnt possible to cast something on the same inheritance level?

    Inheritance Hierarchy

    The Unit Test is as follows and it keeps returning -1

        [Test]
        public void GetLastRowInFile_ReturnsNumberOfRows_Returns3()
        {
            string testString = "first Row" + Environment.NewLine + "second Line" + Environment.NewLine + "Third line";
            List<String> errorMessageList = new List<string>();
    
            Mock<IPathReader> mock = new Mock<IPathReader>();
            mock.Setup(x => x.CreateReader(It.IsAny<string>())).Returns(new StringReader(testString));
    
            IOManager testObject = new IOManager(mock.Object);
    
            int i = testObject.GetLastRowInFile(testString, errorMessageList);              //Replace with It.IsAny<string>()
            Assert.AreEqual(i, 3);
            Assert.AreEqual(errorMessageList.Count, 0);
        }
    

    Im assuming there is fundamental that im missing so id really appreciate some help with this.Thanks for your time.

    EDIT

    Test Method:

        public void GetLastRowInFile_ReturnsNumberOfRows_Returns3()
        {
            StubGetLastRowInFile myStub = new StubGetLastRowInFile();
            List<String> errorMessageList = new List<string>();
            IOManager testObject = new IOManager(myStub);
            int i = testObject.GetLastRowInFile(It.IsAny<string>(), errorMessageList);
            Assert.AreEqual(i, 3);
            Assert.AreEqual(errorMessageList.Count, 0);
        }
    

    Stub declaration:

    public class StubGetLastRowInFile : IPathReader
    {
        public TextReader CreateReader(string path)
        {
            //string testString = "first Row" + Environment.NewLine + "second Line" + Environment.NewLine + "Third line";
            string testString = "04/01/2010 00:00,1.4314,1.4316";
            UTF8Encoding encoding = new UTF8Encoding();
            UnicodeEncoding uniEncoding = new UnicodeEncoding();
    
            byte[] testArray = encoding.GetBytes(testString);
    
            MemoryStream ms = new MemoryStream(testArray);
    
            StreamReader sr = new StreamReader(ms);
    
            return sr;
        }
    }
    

    EDIT 2

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.IO;
    using System.Windows.Forms;
    using System.Data;
    using System.Globalization;
    using System.Collections;
    using System.Reflection;
    using System.ComponentModel;
    
    namespace FrazerMann.CsvImporter.Entity
    {
        public interface IPathReader
        {
            TextReader CreateReader(string path);
        }
    
        public class PathReader : IPathReader
        {
            public TextReader CreateReader(string filePath)
            {
                return new StreamReader(filePath);
            }
        }
    
    
    public interface IIOManager
    {
        Stream OpenFile(string path);
    
        int GetLastRowInFile(string filePath, List<String> errorMessageList);
    
        int GetNumberOfColumnsInFile(string filePath, List<string> errorMessageList);
    
        bool IsReadOnly(string filePath);
    }
    
    
    public class IOManager : IIOManager
    {
        private IPathReader _reader;
    
        public IOManager(IPathReader reader)
        {
            this._reader = reader;
        }
    
    
        public Stream OpenFile(string path)
        {
            return new FileStream(path, FileMode.Open);
        }
    
    
        public int GetNumberOfColumnsInFile(string filePath, List<String> errorMessageList)
        {
            int numberOfColumns = 0;
            string lineElements;
    
            try
            {
                using (StreamReader columnReader = (StreamReader)_reader.CreateReader(filePath))
                {
                    lineElements = columnReader.ReadLine();
                    string[] columns = lineElements.Split(',');
                    numberOfColumns = columns.Length;
                }
            }
            catch (Exception ex)
            {
                errorMessageList.Add(ex.Message);
                numberOfColumns = -1;
            }
            return numberOfColumns;
        }
    
    
        public int GetLastRowInFile(string filePath, List<String> errorMessage)
        {
            int numberOfRows = 0;
            string dataRow;
    
            try
            {
                using (StreamReader rowReader = (StreamReader)_reader.CreateReader(filePath))
                {
                    while ((rowReader.Peek()) > -1)
                    {
                        dataRow = rowReader.ReadLine();
                        numberOfRows++;
                    }
                    return numberOfRows;
                }
            }
            catch (Exception ex)
            {
                errorMessage.Add(ex.Message);
                return -1;
            }
        }
    
    
        public bool IsReadOnly(string filePath)
        {
            FileInfo fi = new FileInfo(filePath);
            return fi.IsReadOnly;
        }
    }
    
    
    public interface IVerificationManager
    {
        void ValidateCorrectExtension(string filePath, List<String> errorMessageList);
    
        void ValidateAccessToFile(string filePath, List<String> errorMessageList);
    
        void ValidateNumberOfColumns(string filePath, int dataTypeCount, List<String> errorMessageList);
    
        int ValidateFinalRow(int finalRow, string filePath, List<String> errorMessageList);
    
        void ValidateRowInputOrder(int initialRow, int finalRow, List<String> errorMessageList);
    
        void EnumeratedDataTypes(UserInputEntity inputs, List<String> errorMessageList);
    
        int GetProgressBarIntervalsForDataVerification(int initialRow, int finalRow, List<String> errorMessageList);
    }
    
    
    public class VerificationManager : IVerificationManager
    {
        private IIOManager _iomgr;
    
        public VerificationManager(IIOManager ioManager)
        {
            this._iomgr = ioManager;
        }
    
        public void ValidateCorrectExtension(string filePath, List<String> errorMessageList)
        {
            if (filePath.EndsWith(".txt", StringComparison.OrdinalIgnoreCase) | filePath.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)) { }
            else
            {
                errorMessageList.Add("Selected file does not have a compatable extension.");
            }
        }
    
        public void ValidateAccessToFile(string filePath, List<String> errorMessageList)
        {
            try
            {
    
                if (_iomgr.IsReadOnly(filePath) == true) { }
                else
                {
                    errorMessageList.Add("Can not read/write to the specified file.");
                }
            }
            catch (Exception e)
            {
                errorMessageList.Add(e.Message);
            }
        }
    
        public void ValidateNumberOfColumns(string filePath, int userSpecifiedColumnCount, List<String> errorMessageList)
        {
            int numberOfColumnsInFile = _iomgr.GetNumberOfColumnsInFile(filePath, errorMessageList);
    
            if (userSpecifiedColumnCount != numberOfColumnsInFile) errorMessageList.Add("Number of columns specified does not match number present in file.");
        }
    
    //**TEST APPLIES HERE**
    
        public int ValidateFinalRow(int finalRow, string filePath, List<String> errorMessageList)
        {
            int totalNumberOfRowsInFile = 0;
    
            totalNumberOfRowsInFile = _iomgr.GetLastRowInFile(filePath, errorMessageList);
    
            if (totalNumberOfRowsInFile != -1)
            {
                if (finalRow == 0)
                {
                    return totalNumberOfRowsInFile;
                }
                else
                {
                    if (finalRow > totalNumberOfRowsInFile)
                    {
                        errorMessageList.Add("Specified 'Final Row' value is greater than the total number of rows in the file.");
                    }
                }
            }
            return 0;
        }
    
        public void ValidateRowInputOrder(int initialRow, int finalRow, List<String> errorMessageList)
        {
            if (initialRow > finalRow)
            {
                errorMessageList.Add("Initial row is greater than the final row.");
            }
        }
    
        public void EnumeratedDataTypes(UserInputEntity inputs, List<String> errorMessageList)
        {
            inputs.EnumeratedDataTypes = new int[inputs.DataTypes.Count];
            try
            {
                for (int i = 0; i < inputs.DataTypes.Count; i++)
                {
                    inputs.EnumeratedDataTypes[i] = (int)Enum.Parse(typeof(Enumerations.ColumnDataTypes), inputs.DataTypes[i].ToUpper());
                }
            }
            catch (Exception ex)
            {
                errorMessageList.Add(ex.Message);
            }
        }
    
        public int GetProgressBarIntervalsForDataVerification(int initialRow, int finalRow, List<String> errorMessageList)
        {
            int progressBarUpdateInverval = -1;
    
            try
            {
                int dif = (finalRow - initialRow) + 1;
                progressBarUpdateInverval = dif / 100;
    
                if (progressBarUpdateInverval == 0)
                {
                    progressBarUpdateInverval = 1;
                }
            }
            catch (Exception ex)
            {
                errorMessageList.Add(ex.Message);
            }
            return progressBarUpdateInverval;
        }
    }
    
    
    
    public class EntityVerification
    {
    
        private VerificationManager _vmgr;
    
        public EntityVerification(VerificationManager vManager)
        {
            this._vmgr = vManager;
        }
    
    
        public void VerifyUserInputManager(UserInputEntity inputs, List<string> errorMessageList)
        {
            _vmgr.ValidateCorrectExtension(inputs.CsvFilePath ,errorMessageList);
            _vmgr.ValidateCorrectExtension(inputs.ErrorLogFilePath, errorMessageList);
    
            _vmgr.ValidateAccessToFile(inputs.CsvFilePath, errorMessageList);
            _vmgr.ValidateAccessToFile(inputs.ErrorLogFilePath, errorMessageList);
    
            _vmgr.ValidateNumberOfColumns(inputs.CsvFilePath, inputs.DataTypes.Count, errorMessageList);
    
            inputs.FinalRow = _vmgr.ValidateFinalRow(inputs.FinalRow, inputs.CsvFilePath, errorMessageList);
    
            _vmgr.ValidateRowInputOrder(inputs.InitialRow, inputs.FinalRow, errorMessageList);
    
            _vmgr.EnumeratedDataTypes(inputs, errorMessageList);
    
            inputs.ProgressBarUpdateIntervalForDataVerification = _vmgr.GetProgressBarIntervalsForDataVerification(inputs.InitialRow, inputs.FinalRow, errorMessageList);
        }
    }
    }
    

    Test Method (applies to the third last method in the VerificationManager class)

        [Test]
        public void ValidateFinalRow_FinalRowReturned_Returns6()
        {
            List<String> errorMessageList = new List<string>();                             //Remove if replaced
    
            Mock<IIOManager> mock = new Mock<IIOManager>();
            mock.Setup(x => x.GetLastRowInFile(It.IsAny<String>(), errorMessageList)).Returns(6);
    
            VerificationManager testObject = new VerificationManager(mock.Object);
            int i = testObject.ValidateFinalRow(0, "Random", errorMessageList);             //Replace with It.IsAny<string>()  and It.IsAny<List<string>>()
            Assert.AreEqual(i, 6);
        }
    
  • Hans Rudel
    Hans Rudel over 11 years
    I was under the impression that StreamReader actually accessed the specified file and that i would therefore need to mock it. From what you have said, im assuming im mistaken? The idea behind catching the exception was because i wanted the program to go through all the checks. It then checks if anything is contained in the errorMessage. If it is, it doesnt let the user proceed further and notifies them of the errors. I thought it would be better to get all errors at once rather than 1 error, correct it, re-run, next error, correct it etc.(this may be a flawed approach?)
  • Jon Skeet
    Jon Skeet over 11 years
    @HansRudel: No, it reads a stream. Any stream. There happen to be convenience constructors which will create a FileStream for you, but there are others which take any stream.
  • Hans Rudel
    Hans Rudel over 11 years
    Is this along the right lines? (See edit to original question at the bottom.) How do you determine whether you need to mock something. ie StreamReader etc?
  • Jon Skeet
    Jon Skeet over 11 years
    @HansRudel: If you just need to provide data, then use a fake or stub. If you need to mimic specific interactions (e.g. "five valid reads, then an IOException") you need mocking.
  • Hans Rudel
    Hans Rudel over 11 years
    Ive tried to write a stub but im now getting -1 instead of 3. (See edit section for updated code.) I ran the Mock version i had before and it was returning 5 instead of 3. Is that because i had "Environment.Newline".
  • Jon Skeet
    Jon Skeet over 11 years
    @HansRudel: You're returning a disposed StreamReader. Get rid of your using statements.
  • Hans Rudel
    Hans Rudel over 11 years
    Silly mistake, sorry. Im getting what i got before though, 5 instead of 3. Im assuming this is because Environment.Newline is being included in the byte array and not just acting as a newline? When should i be calling Dispose on the MemoryStream and StreamReader?
  • Jon Skeet
    Jon Skeet over 11 years
    @HansRudel: I suspect it's actually because you're using Encoding.Unicode to create the byte array, then Encoding.UTF8 implicitly to read. Why aren't you just using StringReader though?
  • Hans Rudel
    Hans Rudel over 11 years
    "If you want to provide test data for a StreamReader, simply create a StreamReader wrapping a MemoryStream" i thought u were saying i needed to use a memorystream and a streamreader? I have changed the CreateReader method in the Stub to return StringReader (return type is still TextReader) but it still causes an exception.
  • Jon Skeet
    Jon Skeet over 11 years
    @HansRudel: There were two suggestions: 1) If you really want to stick with StreamReader, you can use a MemoryStream and wrap it in a StreamReader. 2) It would be better to return a TextReader, in which case you can use StringReader. Does your calling code still try to cast to StreamReader though?
  • Hans Rudel
    Hans Rudel over 11 years
    Your right, i changed the encoding to UTF8 and it works. Regarding the second option, Im assuming i would just need to change the using statement in the calling method to Using(TextReader columnReader = _reader.CreateReader(filePath))? Finally when/how do i call dispose on the CreateReader Stub if i go with option 1. Is it actually possible?
  • Justin
    Justin over 11 years
    @JonSkeet Question: You said "If you just need to provide data, then use a fake or stub." Why? Moq will achieve this same functionality for you with less code, so why stub something out when the mocking tool will handle it for you?
  • Jon Skeet
    Jon Skeet over 11 years
    @KyleTrauberman: Mocks are suitable for interaction testing - do you really want to mock out each call? In this case I see no need for interaction testing - we're really interested in, "Given this data, how would the code behave?" - and I don't think that mocking ends up using less code here. Or rather, you can mock IPathReader if you want, but still return a StringReader. There's no need or benefit from mocking TextReader here, IMO.
  • Jon Skeet
    Jon Skeet over 11 years
    @HansRudel: Yes, exactly - you'd just change the variable declaration. As for disposing - your code already disposes the reader it's given, doesn't it?
  • Hans Rudel
    Hans Rudel over 11 years
    Yes there is a using statement. I was under the impression i needed to dispose of it in either the test or the stub. Im also curious about Kyles comment. Whats ur views on that? (thank you very much for your time thus far)
  • Justin
    Justin over 11 years
    @JonSkeet Isn't that also interaction testing? If we want to test how a method behaves when one of its dependencies returns a particular value, how is that any different?
  • Jon Skeet
    Jon Skeet over 11 years
    @KyleTrauberman: The difference is that we're not interested in which particular method is used to get the data. We don't care if it uses ReadChar lots of times, or ReadLine, or just Read - we only care about the data coming out of it.
  • Hans Rudel
    Hans Rudel over 11 years
    From what i have understood, im not using mocks correctly. What i dont understand is why its an issue. (Please see Edit 2, it contains the entire class and 1 test which im using a mock on instead of a stub) If i was, would i not need to implement all the methods declared in the IIOManager interface in my stub?
  • Jon Skeet
    Jon Skeet over 11 years
    @HansRudel: Yes, if you wanted to create a stub here you'd need to implement all the methods (possibly just throwing exceptions). It's often reasonable to use a mocking framework to essentially provide stubs. But in the case of TextReader there are perfectly suitable implementations already available - so why not use them?
  • Hans Rudel
    Hans Rudel over 11 years
    "But in the case of TextReader there are perfectly suitable implementations already available - so why not use them?" i think my brain is fried :(. are u referring to using a stub for the test or is there something else im missing? (promise this will be the last question)
  • Jon Skeet
    Jon Skeet over 11 years
    @HansRudel: I'm referring to using StringReader when you need to provide a TextReader with canned data, e.g. from a mock IPathReader.
  • Hans Rudel
    Hans Rudel over 11 years
    i think i have a better understanding of it now. Thank you very much for taking the time to help clear this up, i really appreciate it!