How to mock a stream reader in a unit test
Let’s find out how you can mock any type of stream objects in C#. In a unit test, for instance, a stream reader object can be hard to mock. Most of the time, we end up writing an integration test because we are not able to find the right approach to use a memory stream within the unit test in order to fake the file stream or HTTP stream used in our code… So, how do we achieve this? Let’s see…
In this article, I present a solution that I developed to approach this challenge. Here, I use Unity to inject my dependencies, but it does not matter how you do the dependency injection, as long as you do it. Of course, I am only demonstrating concepts here, but you can (and I hope that you do!) extend this to stream writers as well as any other similar type of problem.
Code
Let’s say we have a program that reads content from “something” and writes the content to “something”. Indeed, we will see that the “something” is not important if we have the right mechanism.
class Program { static void Main(string[] args) { UnityContainer container = new UnityContainer(); container.RegisterType<ILog, ConsoleLogger>(); container.RegisterType<Logger>(); container.RegisterType<IStreamReader, FileStreamReader>(); Logger logger = container.Resolve<Logger>(); logger.Read("Content.log"); } }
In the code above, the key is that I have used two interfaces: “IStreamReader” and “ILog”. The interface IStreamReader will be used to read a stream, and ILog to write somewhere like in the console…
Here is the definition of the two interfaces:
public interface ILog { void WriteLine(string line); }
public interface IStreamReader { StreamReader GetReader(string path); }
Now, let’s have a look at this class Logger that will take the class that reads and the class that writes as constructor parameters. I have used dependency injection to inject my ILog object and my IStreamReader object.
public class Logger { private readonly ILog _log; private readonly IStreamReader _reader; public Logger(ILog log, IStreamReader reader) { _log = log; _reader = reader; } public void Read(string path) { using (StreamReader streamReader = _reader.GetReader(path)) { while (!streamReader.EndOfStream) { _log.WriteLine(streamReader.ReadLine()); } streamReader.Close(); } } }
As we can see with the code above, the approach is totally generic. Any class that will fulfill the contract IStreamReader will do the job.
Unit Test
Ideally, a unit test will:
- execute only in memory, never touch the disk, a database, or anything else other than memory.
- execute in less than 100 ms (if you want to develop thousands of unit tests and have them run fast… well, do the math).
- target only one function in a test case
- target a class that you completely decoupled from the rest of the code
- verify only one logical concept in the Assert section
- document your code if the name of the test is written properly (live documentation)
I can easily implement a unit test because I have used interfaces instead of concrete classes in my injection. Mocking an interface is easier than anything else. Also, it helps to define the contracts you really want from your helper classes.
So, here is how to build this unit test in xUnit:
public class LoggerTests { private StringBuilder GetLogContent() { StringBuilder testLog = new StringBuilder(); testLog.AppendLine("LogContent:"); testLog.AppendLine(""); testLog.AppendLine("Start time: 3:09 AM"); testLog.AppendLine("Initializing the runtime and loading plugins."); testLog.AppendLine("Verifying test files."); testLog.AppendLine("Initializing the test runner."); testLog.AppendLine("Running the tests."); testLog.AppendLine("Host started at 2018-05-02 3:09:57 AM."); testLog.AppendLine("Running under CLR v4.0.30319 runtime."); testLog.AppendLine("[passed] Test LoggerTests/ReaderTest"); testLog.AppendLine("[passed] Test LoggerTests/ReaderStatusFailed"); testLog.AppendLine("Host stopped at 2018 - 05 - 02 5:36:08 AM."); testLog.AppendLine("Host process exited with code: 0"); testLog.AppendLine("Generating reports."); testLog.AppendLine("Disposing the test runner."); testLog.AppendLine("Stop time: 5:36 AM(Total execution time: 1723.676 seconds)"); return testLog; } [Fact] public void GivenValidLogContent_WhenRead_ThenTheContentIsLogged() { // +---------------+ // Arrange. // +---------------+ string anyPath = "anyPath"; StringBuilder content = GetLogContent(); MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(content.ToString())); Mock<ILog> log = new Mock<ILog>(); Mock<IStreamReader> reader = new Mock<IStreamReader>(); reader.Setup(sr => sr.GetReader(anyPath)).Returns(new StreamReader(ms)); Logger sut = new Logger(log.Object, reader.Object); // +---------------+ // Act. // +---------------+ sut.Read(anyPath); // +---------------+ // Asserts. // +---------------+ foreach (string line in content.ToString().Split(Environment.NewLine)) { log.Verify(l => l.WriteLine(line), Times.Once()); } reader.Verify(r => r.GetReader(anyPath), Times.Once); } }
In the Arrange section, I used a memory stream to hold the content that I want the logger to read. I can then return a StreamReader (using this memory stream) when the method GetReader will be called. Et voilà!
Then, I can have any type of reader as long as my return value is of type StreamReader:
public class FileStreamReader : IStreamReader { public StreamReader GetReader(string path) { return new StreamReader(path); } }
public class HttpReader : IStreamReader { public StreamReader GetReader(string path) { WebClient webClient = new WebClient(); return new StreamReader(webClient.OpenRead(path)); } }
And the class implementing ILog is only wrapping the Console at the moment, but could be extended to do far more:
public class ConsoleLogger : ILog { public void WriteLine(string line) { Console.WriteLine(line); } }
I hope this example will help anyone who is trying to unit test a class that needs to read from any stream. Of course, this can be extended to stream writer as well. If you want to mock the console, do the same trick. 😉
Want to learn more?
The theory behind this article includes those technologies:
- Unity Containers: https://github.com/unitycontainer/unity
- xUnit: https://xunit.github.io/
- Moq: https://github.com/Moq/moq4/wiki/Quickstart