次の方法で共有


Console Unit Testing

In many scenarios, console applications are very useful, but if you have a console application of intermediate complexity, you should obviously unit test it like all the other code you write - right? If you have a console application as an administration utility for a complex piece of software you are developing, the console application should obviously just be a thin UI layer that delegates all the work to a testable library that may already exist, but even so, you may have some complex UI interaction that may benefit from testing.

For once, consider the task of parsing the command line arguments. This may involve some semi-complex logic where unit testing may save you a lot of troubleshooting. You may also want to have a regression test suite to validate that console output is as expected. In any case, you need to deal with console input and output.

To make a console application testable, you could obviously introduce an abstraction of the console, so that whenever you wanted to write to the console, you would actually write to an abstract stream or 'console provider', and then, while unit testing, you would use dependency injection to inject a stub or mock into your console library. While this is definitely a tried and true approach, the architecture of System.Console provides a much simpler alternative.

Let's say that I want to unit test this method:

 public void DoWork() 
 {
     Console.WriteLine("Ploeh"); 
 }

In a unit test, I'd like to test that the correct text is being written to the console. Since System.Console allows redirection of its standard output to any TextWriter, I can write my unit test like this:

 [TestMethod]

public void ValidateConsoleOutput()
{
    using (StringWriter sw = new StringWriter())
    {
        Console.SetOut(sw);

        ConsoleUser cu = new ConsoleUser();

        cu.DoWork();

        string expected = 
            string.Format("Ploeh{0}", Environment.NewLine);

        Assert.AreEqual<string>(expected, sw.ToString()); 
    }
}

Console.SetOut redirects the console output to the StringWriter I just created, so after executing the DoWork method, I can examine what was written to the 'console'. Since this test case redirects the standard output for System.Console it's important to reset it when the test is done, since all test cases should be independent, and you may have other tests where you don't want the console output to be redirected. Usually, I use a test initialization method for this task:

 [TestInitialize]

 public void InitializeTest()
 {
     StreamWriter standardOut =
         new StreamWriter(Console.OpenStandardOutput());

     standardOut.AutoFlush = true;

     Console.SetOut(standardOut);
 }

By using OpenStandardOutput, you can always retrieve the original output stream and use it with the SetOut method to reset the output stream.

You don't even need to delegate all the work to a testable library, since you can actually create a project reference to an executable. If you have created a console application project in Visual Studio, you can create a project reference to this project from your unit testing project. Let's say I want to test the UI interaction of this simple console application:

 public class Program
 {
     public static void Main(string[] args)
     {
         Console.WriteLine("Enter your name.");

         string name = Console.ReadLine();

         Console.WriteLine("Hello, {0}.", name);

         Console.WriteLine("Type a message.");

         string message = Console.ReadLine();

         Console.WriteLine("You wrote: {0}", message);
     }
 }

Note

Since I want to test Main, I need to make both Main and the Program class public. Now I can write the following test:

 [TestMethod]

 public void RunMain() 
 {
     using (StringWriter sw = new StringWriter())
     {
         Console.SetOut(sw);

         using (StringReader sr = new StringReader(string.Format("Mark{0}Ploeh{0}",
             Environment.NewLine)))
         {
             Console.SetIn(sr);

             Program.Main(new string[] { });

             string expected = string.Format(
                 "Enter your name.{0}Hello, Mark.{0}Type a message.{0}You wrote: Ploeh{0}",
                 Environment.NewLine);

             Assert.AreEqual<string>(expected, sw.ToString());
         }
     }
 }

In this case, I'm not only redirecting the console output, but also its input stream. This means that each time Main has a call to Console.ReadLine, it will read a line from the StringReader instance, so before calling Main, I'm setting up the StringReader with all the console input for this test. After Main is done, I can validate that the complete console output is as expected.

Since I can just call Main from my unit test, I could also write unit tests where I supply it with command line arguments to test any arguments parsing logic I may have. Since this is often a cause for bugs, having this ability readily available is quite valuable.

Unfortunately, Console.ReadKey doesn't allow you to redirect the input stream, so if you rely on this method to implement a "press any key to continue" functionality, you wll not be able to unit test this part of the code with console redirection.

Besides redirecting console input and output, you can also redirect the error stream by using SetError.

Comments

  • Anonymous
    June 18, 2008
    Nice article. Interesting use of the streamwriter for unit testing. On a similar vein, we have come up with a method of mocking a console app you do not own by using dynamic compilation at runtime of the unit tests. I'll have to see what happens if we combine the two approaches.

  • Anonymous
    September 08, 2008
    The comment has been removed

  • Anonymous
    September 08, 2008
    Thanks for sharing :)

  • Anonymous
    June 15, 2010
    Thanks for posting this article. Very useful.

  • Anonymous
    August 26, 2012
    How about if I want to simulate input with pause and test in between? Like: Type 'A', Assert, Type 'B', assert

  • Anonymous
    October 01, 2014
    While this post is rather old, I would like to add a small comment still: Make sure to redirect your stdin, stdout or stderr only just before your individual test runs, do not set it globally on class initialization. After your inidividual test has run, reset it back. If you don't, output from other tests will not be captured by your test runner. In addition, the test runner will not capture the stdout because you are redirecting, so make sure that on any error you would like to report, that you do so through the Assert methods, as Console.WriteLine and friends will not be captured by the test runner while you are redirecting.