Pages

Wednesday, February 22, 2017

TFS Continuous Integration Walk Through Part 5b - Multiple Solutions: Simple Project References

ci-logoThis is part of a series of walk throughs exploring CI in TFS, starting from the ground up. The entire series and source code are maintained at this BitBucket repository.

https://bitbucket.org/bladewolf55/tfs-ci-samples

Previous Part: TFS Continuous Integration Walk Through Part 5a - Multiple Solutions - Overview




A New Beginning

One of my goals for the Multiple Solutions walkthroughs is to put the TFS repository in a, let's say, less than stellar organizational state, and then clean up. To do that, I'll start with a new, clean TFS collection.

Open TFS Administration Console, select Team Project Collections, and click Create Collection.

2017-02-20_145443

Choose a silly name like "CICollection2". Next.

2017-02-20_145927

Verify the SQL server instance and create a new database. Next, Verify, Create.

2017-02-20_150035

It'll take a few minute to create the collection. Click Complete, then Close. You can close the Admin Console, too.

2017-02-20_150517

Connect Visual Studio to the New Collection

Complaint
This was stupidly difficult to figure out. Google searches yielded nothing.

Open Visual Studio and the Team Explorer, then open Manage Connections.

2017-02-20_161007

Drop down "Manage Connections" and choose Connect to Team Project.

2017-02-20_161132

Select CICollection2, then click Connect.

2017-02-20_161318

This is the first time I'm using this collection, so I need to map my TFS Workspaces.

Complaint
I hate TFS workspaces.

2017-02-20_161528

Pick one of the "map workspaces" links, take the defaults, click Map & Get.

2017-02-20_161942

Now I'm connected to the new collection.

2017-02-20_162046

Create a new Team Project

In Visual Studio Team Manager, create a new Team Project by choosing Home > Projects & My Teams > Create Team Project.

2017-02-20_162400

Remember, one of my intentions is a messy collection. So, I'll name my team project Main. (I'm tempted to name it "Turing" and make life even worse, but this will do.) Click Next.

2017-02-20_162607

The default Agile process is fine. Next.

2017-02-20_162652

We're sticking with Team Foundation Version Control. Click Finish.

2017-02-20_162805

After a few seconds our team project is created.

2017-02-20_162930

Add the TuringDesktop Solution

In Visual Studio, create a new Console project. In the dialog, check the Add to Source Control box.

2017-02-20_163059

In the source control dialog, accept the defaults. We're adding our solution folder to the root.

2017-02-20_163216

So, right now our TFS collection structure is:

CICollection2
|_$/Main
  |_TuringDesktop

If you remember from the previous part, our desktop app is going to have two dependencies: the Magic8Engine, and TextColor. Right now, I don't know Magic8 is going to be a shared NuGet package, so I just add it as a new Class Library project. The same with TextColor.

I'm also going to need a unit test project, so I'll add an MSTest project now as well. I'm going to name it TuringDesktop.Tests, even though it's going to contain tests for all my projects.

2017-02-20_164651

In the end, this is my solution's folder structure.

2017-02-20_164939

Our First Suite

References

Assign TextWriter to MemoryWriter
Mocking System Console Behaviour
Magic 8-Ball

Below is all the code and tests for the solution. This is my super-pre-release versions, and has some (mostly intentional) problems.

The Solution Projects

Right now I've got everything in one solution. I'll explain each project in turn.

TuringDesktop

This is a Console application. It references the Magic8Engine and TextColor projects.

2017-02-22_102302

I'm simulating a database with a class that returns a list of answers, and is initialized with a connection string. For this simple sample, the connection string is just a name like "production". If the connection string is named something other than "production", then the name is prepended to each answer. For example, if the connection string were "stage", an answer might be "STAGE: It is certain."

using System;
using System.Collections.Generic;
using System.Linq;

namespace TuringDesktop
{
    public static class AnswerDatabase
    {
        /// <summary>
        /// Any dbName other than "prod" prepends answers with dbName.
        /// Ex. dbName = "stage", then answer is "STAGE: It could be so."
        /// </summary>
        /// <param name="connectionString"></param>
        public static IEnumerable<string> GetAnswers(string connectionString)
        {
            if (String.IsNullOrWhiteSpace(connectionString))
            {
                throw new ArgumentException("dbName cannot be null or empty");
            }
            if (connectionString == "production")
            {
                return DefaultAnswers;
            }
            else
            {
                return DefaultAnswers.Select(a => connectionString.ToUpper() + ": " + a);
            }
        }

        private static IEnumerable<string> DefaultAnswers
        {
            get
            {
                yield return "It is certain";
                yield return "It is decidedly so";
                yield return "Without a doubt";
                yield return "Yes, definitely";
                yield return "You may rely on it";
                yield return "As I see it, yes";
                yield return "Most likely";
                yield return "Outlook good";
                yield return "Yes";
                yield return "Signs point to yes";
                yield return "Reply hazy try again";
                yield return "Ask again later";
                yield return "Better not tell you now";
                yield return "Cannot predict now";
                yield return "Concentrate and ask again";
                yield return "Don't count on it";
                yield return "My reply is no";
                yield return "My sources say no";
                yield return "Outlook not so good";
                yield return "Very doubtful";
            }
        }
    }
}

Here's the Program class with my Main code. There are a few things to note:

  • I removed the args parameter from the Main method. I don't need it.
  • I made the Main method public, so it can be call from unit tests.
  • I'm using Dependency Injection for the answer engine (Oracle) and the console text colorizer (ConsoleColorizer).
  • There are three settings using a string literal: _connectionString, questionColor and answerColor.
using System;
using TextColor;
using Magic8Engine;

namespace TuringDesktop
{
    public class Program
    {
        static string _connectionString = "production";
        static IConsoleColorizer _colorizer = new ConsoleColorizer();
        static IOracle _oracle = new Oracle(AnswerDatabase.GetAnswers(_connectionString));

        public Program() { }
        public Program(IConsoleColorizer colorizer, IOracle oracle)
        {
            _colorizer = colorizer;
            _oracle = oracle;
        }

        public static void Main()
        {
            string name = "";
            string answer = "";
            string question = "";
            string questionColor = "Red";
            string answerColor = "green";

            Console.Write("Welcome! I'm AT. Please tell me your name. >> ");
            name = Console.ReadLine();
            Console.WriteLine("Good to meet you, " + name + ". "
                + "Ask me a yes-or-no question and I'll give you an answer. "
                + "When you're finished, say 'bye'.");
            do
            {
                try
                {
                    _colorizer.ColorizeWriteLine("Question?", questionColor);
                    question = Console.ReadLine();
                    if (question.ToLower() == "bye") { break; }
                    answer = _oracle.GetAnswer();
                    _colorizer.ColorizeWriteLine(answer, answerColor);
                    Console.WriteLine();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error: " + ex.GetBaseException().Message);
                    Console.Write("Press any key to quit.");
                    Console.Read();
                    return;
                }
            }
            while (true);
        }
    }
}

The program's output is simple enough. It greets the user, asks for a name, then starts answering questions. Text coloring is used to make things clear.

2017-02-22_105715

Magic8Engine

I have an IOracle inteface, so that it's easier to use in unit tests.

namespace Magic8Engine
{
    public interface IOracle
    {
        string GetAnswer();
    }
}

The concrete class is initialized with a list of answers (the "database") from which it randomly selects.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Magic8Engine
{
    public class Oracle : IOracle
    {
        Random _random = new Random();
        private IEnumerable<string> _answers = new List<string>();

        public Oracle(IEnumerable<string> answers)
        {
            _answers = answers;
        }

        public string GetAnswer()
        {
            if (_answers.Count() == 0)
            {
                return "";
            }
            int index = _random.Next(_answers.Count());
            return _answers.ToArray()[index];
        }
    }
}

TextColor

The ConsoleColorizer class also implements an interface to improve unit testing.

using System;

namespace TextColor
{
    public interface IConsoleColorizer
    {
        void ColorizeWriteLine(string text, string colorName, bool resetColor = true);
        ConsoleColor GetConsoleColor(string colorName);
        void ResetConsoleColor();
        void SetConsoleColor(string colorName);
    }
}

The concrete class sets properties on the Console object.

using System;

namespace TextColor
{
    public class ConsoleColorizer : IConsoleColorizer
    {
        public void ColorizeWriteLine(string text, string colorName, bool resetColor = true)
        {
            SetConsoleColor(colorName);
            Console.WriteLine(text);
            if (resetColor) { Console.ResetColor(); }
        }

        public ConsoleColor GetConsoleColor(string colorName)
        {
            ConsoleColor color;
            if (Enum.TryParse<ConsoleColor>(colorName, true, out color))
            {
                return color;
            }
            else
            {
                throw new ArgumentException("Invalid ConsoleColor: " + colorName);
            }
        }

        public void SetConsoleColor(string colorName)
        {
            Console.ForegroundColor = GetConsoleColor(colorName);
        }

        public void ResetConsoleColor()
        {
            Console.ResetColor();
        }
    }
}

This should raise a red flag. System.Console is a dependency. Why didn't I abstract that out? I could have. For example, I could have created an IConsole interface with just the features I'm using, then an explicit SystemConsole wrapper class that implented the interface. But it turned out I could test Console successfully without needing to make it entirely replacable.

Good question though. Glad you're thinking.

Tests

Finally, our test project has a class for each project we're testing.

2017-02-22_112309

Here are the unit tests for each project. I'm using nested classes to keep the tests readable.

Magic8 Tests

Note how these test make use of passing in a custom "answer database".

using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Magic8Engine;

namespace TuringDesktop.Tests
{
    [TestClass]
    public class OracleClass
    {
        [TestClass]
        public class GetAnswer_Should: OracleClass
        {
            static List<string> _answers;

            [TestInitialize]
            public void TestInitialize()
            {
                //Start each test with empty list;
                _answers = new List<string>();
            }

            [TestMethod]
            public void ReturnARandomAnswerEachTimeItIsCalled()
            {
                //arrange
                _answers.AddRange(new string[] { "a", "b", "c" });
                var oracle = new Oracle(_answers);
                //act
                List<string> answers = new List<string>();
                for (int i = 0; i < 10; i++)
                {
                    answers.Add(oracle.GetAnswer());
                }
                int uniqueAnswers = answers.Distinct().Count();
                //assert
                Assert.IsTrue(uniqueAnswers > 1);
            }

            [TestMethod]
            public void ReturnAnEmptyAnswerIfAnswerListIsEmpty()
            {
                //arrange
                var oracle = new Oracle(_answers);
                //act
                string actual = oracle.GetAnswer();
                //assert
                Assert.AreEqual("", actual, "Answer list count: " + _answers.Count());
            }
        }
    }
}

TextColor Tests

Neither of these tests confirms the correct color gets set. That's really something best left to a human to verify. But notice in WriteTheUserEnteredString how I'm using the Console.SetOut property to let me verify that user input actually gets written to the console.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.IO;
using TextColor;

namespace TuringDesktop.Tests
{
    [TestClass]
    public class ConsoleColorizerClass
    {
        [TestClass]
        public class GetConsoleColor_Should
        {
            [TestMethod]          
            public void ReturnTheEnumUsingCaseInsensitive()
            {
                //arrange
                var tc = new ConsoleColorizer();
                string colorName = "gReEn";
                Exception actualEx = null;
                string errorMsg = "";
                //Will change to Green below.
                ConsoleColor color = ConsoleColor.Black;
                //act
                try
                {
                    color = tc.GetConsoleColor(colorName);
                }
                catch (Exception ex) { actualEx = ex; errorMsg = "Threw error " + ex.GetBaseException().Message; }
                Assert.IsNull(actualEx, errorMsg);
                Assert.AreEqual(ConsoleColor.Green, color);
            }

            [TestMethod]
            public void WriteTheUserEnteredString()
            {
                //arrange
                //Store the Console output in a stringwriter.
                StringWriter sw = new StringWriter();
                Console.SetOut(sw);
                var tc = new ConsoleColorizer();
                //act
                tc.ColorizeWriteLine("blamo", "Red");
                //assert
                Assert.AreEqual("blamo\r\n", sw.ToString());
            }
        }
    }
}

Console App Tests

If you're thinking the Main method should be refactored, you're right. I'll do that later. For now, it's interesting to see how we can test a console app.

First, I'll create my mock objects. I'm not even checking MockOracle's GetAnswer method, but that's OK. What I'm guaranteeing is my unit tests have no external dependencies.

MockOracle

    public class MockOracle : Magic8Engine.IOracle
    {
        public string GetAnswer()
        {
            return "This is a fake answer";
        }
    }

MockColorizer

In this mock, I store the simulated user's input in a list, and I always return a black console color. I don't set or reset console colors.

Important
Remember, I'm not testing if the colorizer works. There are already unit tests for that. I'm testing if the Main method works. I just need my mock objects to return consistent results quickly.

    public class MockColorizer : TextColor.IConsoleColorizer
    {
        public List<string> ConsoleLines = new List<string>();


        public void ColorizeWriteLine(string text, string colorName, bool resetColor = true)
        {
            ConsoleLines.Add(text);
        }

        public ConsoleColor GetConsoleColor(string colorName)
        {
            return ConsoleColor.Black;
        }

        public void ResetConsoleColor() { }

        public void SetConsoleColor(string colorName) { }
    }

Here are the tests for the Main method. Notice how I'm using StringWriters for both Console.In and Console.Out. This lets me buffer all the responses a user would make, and capture the console's output. I use a couple of helper methods to make this work.

What I'm not trying to do is test if Console works. I'm testing if I'm my code that writes to the console works. But, unfortunately, I do have a dependency on Console.

So, are these unit tests, or integration tests? Short answer: integration. I'll make some improvements later to isolate code that doesn't depend on Console.

There's a real danger here that I'll forget to use SendUserInputs, leading to the application hanging. I know this danger exists because I did it. The tests are brittle.

    [TestClass]
    public class ProgramClass
    {
        [TestClass]
        public class Main_Should
        {
            StringWriter _consoleOut = new StringWriter();
            string UserInputs = "";

            [TestInitialize]
            public void TestInitialize()
            {
                //Store the Console output in a stringwriter.
                Console.SetOut(_consoleOut);
            }

            [TestMethod]
            public void DisplayInitialGreetingMessage()
            {
                //arrange
                AddUserInput("Charles");
                AddUserInput("bye");
                SendUserInputs();
                var mockColorizer = new MockColorizer();
                var mockOracle = new MockOracle();
                var program = new Program(mockColorizer, mockOracle);
                string expected = "Welcome! I'm AT. Please tell me your name.";
                //act
                Program.Main();
                //assert
                string output = _consoleOut.ToString();
                Assert.IsTrue(output.IndexOf(expected) >= 0);
            }

            [TestMethod]
            public void DisplayWelcomeWithName()
            {
                //arrange
                AddUserInput("Charles");
                AddUserInput("bye");
                SendUserInputs();
                var mockColorizer = new MockColorizer();
                var mockOracle = new MockOracle();
                var program = new Program(mockColorizer, mockOracle);
                string expected = "Good to meet you, Charles.";
                //act
                Program.Main();
                //
                //assert
                string output = _consoleOut.ToString();
                Assert.IsTrue(output.IndexOf(expected) >= 0, "Output was: " + output);
            }

            #region "Test Helpers"
            private void AddUserInput(string value)
            {
                UserInputs += value + Environment.NewLine;
            }

            private void SendUserInputs()
            {
                //Send all the inputs needed for the Read and ReadLine statements
                StringReader consoleIn = new StringReader(UserInputs);
                Console.SetIn(consoleIn);
            }
            #endregion
        }
    }

Running my tests, I get nice, readable output.

2017-02-22_115207

Initial Continuous Integration Build Definition

In Visual Studio Team Explorer, click Builds.

2017-02-22_115629

Click New Build Definition.

2017-02-22_115709

This will open the web page and prompt for a definition. This is nice because we're taken directly to our team project builds.

2017-02-22_115934

Choose the Visual Studio template, check the box that says Continuous Integration and accept the defaults. Save the definition, naming it TuringDesktop Suite Build.

If you've been following along, you'll realize that we've never committed any of our source code! Do so now and the build should happen automatically and successfully. Be sure to verify that all the tests ran!

2017-02-22_122709

2017-02-22_122936

Next Up

We have a working application that's automatically built in our CI server. But we've also got some problems:

  • Refactoring for the main application's Console dependency.
  • String literals for settings.
  • Our application's data source is the same in development, CI, and production.
  • No separate integration tests.

In short, we're about to go from nice, sunny sample development to real-world, why-does-this-have-to-be-so-hard programming.


Next Part: TFS Continuous Integration Walk Through Part 5c - Multiple Solutions: Build Settings

No comments:

Post a Comment