There are a lot of answers in the present question, as well as in Best way to unit test console c# app, NUnit Test - Looping - C#, and possibly many others, indicating that direct unit testing of the unmodified, "untestable" console application is not a good way to test. They are all correct.
However, if you really need to test this way for some reason, and if you are able to refer to the console application as a reference from your test project (which, if the two are in the same solution, you likely can), it is possible to do so without resorting to Process.Start
. In .NET 4.5 or later, using xunit syntax:
[Theory]
[MemberData("YourStaticDataProviderField")]
public async void SomeTest(string initialString, string resultString, params int[] indexes)
{
using (var consoleInStream = new AnonymousPipeServerStream(PipeDirection.Out))
using (var consoleOutStream = new AnonymousPipeServerStream(PipeDirection.In))
using (var writer = new StreamWriter(consoleInStream, Encoding.Default, 1024, true))
using (var reader = new StreamReader(consoleOutStream, Encoding.Default, false, 1024, true))
using (var tokenSource = new CancellationTokenSource())
{
// AutoFlush must be set to true to emulate actual console behavior,
// else calls to Console.In.Read*() may hang waiting for input.
writer.AutoFlush = true;
Task programTask = Task.Run(() =>
{
using (var consoleInReader =
new StreamReader(new AnonymousPipeClientStream(PipeDirection.In,
consoleInStream.GetClientHandleAsString())))
using (var consoleOutWriter =
new StreamWriter(new AnonymousPipeClientStream(PipeDirection.Out,
consoleOutStream.GetClientHandleAsString())))
{
// Again, AutoFlush must be true
consoleOutWriter.AutoFlush = true;
Console.SetIn(consoleInReader);
Console.SetOut(consoleOutWriter);
// Of course, pass any arguments your console application
// needs to run your test. Assuming no arguments are
// needed:
Program.Main(new string[0]);
}
}, tokenSource.Token);
// Read and write as your test dictates.
await writer.WriteLineAsync(initialString.Length.ToString());
await writer.WriteLineAsync(initialString);
await writer.WriteLineAsync(indexes.Length.ToString());
await writer.WriteLineAsync(String.Join(" ", indexes));
var result = await reader.ReadLineAsync();
await writer.WriteLineAsync();
// It is probably a good idea to set a timeout in case
// the method under test does not behave as expected (e.g.,
// is still waiting for input). Adjust 5000 milliseconds
// to your liking.
if (!programTask.Wait(5000, tokenSource.Token))
{
tokenSource.Cancel();
Assert.False(true, "programTask did not complete");
}
// Assert whatever your test requires.
Assert.Null(programTask.Exception);
Assert.Equal(resultString, result);
}
}
This solution is likely adaptable to .NET 3.5 or later if you handle asynchronous methods differently. AnonymousPipe(Server|Client)Stream
was
introduced in .NET 3.5. Other unit test frameworks should work with the
appropriate syntax changes.
The pipe streams System.IO.Pipes.AnonymousPipeServerStream
and System.IO.Pipes.AnonymousPipeClientStream
are key to making this solution work. Because a stream has a current position, it does not work as reliably to have two different processes referring to the same MemoryStream
at the same time. Using the pipe streams instead allows the streams' use in parent and child processes, as is done here. Running Program.Main(string[])
in a child task is necessary so that the unit test process can read and write from the console while the program is running. The AnonymousPipeClientStream
objects should belong to the child task, according to the documentation, which is why they are created within the task runner.
You can obtain exception data from the programTask
object if you need to test for exceptions (or, under xunit, use something like Assert.ThrowsAsync<ExpectedException>(Func<Task>)
to run the child task).