Introduction
All UI tools frequently have convenient command lines to perform automation. When you are working on a small application, you usually don't need a user interface at all.
Automated testing is very expensive, but when it goes around console applications, there is a way to avoid spending much time and effort on implementing automation. Welcome to the world of console testing.
Definitions
Solution, project and assembly are used in Visual Studio/.NET meaning.
- Tested assembly/project – project referenced by testing assembly and needs to be unit tested
- Testing assembly/project – project that performs unit testing of tested assembly
General Recommendation for Testing in Visual Studio
Using the Code
So all you need is the usual test fixture either in Visual Studio Team Edition or NUnit.
I usually name those fixtures as ProgramTests and keep them in the same testing project as other unit tests.
There are some class members used:
TextWriter m_normalOutput;
used to swap console output
Other two used to accumulate all messages retrieved during application execution are:
StringWriter m_testingConsole;
StringBuilder m_testingSB;
The standard test fixture methods used to replace output and snip error code are as follows:
TestFixtureSetUp
– Set working folder and replace output with mock streams. Mock streams are usual string builders and can be used to analyze output. For example, if you know that your application should output message "in progress" you can check it after execution TestFixtureTearDown
– Restore normal output after working out whole test fixture SetUp
– Clears string builder before running each test TearDown
– Prints cached in string builder content to normal console. This is not a must, but it's frequently needed in development.
namespace MyConsoleApp.Tests
{
[TestFixture]
public class ProgramTests
{
TextWriter m_normalOutput;
StringWriter m_testingConsole;
StringBuilder m_testingSB;
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
string assemblyCodeBase =
System.Reflection.Assembly.GetExecutingAssembly().CodeBase;
string dirName = Path.GetDirectoryName(assemblyCodeBase);
if (dirName.StartsWith("file:\\"))
dirName = dirName.Substring(6);
Environment.CurrentDirectory = dirName;
m_testingSB = new StringBuilder();
m_testingConsole = new StringWriter(m_testingSB);
m_normalOutput = System.Console.Out;
System.Console.SetOut(m_testingConsole);
}
[TestFixtureTearDown]
public void TestFixtureTearDown()
{
System.Console.SetOut(m_normalOutput);
}
[SetUp]
public void SetUp()
{
m_testingSB.Remove(0, m_testingSB.Length);
}
[TearDown]
public void TearDown()
{
m_normalOutput.Write(m_testingSB.ToString());
}
Private
method StartConsoleApplication
is used to start a process in "unit tests". It waits until the console tested application that is running returns an error code of executed tested application. All output is saved in local member testingSB
.
private int StartConsoleApplication(string arguments)
{
Process proc = new Process();
proc.StartInfo.FileName = "MyConsoleApp.exe";
proc.StartInfo.Arguments = arguments;
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.RedirectStandardError = true;
proc.StartInfo.WorkingDirectory = Environment.CurrentDirectory;
proc.Start();
proc.WaitForExit();
System.Console.WriteLine(proc.StandardOutput.ReadToEnd());
System.Console.Write(proc.StandardError.ReadToEnd());
return proc.ExitCode;
}
The simplest test example is as follows:
[Test]
public void ShowCmdHelpIfNoArguments()
{
Assert.AreEqual(0, StartConsoleApplication(""));
Assert.IsTrue(m_testingSB.ToString().Contains(
Program.COMMAND_LINE_HELP));
}
The first line checks that the application has worked out correctly and returned zero error code. The latter checks that console output contains command line help. It's invariant behavior for any pure console application. Empty quote specifies no arguments, otherwise you can put arguments as in usual command prompt: StartConsoleApplication("argument /switcher options").
WARNING: Arguments separated by space will be recognized as different, quote itself (") is neglected. But the current framework can be easily extended to accept multiple-word arguments.
Negative testing can be done like this: In this framework you have an opportunity to check if output messages are correct by using m_testingSB.ToString().Contains()
construction.
[Test]
public void StartWithNonExistingPath()
{
Assert.AreEqual(1, StartConsoleApplication(
"NonExistingMetricsTable.xls"));
Assert.IsTrue(m_testingSB.ToString().Contains("File not found"));
Assert.IsTrue(m_testingSB.ToString().Contains("11"));
Assert.IsTrue(m_testingSB.ToString().Contains("NonExistingMetricsTable.xls"));
}
Final Notes
Please pay attention to the following:
- You don't need to reference the tested project to perform testing like this: You just use command line to run the executable.
- It's acceptable both for .NET 1.1 and .NET 2.0 (Only disadvantage of .NET 1.1: you cannot reference the tested executable assembly to get its resources likewise
Program.COMMAND_LINE_HELP
). - You can use the code snippet attached to this article.
- ProgramTest is quite slow and definitely will spoil your statistics of unit testing timing, but you can still keep it separately from other unit tests.
- It's good to meet the same recommendation as for unit tests: keep program tests independent from the environment, relative path, order of running and so on.
- Such program test easily can be involved in continuous integration.
- Future development of this project plans to implement timeout for test to not let whole build process to halt.
History
- 15th February, 2007: Initial post