Introduction to Unit Tests (with examples in .Net) - Part 3 - Test Frameworks and Manual Mocks

July 31, 2022

So far, in this series of posts on the basics of unit tests, we’ve spoken about concepts and methodologies, but we’ve steered away from using any specific frameworks or tools. In this post, we’ll investigate what a test framework can do for us.

We’ll continue to work with the code that we created in the previous post, but we’ll address the issues that we still had at the end of that post.

A Recap of the Story So Far

At the end of the previous post, we had the following code:



for (int i = 1; i <= 100; i++)
{
    // Arrange
    Func<string> mockInput = () => "5";
 
    // Act
    string result = RunMethod(mockInput);
 
    // Assert
    if (result == "Well done, you guessed!")
    {
        Console.WriteLine("Test Passed");
        break;
    }
}
 
for (int i = 1; i <= 100; i++)
{
    // Arrange
    Func<string> mockInput = () => "5";
 
    // Act
    string result = RunMethod(mockInput);
 
    // Assert
    if (result.StartsWith("Sorry, that was the wrong number"))
    {
        Console.WriteLine("Test Passed");
        break;
    }
}
 
{
    // Arrange
    Func<string> mockInput = () => "";
 
    // Act
    string result = RunMethod(mockInput);
 
    // Assert
    if (result == "Invalid guess")
    {
        Console.WriteLine("Test Passed");
    }
}


We had yet to introduce any tools or frameworks, but we had managed to test our code. We still had the following issues, however:

  1. The tests passed, but we visually have to visually ascertain that.
  2. We were outputting to the console needlessly.
  3. Our tests were not resilient – a change of a single character in the user output, and the tests would break.
  4. The tests were not deterministic – they were dependent on the result of a pseudo random number.

In this post, we’ll address these issues in order (apart from the third one, but we’ll come back to that) : we’ll start with the first.

1. The tests passed, but we visually have to visually ascertain that

How can we ascertain the result of a test without watching to see what happens with the test. One thing we could do is something similar to the following:



int RunTest1()
{
    for (int i = 1; i <= 100; i++)
    {
        // Arrange
        Func<string> mockInput = () => "5";

        // Act
        string result = RunMethod2(mockInput);

        // Assert
        if (result == "Well done, you guessed!")
        {
            Console.WriteLine("Test Passed");
            return 0;
        }
    }
    return 1;
}

int RunTest2()
{
    for (int i = 1; i <= 100; i++)
    {
        // Arrange
        Func<string> mockInput = () => "5";

        // Act
        string result = RunMethod2(mockInput);

        // Assert
        if (result.StartsWith("Sorry, that was the wrong number"))
        {
            Console.WriteLine("Test Passed");
            return 0;
        }
    }
    return 1;
}

int RunTest3()
{
    // Arrange
    Func<string> mockInput = () => "";

    // Act
    string result = RunMethod2(mockInput);

    // Assert
    if (result == "Invalid guess")
    {
        Console.WriteLine("Test Passed");
        return 0;
    }
    return 1;
}

Console.WriteLine(RunTest1());
Console.WriteLine(RunTest2());
Console.WriteLine(RunTest3());


There’s a lot of code here, but all we’ve actually done is wrap the tests up in functions, and then returned a value based on the result of the test. This means that we can write something like this:



if (RunTest1() != 0 | RunTest2() != 0 | RunTest3() != 0)
{
    Console.WriteLine("Some tests failed");
}

In case you didn’t know, the single pipe (|) in C# in a bitwise or - that is, it will execute all conditions regardless of the result and then evaluate the result, a logical or (||) would only run the tests until one failed and then exit the condition.

This approach also helps with the second issue.

2. We were outputting to the console needlessly

We’re outputting to the console for two reasons: the first is to validate the tests; we can now simply remove all of those from the test, since we have an actual value that we can test against. The second reason is that the code itself outputs to the console. As has been mentioned in a previous post, there are ways to redirect the output of the console without mocking it; however, I’m trying to keep this series generic, and that is specific to the .Net console (although I strongly suspect that most languages provide a similar concept).

To get around this, we could replicate what we did in the second post; however, we can also take a slightly different approach and wrap the entire input / output functionality in its own class; for example:



    internal class ConsoleInputOutputWrapper
    {
        public void Output(string text) => Console.WriteLine(text);
        public string GetInput(string prompt)
        {
            Output(prompt);
            return Console.ReadLine();
        }
    }

This idea gives us some additional benefits - as you can see, we already have the prompt and input in a single method; and we could go further - we could do some validation inside the method, too; what if the user doesn’t enter anything:



        public string GetInput(string prompt)
        {            
            while (true)
            {
                Output(prompt);
                string? answer = Console.ReadLine();
                if (!string.IsNullOrWhiteSpace(answer)) return answer;
            }
        }

We can now replace the direct references to Console with references to this:



string RunMethod(Func<string> readData)
{
    var io = new ConsoleInputOutputWrapper();
    int myNumber = Random.Shared.Next(100) + 1;

    io.Output("Guess the number that I'm thinking between 1 - 100");
    string? guess = readData();
    string result = BusinessLogic2(myNumber, guess);
    io.Output(result);
    return result;
}

We haven’t actually changed anything here, though - the console is still being written to. We need to be able to replace the functionality within the system for our test. We can do that by replacing the concrete class with an interface.

Adding an Interface

Adding an interface is much simpler than it may sound. Let’s see what needs to change in our ConsoleInputOutputWrapper class:



internal class ConsoleInputOutputWrapper : IInputOutputWrapper

We’ve implemented an interface that we’ve named IInputOutputWrapper - we’ve named it this because it’s more generic (that is, it doesn’t actually need to be a Console).

The interface just needs to specify the public methods in the class:



    internal interface IInputOutputWrapper
    {
        void Output(string text);
        string GetInput(string prompt);
    }

Whilst this syntax is specific to C#, the concept of an interface is not.

While we’re introducing an interface, and to clean our code a little, we can extract both our RunMethod and BusinessLogic methods into their own class - let’s call it Game:



    internal class Game
    {
        public string RunMethod(Func<string> readData)
        {
            var io = new ConsoleInputOutputWrapper();
            int myNumber = Random.Shared.Next(100) + 1;

            io.Output("Guess the number that I'm thinking between 1 - 100");
            string? guess = readData();
            string result = BusinessLogic(myNumber, guess);
            io.Output(result);
            return result;
        }

        public string BusinessLogic(int myNumber, string guessedNumber)
        {
            if (string.IsNullOrEmpty(guessedNumber))
            {
                return "Invalid guess";
            }

            if (int.Parse(guessedNumber) == myNumber)
            {
                return "Well done, you guessed!";
            }
            else
            {
                return $"Sorry, that was the wrong number, I was thinking of {myNumber}";
            }
        }

    }

This makes things much simpler. We can now create a constructor, and pass in our new interface:



    internal class Game
    {
        private readonly IInputOutputWrapper \_inputOutputWrapper;

        public Game(IInputOutputWrapper inputOutputWrapper)
        {
            \_inputOutputWrapper = inputOutputWrapper;
        }

Now that we have this instance, we can simply replace the method with a reference to this instead:



        public string RunMethod()
        {            
            int myNumber = Random.Shared.Next(100) + 1;
            
            string guess = \_inputOutputWrapper.GetInput("Guess the number that I'm thinking between 1 - 100");
            string result = BusinessLogic(myNumber, guess);
            \_inputOutputWrapper.Output(result);
            return result;
        }

We can now update our tests to call this new class, but we can pass in our own version of the IInputOutputWrapper, which may look like this:



    internal class MockInputOutputWrapper : IInputOutputWrapper
    {
        public string GetInput(string prompt)
        {
            return "5";
        }

        public void Output(string text) { }
    }

The test would then look something like this:



    for (int i = 1; i <= 100; i++)
    {
        // Arrange
        var inputOutputWrapper = new MockInputOutputWrapper();
        var sut = new Game(inputOutputWrapper);

        // Act
        string result = sut.RunMethod();

        // Assert
        if (result == "Well done, you guessed!")
        {            
            return 0;
        }
    }
    return 1;

Next, we’ll skip number 3 and jump to 4.

4. The tests were not deterministic – they were dependent on the result of a pseudo random number

We can use the same pattern to create a wrapper for our random number chooser:



    internal class RandomNumberChooser : IRandomNumberChooser
    {
        public int Choose() =>
            Random.Shared.Next(100) + 1;        
    }

We can then mock that out, as before:



internal class MockRandomNumberChooser : IRandomNumberChooser
{
    public int Choose() => 12;
}

This definitely works, but it’s not brilliant. We have a few remaining issues - for example, if we want to test the number are the same, or different, we’ll need two mock classes. There are ways around this, too - for example:



    internal class MockInputOutputWrapper : IInputOutputWrapper
    {
        private readonly string \_inputValue;

        public MockInputOutputWrapper(string inputValue) =>
            \_inputValue = inputValue;        

        public string GetInput(string prompt) => \_inputValue;        

        public void Output(string text) { }
    }

We’ll come back to neater ways to achieve this in a future post, but for now, let’s put all this together and introduce a test framework.

Introducing a Test Framework

Test frameworks give you four basic things (some, in fact most, do more, but these are the absolute basics that you need - otherwise, you might as well roll your own):

1. A return value from the test run to determine whether the tests pass or fail
2. An ability to assert a value is in a given state
3. Some kind of integration into your IDE
4. Method discovery (that is, some way to mark your tests as tests)

For this example, we’ll use xUnit.net. Every language has its own options here - in .Net I’ve used MS Test, Nunit, and xUnit - and they’re all broadly the same; I’ve also seen libraries in Javascript and Python and, again, they mostly do the same stuff.

We’ll need to install the following libraries:



Install-Package Microsoft.Test.Sdk
install-package Xunit
Install-Package Xunit.Runner.Console
Install-Package Xunit.Runner.VisualStudio

This will enable you to create a test such as this:



[Fact]
public void RunMethod\_GuessedCorrectly\_CorrectTextReturned()
{
    // Arrange
    var inputOutputWrapper = new MockInputOutputWrapper("12");
    var randomNumberChooser = new MockRandomNumberChooser();
    var sut = new Game(inputOutputWrapper, randomNumberChooser);

    // Act
    string result = sut.RunMethod();

    // Assert
    Assert.Equal("Well done, you guessed!", result);
}

We no longer need to run this 100 times, because we can force a correct and incorrect guess:



        [Fact]
        public void RunMethod\_GuessedIncorrectly\_CorrectTestReturned()
        {
            // Arrange
            var inputOutputWrapper = new MockInputOutputWrapper("13");
            var randomNumberChooser = new MockRandomNumberChooser();
            var sut = new Game(inputOutputWrapper, randomNumberChooser);

            // Act
            string result = sut.RunMethod();

            // Assert
            Assert.StartsWith("Sorry, that was the wrong number", result);
        }

Summary

We’ve now seen how we can manually mock functionality, and how that can help us to accurately test methods; we’ve also introduced a testing framework. In the next post, we’ll discuss mocking frameworks, and how they can make this even easier. We’ll also re-visit the test resilience.



Profile picture

A blog about one man's journey through code… and some pictures of the Peak District
Twitter

© Paul Michaels 2024