TDD and Automated Unit Testing for Console Application
Posted by 3/10/2018 10:32:00 AM with No comments
on You can find the code samples used in this post on this GitHub repository.
Some friends of mine complained that sometimes they are not able to apply TDD or write automated unit tests for some kinds of modules or applications and the one on the top of these kinds is Console applications. How could I test a console application when the input is passed by key strokes and the output is presented on a screen?!!
Actually this happens from time to time, you find yourself trying to write automated unit tests for something you seem to not have any control upon. But, the truth is, you just missed the point, you don't want to test the "Console" application, you want to test the business logic behind it.
When 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. So, this leads us to the next point.
After pinpointing what you really want to test and what you don't, you have to think 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 that is what we are going to do in the rest of the post.
Here is what we are going to do:
1. Build a simple (trivial) Console application first in the bad way
2. Re-implement it in the good way
3. Write some automated unit tests
1. Build a simple (trivial) Console application first in the bad way
Here is what we are going to build.Create a VS project of type "Console Application" and name it "MyConsoleApp". Change the code inside the "Program" class to be as follows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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) { string 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"); } } } |
2. Re-implement it in the good way
Create a "ConsoleManager" class library project and add the following classes to the project.IConsoleManager.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 | using System; namespace ConsoleManager { public interface IConsoleManager { void Write(string value); void WriteLine(string value); ConsoleKeyInfo ReadKey(); string ReadLine(); void Clear(); } } |
ConsoleManagerBase.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; 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); } } |
ConsoleManager.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | using System; namespace ConsoleManager { public class ConsoleManager : ConsoleManagerBase { #region ConsoleManagerBase Implementations 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); } #endregion } } |
ConsoleManagerStub.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | using System; using System.Collections.Generic; using System.Text; namespace ConsoleManager { public class ConsoleManagerStub : ConsoleManagerBase { #region Fields private int currentOutputEntryNumber = 0; private Queue<object> userInputs = new Queue<object>(); private List<string> outputs = new List<string>(); #endregion #region Properties public Queue<object> UserInputs { get { return userInputs; } } public List<string> Outputs { get { return outputs; } } #endregion #region Events public event Action<int> OutputsUpdated; public event Action OutputsCleared; #endregion #region ConsoleManagerBase Implementations public override void Clear() { currentOutputEntryNumber++; outputs.Clear(); OnOutputsCleared(); OnOutputsUpdated(currentOutputEntryNumber); } public override ConsoleKeyInfo ReadKey() { ConsoleKeyInfo result = new ConsoleKeyInfo(); object input = null; if (userInputs.Count > 0) { input = userInputs.Dequeue(); } else { throw new Exception("No input was presented when an inpput was expected"); } if (input is ConsoleKeyInfo) { result = (ConsoleKeyInfo)input; } else { throw new Exception("Invalid input was presented when ConsoleKeyInfo was expected"); } return result; } public override string ReadLine() { string result = null; object input = null; if (userInputs.Count > 0) { input = userInputs.Dequeue(); } else { throw new Exception("No input was presented when an inpput was expected"); } if (input is string) { result = (string)input; WriteLine(result); } else { throw new Exception("Invalid input was presented when String was expected"); } return result; } public override void Write(string value) { outputs.Add(value); currentOutputEntryNumber++; OnOutputsUpdated(currentOutputEntryNumber); } public override void WriteLine(string value) { outputs.Add(value + "\r\n"); currentOutputEntryNumber++; OnOutputsUpdated(currentOutputEntryNumber); } #endregion #region Events Handlers protected void OnOutputsUpdated(int outputEntryNumber) { if (OutputsUpdated != null) { OutputsUpdated(outputEntryNumber); } } protected void OnOutputsCleared() { if (OutputsCleared != null) { OutputsCleared(); } } #endregion #region Object Overrides public override string ToString() { string result = string.Empty; if (outputs != null && outputs.Count > 0) { StringBuilder builder = new StringBuilder(); foreach (string output in outputs) { /*if (output.Contains("\n")) { string[] parts = output.Split('\n'); foreach (string part in parts) { if (!string.IsNullOrEmpty(part)) { builder.AppendLine(part); } } } else { builder.Append(output); }*/ builder.Append(output); } result = builder.ToString(); } return result; } #endregion } } |
Apply the following modifications to the "MyConsoleApp" project after installing the Nugget package "Ninject".
Add ProgramManager.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | using ConsoleManager; namespace MyConsoleApp { public class ProgramManager { #region Fields private IConsoleManager consoleManager = null; #endregion #region Constructors public ProgramManager(IConsoleManager consoleManager) { this.consoleManager = consoleManager; } #endregion #region Methods public void Run(string[] args) { string input = string.Empty; do { consoleManager.WriteLine("Welcome to my console app"); consoleManager.WriteLine("[1] Say Hello?"); consoleManager.WriteLine("[2] Say Goodbye?"); consoleManager.WriteLine(""); consoleManager.Write("Please enter a valid choice: "); input = consoleManager.ReadLine(); if (input == "1" || input == "2") { consoleManager.Write("Please enter your name: "); string name = consoleManager.ReadLine(); if (input == "1") { consoleManager.WriteLine("Hello " + name); } else { consoleManager.WriteLine("Goodbye " + name); } consoleManager.WriteLine(""); consoleManager.Write("Press any key to exit... "); consoleManager.ReadKey(); } else { consoleManager.Clear(); } } while (input != "1" && input != "2" && input != "Exit"); } #endregion } } |
Add NinjectDependencyResolver.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 | using Ninject.Modules; using ConsoleManager; namespace MyConsoleApp { public class NinjectDependencyResolver : NinjectModule { public override void Load() { Bind<IConsoleManager>().To<ConsoleManager.ConsoleManager>(); } } } |
Update Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using ConsoleManager; using Ninject; using System.Reflection; namespace MyConsoleApp { class Program { #region Fields private static ProgramManager programManager = null; private static IConsoleManager consoleManager = null; #endregion static void Main(string[] args) { StandardKernel kernel = new StandardKernel(); kernel.Load(Assembly.GetExecutingAssembly()); consoleManager = kernel.Get<IConsoleManager>(); programManager = new ProgramManager(consoleManager); programManager.Run(args); } } } |
3. Write some automated unit tests
Create a "MyConsoleApp.Tests" class library project, install the Nugget package "NUnit", from VS Tools > Extensions and Updates > Search online for "Nunit Test Adapter" and install it and finally add the following classes to the project.ProgramManagerTests.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | using ConsoleManager; using NUnit.Framework; using System; using System.Collections.Generic; namespace MyConsoleApp.Tests { [TestFixture] public class ProgramManagerTests { #region Fields private ConsoleManagerStub consoleManager = null; private ProgramManager programManager = null; #endregion [SetUp] public void SetUp() { consoleManager = new ConsoleManagerStub(); programManager = new ProgramManager(consoleManager); } [TearDown] public void TearDown() { programManager = null; consoleManager = null; } [TestCase("Ahmed Tarek")] [TestCase("")] [TestCase(" ")] public void RunWithInputAs1AndName(string name) { consoleManager.UserInputs.Enqueue("1"); consoleManager.UserInputs.Enqueue(name); consoleManager.UserInputs.Enqueue(new ConsoleKeyInfo()); List<string> expectedOutput = new List<string>(); expectedOutput.Add("Welcome to my console app\r\n"); expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n"); expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n"); expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n"); expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: "); expectedOutput.Add("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"); expectedOutput.Add("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: "); expectedOutput.Add("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"); expectedOutput.Add("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"); expectedOutput.Add("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"); expectedOutput.Add("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... "); consoleManager.OutputsUpdated += (int outputEntryNumber) => { Assert.AreEqual(expectedOutput[outputEntryNumber - 1], consoleManager.ToString()); }; programManager.Run(new string[] { }); } [TestCase("Ahmed Tarek")] [TestCase("")] [TestCase(" ")] public void RunWithInputAs2AndName(string name) { consoleManager.UserInputs.Enqueue("2"); consoleManager.UserInputs.Enqueue(name); consoleManager.UserInputs.Enqueue(new ConsoleKeyInfo()); List<string> expectedOutput = new List<string>(); expectedOutput.Add("Welcome to my console app\r\n"); expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n"); expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n"); expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n"); expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: "); expectedOutput.Add("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"); expectedOutput.Add("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: "); expectedOutput.Add("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"); expectedOutput.Add("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"); expectedOutput.Add("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"); expectedOutput.Add("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... "); consoleManager.OutputsUpdated += (int outputEntryNumber) => { Assert.AreEqual(expectedOutput[outputEntryNumber - 1], consoleManager.ToString()); }; programManager.Run(new string[] { }); } [Test] public void RunShouldKeepTheMainMenuWhenInputIsNeither1Nor2() { consoleManager.UserInputs.Enqueue("any invalid input 1"); consoleManager.UserInputs.Enqueue("any invalid input 2"); consoleManager.UserInputs.Enqueue("Exit"); List<string> expectedOutput = new List<string>(); // initital menu expectedOutput.Add("Welcome to my console app\r\n"); // outputEntryNumber 1 expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n"); // outputEntryNumber 2 expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n"); // outputEntryNumber 3 expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n"); // outputEntryNumber 4 expectedOutput.Add("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 expectedOutput.Add("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 expectedOutput.Add(""); // outputEntryNumber 7 expectedOutput.Add("Welcome to my console app\r\n"); // outputEntryNumber 8 expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n"); // outputEntryNumber 9 expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n"); // outputEntryNumber 10 expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n"); // outputEntryNumber 11 expectedOutput.Add("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 expectedOutput.Add("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 expectedOutput.Add(""); // outputEntryNumber 14 expectedOutput.Add("Welcome to my console app\r\n"); // outputEntryNumber 15 expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n"); // outputEntryNumber 16 expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n"); // outputEntryNumber 17 expectedOutput.Add("Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n"); // outputEntryNumber 18 expectedOutput.Add("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 expectedOutput.Add("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 consoleManager.OutputsUpdated += (int outputEntryNumber) => { if ((outputEntryNumber - 1) < expectedOutput.Count) { Assert.AreEqual(expectedOutput[outputEntryNumber - 1], consoleManager.ToString()); } }; programManager.Run(new string[] { }); } } } |
That's it, now you have successfully written automated unit tests for a Console application.
You can find the code samples used in this post on this GitHub repository.