Some colleagues of mine are complaining that sometimes they are not able to apply TDD or write unit tests for some modules or applications, Console Applications are one of these.
How could I test a Console application when the input is passed by keystrokes and the output is presented on a screen?!!
Actually, this happens from time to time, you find yourself trying to write unit tests for something you seem to not have any control upon.
The truth is, you just missed the point. You don’t need to test the “Console” application, you want to test the business logic behind it.
When you are building a Console application, you are building an application for someone to use, he expects to pass some inputs and get some corresponding outputs, and that’s what you really need to test.
You don’t want to test the System.Console
static class, this is a built-in class that is included in the .NET framework and you have to trust Microsoft on this.
Now, you need to think about how to separate these two areas into separate components or modules so that you can start writing tests for the one you desire without interfering with the other one, and this is what I am going to explain to you…
First, let’s come up with a stupid simple Console application idea and use it as an example to apply on.
First, you have this simple menu.
When you choose option 1 and enter your name, you get the Hello message as in the image below. Hitting enter would close the application.
When you choose option 2 and enter your name, you get the Goodbye message as in the image below. Hitting enter would close the application.
Too simple, right? Yes, I agree with you. However, let’s assume that the UI, strings, characters, and everything you see on the screen, is part of the requirements.
This means that if you are going to write unit tests, this should also be covered in a way that a minor change on a single character in the production code, should trigger a failing unit test.
This is our plan:
Simply, do everything in one place.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyConsoleApp
{
class Program
{
static void Main(string[] args)
{
var input = string.Empty;
do
{
Console.WriteLine("Welcome to my console app");
Console.WriteLine("[1] Say Hello?");
Console.WriteLine("[2] Say Goodbye?");
Console.WriteLine("");
Console.Write("Please enter a valid choice: ");
input = Console.ReadLine();
if (input == "1" || input == "2")
{
Console.Write("Please enter your name: ");
string name = Console.ReadLine();
if (input == "1")
{
Console.WriteLine("Hello " + name);
}
else
{
Console.WriteLine("Goodbye " + name);
}
Console.WriteLine("");
Console.Write("Press any key to exit... ");
Console.ReadKey();
}
else
{
Console.Clear();
}
}
while (input != "1" && input != "2");
}
}
}
What we can notice here:
System.Console
class.System.Console
.
Really? are you really expecting to be able to write a unit test for that code?
System.Console
.If you can do something about it, you are a hero… believe me.
Now, let’s split our solution into smaller modules.
This is the module that is responsible for providing the functionality we need from the Console… any console.
This module would consist of two parts:
Therefore we will have the following:
IConsoleManager
: This is the interface defining what we are expecting from any Console Manager.ConsoleManagerBase
: This is the abstract class implementing IConsoleManager
and providing any common implementations between all Console Managers.ConsoleManager
: This is the default Console Manager implementation which wraps System.Console
and is actually used at runtime.
using System;
namespace ConsoleManager
{
public interface IConsoleManager
{
void Write(string value);
void WriteLine(string value);
ConsoleKeyInfo ReadKey();
string ReadLine();
void Clear();
}
}
using System;
namespace ConsoleManager
{
public abstract class ConsoleManagerBase : IConsoleManager
{
public abstract void Clear();
public abstract ConsoleKeyInfo ReadKey();
public abstract string ReadLine();
public abstract void Write(string value);
public abstract void WriteLine(string value);
}
}
using System;
namespace ConsoleManager
{
public class ConsoleManager : ConsoleManagerBase
{
public override void Clear()
{
Console.Clear();
}
public override ConsoleKeyInfo ReadKey()
{
return Console.ReadKey();
}
public override string ReadLine()
{
return Console.ReadLine();
}
public override void Write(string value)
{
Console.Write(value);
}
public override void WriteLine(string value)
{
Console.WriteLine(value);
}
}
}
What we can notice here:
IConsoleManager
.IConsoleManager
while writing unit tests.ConsoleManagerBase
we are not providing any common implementation to be used by children.This is the module that is responsible for providing the main application functionality.
This module would consist of two parts:
Therefore we will have the following:
IProgramManager
: This is the interface defining what we are expecting from any Program Manager.ProgramManagerBase
: This is the abstract class implementing IProgramManager
and providing any common implementations between all Program Managers.ProgramManager
: This is the default Program Manager implementation which is actually used at runtime. It also depends on the IConsoleManager
.
namespace ProgramManager
{
public interface IProgramManager
{
void Run();
}
}
namespace ProgramManager
{
public abstract class ProgramManagerBase : IProgramManager
{
public abstract void Run();
}
}
using ConsoleManager;
namespace ProgramManager
{
public class ProgramManager : ProgramManagerBase
{
private readonly IConsoleManager m_ConsoleManager;
public ProgramManager(IConsoleManager consoleManager)
{
m_ConsoleManager = consoleManager;
}
public override void Run()
{
string input;
do
{
m_ConsoleManager.WriteLine("Welcome to my console app");
m_ConsoleManager.WriteLine("[1] Say Hello?");
m_ConsoleManager.WriteLine("[2] Say Goodbye?");
m_ConsoleManager.WriteLine("");
m_ConsoleManager.Write("Please enter a valid choice: ");
input = m_ConsoleManager.ReadLine();
if (input == "1" || input == "2")
{
m_ConsoleManager.Write("Please enter your name: ");
var name = m_ConsoleManager.ReadLine();
if (input == "1")
{
m_ConsoleManager.WriteLine("Hello " + name);
}
else
{
m_ConsoleManager.WriteLine("Goodbye " + name);
}
m_ConsoleManager.WriteLine("");
m_ConsoleManager.Write("Press any key to exit... ");
m_ConsoleManager.ReadKey();
}
else
{
m_ConsoleManager.Clear();
}
}
while (input != "1" && input != "2" && input != "Exit");
}
}
}
What we can notice here:
ProgramManager
on IConsoleManager
well defined.IProgramManager
and we can use mocks and stubs to replace IProgramManager
while writing unit tests.ProgramManagerBase
we are not providing any common implementation to be used by children.
The ProgramManager
class could be split into smaller parts. That would make it easier to track and cover with unit tests. However, this is something I am leaving to you to do.
This is the main application.
Here we are going to use
On the main Console Application project, we would create NinjectDependencyResolver.cs
file. This file would be as follows.
using Ninject.Modules;
using ConsoleManager;
using ProgramManager;
namespace MyConsoleApp
{
public class NinjectDependencyResolver : NinjectModule
{
public override void Load()
{
Bind<IConsoleManager>().To<ConsoleManager.ConsoleManager>();
Bind<IProgramManager>().To<ProgramManager.ProgramManager>();
}
}
}
What we can notice here:
NinjectDependencyResolver
class is inheriting NinjectModule
.void Load()
method where we are setting our bindings as expected.
Now, on the Program.cs
:
using Ninject;
using System.Reflection;
using ProgramManager;
namespace MyConsoleApp
{
class Program
{
private static IProgramManager m_ProgramManager = null;
static void Main(string[] args)
{
var kernel = new StandardKernel();
kernel.Load(Assembly.GetExecutingAssembly());
m_ProgramManager = kernel.Get<IProgramManager>();
m_ProgramManager.Run();
}
}
}
What we can notice here:
IProgramManager
.var kernel = new StandardKernel();
.kernel.Load(Assembly.GetExecutingAssembly());
. This instructs Ninject to get its bindings from all classes inheriting NinjectModule
inside the current assembly/project.NinjectDependencyResolver
class as it is inheriting NinjectModule
and located inside the current assembly/project.IProgramManager
we are using the IoC container as follows kernel.Get<IProgramManager>();
.
Now, let’s see if this design and work we have done till this moment has fixed our problem.
So, the question now is, can we cover our Console Application with unit tests? To answer this question, let’s try to write some unit tests…
If you have some experience with unit testing, you should know that we have Stubs and Mocks to be used to replace our dependencies.
Just for fun, I would use stubs for our example here.
So, I would define ConsoleManagerStub
as a stub for IConsoleManager
as follows:
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleManager
{
public class ConsoleManagerStub : ConsoleManagerBase
{
private int m_CurrentOutputEntryNumber;
private readonly List<string> m_Outputs = new List<string>();
public event Action<int> OutputsUpdated;
public event Action OutputsCleared;
public Queue<object> UserInputs { get; } = new Queue<object>();
public override void Clear()
{
m_CurrentOutputEntryNumber++;
m_Outputs.Clear();
OnOutputsCleared();
OnOutputsUpdated(m_CurrentOutputEntryNumber);
}
public override ConsoleKeyInfo ReadKey()
{
ConsoleKeyInfo result;
object input;
if (UserInputs.Count > 0)
{
input = UserInputs.Dequeue();
}
else
{
throw new ArgumentException("No input was presented when an input was expected");
}
if (input is ConsoleKeyInfo key)
{
result = key;
}
else
{
throw new ArgumentException("Invalid input was presented when ConsoleKeyInfo was expected");
}
return result;
}
public override string ReadLine()
{
object input;
if (UserInputs.Count > 0)
{
input = UserInputs.Dequeue();
}
else
{
throw new ArgumentException("No input was presented when an input was expected");
}
string result;
if (input is string str)
{
result = str;
WriteLine(result);
}
else
{
throw new ArgumentException("Invalid input was presented when String was expected");
}
return result;
}
public override void Write(string value)
{
m_Outputs.Add(value);
m_CurrentOutputEntryNumber++;
OnOutputsUpdated(m_CurrentOutputEntryNumber);
}
public override void WriteLine(string value)
{
m_Outputs.Add(value + "\r\n");
m_CurrentOutputEntryNumber++;
OnOutputsUpdated(m_CurrentOutputEntryNumber);
}
protected void OnOutputsUpdated(int outputEntryNumber)
{
OutputsUpdated?.Invoke(outputEntryNumber);
}
protected void OnOutputsCleared()
{
OutputsCleared?.Invoke();
}
public override string ToString()
{
var result = string.Empty;
if (m_Outputs == null || m_Outputs.Count <= 0) return result;
var builder = new StringBuilder();
foreach (var output in m_Outputs)
{
builder.Append(output);
}
result = builder.ToString();
return result;
}
}
}
And finally, the unit tests would be as follows:
using ConsoleManager;
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace MyConsoleApp.Tests
{
[TestFixture]
public class ProgramManagerTests
{
private ConsoleManagerStub m_ConsoleManager = null;
private ProgramManager.ProgramManager m_ProgramManager = null;
[SetUp]
public void SetUp()
{
m_ConsoleManager = new ConsoleManagerStub();
m_ProgramManager = new ProgramManager.ProgramManager(m_ConsoleManager);
}
[TearDown]
public void TearDown()
{
m_ProgramManager = null;
m_ConsoleManager = null;
}
[TestCase("Ahmed")]
[TestCase("")]
[TestCase(" ")]
public void RunWithInputAs1AndName(string name)
{
m_ConsoleManager.UserInputs.Enqueue("1");
m_ConsoleManager.UserInputs.Enqueue(name);
m_ConsoleManager.UserInputs.Enqueue(new ConsoleKeyInfo());
var expectedOutput = new List<string>
{
"Welcome to my console app\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: ",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\nPlease enter your name: ",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\nPlease enter your name: " + name + "\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\nPlease enter your name: " + name + "\r\nHello " + name +"\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\nPlease enter your name: " + name + "\r\nHello " + name +"\r\n\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\nPlease enter your name: " + name + "\r\nHello " + name +"\r\n\r\nPress any key to exit... "
};
m_ConsoleManager.OutputsUpdated +=
outputEntryNumber =>
{
Assert.AreEqual(
expectedOutput[outputEntryNumber - 1],
m_ConsoleManager.ToString());
};
m_ProgramManager.Run();
}
[TestCase("Ahmed")]
[TestCase("")]
[TestCase(" ")]
public void RunWithInputAs2AndName(string name)
{
m_ConsoleManager.UserInputs.Enqueue("2");
m_ConsoleManager.UserInputs.Enqueue(name);
m_ConsoleManager.UserInputs.Enqueue(new ConsoleKeyInfo());
var expectedOutput = new List<string>
{
"Welcome to my console app\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: ",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\nPlease enter your name: ",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\nPlease enter your name: " + name + "\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\nPlease enter your name: " + name + "\r\nGoodbye " + name + "\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\nPlease enter your name: " + name + "\r\nGoodbye " + name + "\r\n\r\n",
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\nPlease enter your name: " + name + "\r\nGoodbye " + name + "\r\n\r\nPress any key to exit... "
};
m_ConsoleManager.OutputsUpdated +=
outputEntryNumber =>
{
Assert.AreEqual(
expectedOutput[outputEntryNumber - 1],
m_ConsoleManager.ToString());
};
m_ProgramManager.Run();
}
[Test]
public void RunShouldKeepTheMainMenuWhenInputIsNeither1Nor2()
{
m_ConsoleManager.UserInputs.Enqueue("any invalid input 1");
m_ConsoleManager.UserInputs.Enqueue("any invalid input 2");
m_ConsoleManager.UserInputs.Enqueue("Exit");
var expectedOutput = new List<string>
{
// initial menu
"Welcome to my console app\r\n", // outputEntryNumber 1
"Welcome to my console app\r\n[1] Say Hello?\r\n", // outputEntryNumber 2
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n", // outputEntryNumber 3
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n", // outputEntryNumber 4
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: ", // outputEntryNumber 5
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: any invalid input 1\r\n", // outputEntryNumber 6
// after first trial
"", // outputEntryNumber 7
"Welcome to my console app\r\n", // outputEntryNumber 8
"Welcome to my console app\r\n[1] Say Hello?\r\n", // outputEntryNumber 9
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n", // outputEntryNumber 10
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n", // outputEntryNumber 11
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: ", // outputEntryNumber 12
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: any invalid input 2\r\n", // outputEntryNumber 13
// after second trial
"", // outputEntryNumber 14
"Welcome to my console app\r\n", // outputEntryNumber 15
"Welcome to my console app\r\n[1] Say Hello?\r\n", // outputEntryNumber 16
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n", // outputEntryNumber 17
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n", // outputEntryNumber 18
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: ", // outputEntryNumber 19
"Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: Exit\r\n" // outputEntryNumber 20
};
m_ConsoleManager.OutputsUpdated +=
outputEntryNumber =>
{
if (outputEntryNumber - 1 < expectedOutput.Count)
{
Assert.AreEqual(
expectedOutput[outputEntryNumber - 1],
m_ConsoleManager.ToString());
}
};
m_ProgramManager.Run();
}
}
}
Now we have been able to cover our Console Application with unit tests. However, you might think that this is too much for a simple application like the one we have here. Isn’t this overkill?
Actually, it depends on what you want to cover. For example, in our simple application, I dealt with every character on the UI as a requirement that should be covered by unit tests. So, if you go and change a character on the main implementation, a unit test would fail.
Maybe in your case, it would be different. However, it would always be good if you know how to do it even down to the smallest character.
That’s it, hope you found reading this article as interesting as I found writing it.
Also Published Here