TDD and Automated Unit Testing for Console Application

Posted by Ahmed Tarek Hasan on 3/10/2018 10:32:00 AM with No comments



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.