C# – How to unit test code that reads and writes to the console

When you’re unit testing code that uses the console IO methods, you’ll need a way to automate the input and a way to capture the output. There are two ways to do this:

  • Redirect the console IO.
  • Wrap the console IO method calls and mock out the wrapper.

In this article, I’ll show how to do both options by unit testing the following code that calls the console IO methods:

public class Greeter { public void Greet() { Console.WriteLine("What's your name?"); var name = Console.ReadLine(); Console.WriteLine($"Hello {name}"); } }
Code language: C# (cs)

Note: The console is the user interface (UI). In general, it’s a good idea to separate UI from logic, and then only unit test the logic. If you feel the need to unit test the UI (the console IO in this case), carry on.

Option 1 – Redirect the console IO

To redirect output so you can capture it in a unit test, call Console.SetOut() and pass in a StringWriter object, like this:

var stringWriter = new StringWriter(); Console.SetOut(stringWriter); Console.WriteLine("Hello World"); Assert.AreEqual("Hello World", stringWriter.ToString());
Code language: C# (cs)

Likewise, to pass in fake console input from a unit test, call Console.SetIn() and pass in a StringReader object, like this:

var stringReader = new StringReader("Hello World"); Console.SetIn(stringReader); var line1 = Console.ReadLine(); Assert.AreEqual("Hello World", line1);
Code language: C# (cs)

Note: ReadLine() will return null when there’s no new line for it to return. For example, if you initialized StringReader with “Hello World” and call ReadLine() twice, it’ll return “Hello World” for the first call, and null for the second (and subsequent) calls.

Here’s how to unit the Greeter.Greet() method using this console IO redirection approach:

using System.IO; [TestMethod()] public void TestGreet_AsksName_ThenGreetsWithName() { //arrange var greeter = new Greeter(); var name = "Charlemagne"; var stringWriter = new StringWriter(); Console.SetOut(stringWriter); var stringReader = new StringReader(name); Console.SetIn(stringReader); //act greeter.Greet(); //assert var output = stringWriter.ToString(); Assert.AreEqual($"What's your name?\r\nHello {name}\r\n", output); }
Code language: C# (cs)

Notice that stringWriter.ToString() returns a single string. All of the output, including the newlines, are captured in this single string. You can either assert against the single string, with newline delimiters (\r\n on Windows), or split the string and assert each line individually, like this:

//assert var outputLines = stringWriter.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); Assert.AreEqual("What's your name?", outputLines[0]); Assert.AreEqual($"Hello {name}", outputLines[1]);
Code language: C# (cs)

Reading in multiple lines

If your code is calling Console.ReadLine() multiple times, then you’ll need to initialize StringReader with a string with one line per call to Console.ReadLine().

For example, the following initializes StringReader with two lines (“Hello World” and “Hi”):

var stringReader = new StringReader("Hello World\r\nHi"); Console.SetIn(stringReader); var line1 = Console.ReadLine(); Assert.AreEqual("Hello World", line1); var line2 = Console.ReadLine(); Assert.AreEqual("Hi", line2);
Code language: C# (cs)

You can use a StringBuilder to append lines together to clean things up a bit:

var stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Hello World"); stringBuilder.AppendLine("Hi"); var stringReader = new StringReader(stringBuilder.ToString()); Console.SetIn(stringReader); var line1 = Console.ReadLine(); Assert.AreEqual("Hello World", line1); var line2 = Console.ReadLine(); Assert.AreEqual("Hi", line2);
Code language: C# (cs)

Option 2 – Wrap the console IO and mock out the wrapper

The console IO methods (i.e. Console.WriteLine()) are static methods, and since your code is dependent on these, you can use a standard approach for unit testing code that depends on static methods:

  • Wrap the static methods.
  • Extract an interface for the wrapper.
  • Dependency inject the interface.
  • Mock out the interface in the unit tests.

I’ll show step-by-step how to wrap the console IO methods and mock it out in a unit test.

Step 1 – Wrap the console IO methods and extract an interface

Create a wrapper class called ConsoleIO. This simply calls the console IO methods:

public class ConsoleIO : IConsoleIO { public void WriteLine(string s) { Console.WriteLine(s); } public string ReadLine() { return Console.ReadLine(); } }
Code language: C# (cs)

Extract out an interface for the wrapper called IConsoleIO:

public interface IConsoleIO { void WriteLine(string s); string ReadLine(); }
Code language: C# (cs)

Step 2 – Dependency inject the interface and use it

In the code that calls the console IO methods, constructor inject IConsoleIO, and then replace calls directly using the static console IO methods with calls to IConsoleIO:

public class Greeter { private readonly IConsoleIO ConsoleIO; public Greeter(IConsoleIO consoleIO) { ConsoleIO = consoleIO; } public void Greet() { ConsoleIO.WriteLine("What's your name?"); var name = ConsoleIO.ReadLine(); ConsoleIO.WriteLine($"Hello {name}"); } }
Code language: C# (cs)

Step 3 – Mock out the interface and use it in tests

In the unit test, create the mock IConsoleIO object. Use .Setup() to make ReadLine() return fake input. Use .Verify() to assert that WriteLine() was called with the expected output.

using Moq; [TestMethod()] public void TestGreet_AsksName_ThenGreetsWithName() { //arrange var name = "Charlemagne"; var mockConsoleIO = new Mock<IConsoleIO>(); mockConsoleIO.Setup(t => t.ReadLine()).Returns(name); var greeter = new Greeter(mockConsoleIO.Object); //act greeter.Greet(); //assert mockConsoleIO.Verify(t => t.WriteLine("What's your name?"), Times.Once()); mockConsoleIO.Verify(t => t.WriteLine($"Hello {name}"), Times.Once()); }
Code language: C# (cs)

Notes:

  • This example is using the Moq mocking framework. You can get this by installing the Moq nuget package. Execute the following PowerShell command using Package Manager Console (View > Other Windows > Package Manager Console):
Install-Package Moq
Code language: PowerShell (powershell)
  • .Setup() with ReadLine() is equivalent to using Console.SetIn()
  • .Verify() with WriteLine() is equivalent to to using Console.SetOut().

Reading in multiple lines with the mock

If the code you’re testing is calling ReadLine() multiple times, and you need it to return different values each time, then use SetupSequence() instead of Setup().

For example, let’s say you want to test the following method:

private string GetLines(IConsoleIO consoleIO) { return consoleIO.ReadLine() + consoleIO.ReadLine(); }
Code language: C# (cs)

Set up the mock with SetupSequence(), like this:

mockConsoleIO.SetupSequence(t => t.ReadLine()) .Returns("1") .Returns("2"); var input = GetLines(mockConsoleIO.Object); Assert.AreEqual("12", input);
Code language: C# (cs)

The first time ReadLine() is called, it’ll return “1”. The second call (and subsequent calls) will return “2”.

Leave a Comment