Unit Testing the Right Way (or at least the less wrong way) Part 1

On this blog, I’ve covered automated UI testing and automated HTTP testing. This post will cover the bottom of the agile automated test pyramid, unit testing.

AATP

I will use a demo application named FlightSim to help us learn some basic unit testing techniques.

Each unit test should test one and only one unit.

In order to keep tests simple and to avoid ambiguity each test should test one and only one unit.

For example, in our FlightSim entities, we have IsValid() methods that determine if the entity is valid or not. Here is the Airplane entity.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FlightSim.Entity
{
    public class Airplane
    {
        public Airplane() { }
        public Airplane(String name, Int32 numberOfSeats, Int32 rows, Int32 seatsPerRow)
        {
            Name = name;
            NumberOfSeats = numberOfSeats;
            Rows = rows;
            SeatsPerRow = seatsPerRow;

            var errorMessages = this.IsValid();
            if (errorMessages.Any())
            {
                throw new Exception("Invalid airplane." + Environment.NewLine + String.Join(Environment.NewLine, errorMessages));
            }
        }
        public String Name { get; set; }
        public Int32 NumberOfSeats { get; set; }
        public Int32 Rows { get; set; }
        public Int32 SeatsPerRow { get; set; }

        public IEnumerable<String> IsValid()
        {
            var errorMessages = new List<String>();

            if (String.IsNullOrWhiteSpace(Name))
            {
                errorMessages.Add("'Name' is required.");
            }
            
            if (NumberOfSeats <= 0)
            {
                errorMessages.Add("'Number of seats' must be greater than 0.");
            }
            else
            {
                if (Rows * SeatsPerRow != NumberOfSeats)
                {
                    errorMessages.Add("'Rows' and 'seats per row' does not match 'number of seats'.");
                }
            }

            return errorMessages;
        }
    }
}

It is best to write a single test for each of the validations so that when one of those tests fails, we know exactly which validation needs looking at just by looking at the name of the test.

using FlightSim.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace FlightSim.Entity.Tests
{
    [Trait("Category", "Airplane")]
    public class WhenICreateAnAirplaneWithNoName
    {
        private IEnumerable<String> _messages;

        public WhenICreateAnAirplaneWithNoName()
        {
            var airplane = new Airplane("", 1, 1, 1);
            _messages = airplane.IsValid();
        }

        [Fact]
        public void ItShouldReturnOneErrorMessage()
        {
            Assert.Equal(1, _messages.Count());
        }

        [Fact]
        public void ItShouldReturnTheCorrectErrorMessage()
        {
            Assert.Equal("'Name' is required.", _messages.First());
        }
    }
}

If we test more than one validation per test, we would have to run the test in order to determine why it failed.

xUnit

There are many frameworks out there for performing unit tests and feel free to choose the one you like the best. For this post, we will use xUnit. I believe you will find this information helpful regardless of the framework you use.

I want to take look at the test above and explain what its doing, but first I need to let you know which NuGet packages you need to build this test.

  • xunit – this contains all the binaries you need for running tests and asserting values.
  • xunit.runner.visualstudio – you need this package so that your xUnit tests display in the test explorer. You only need to add this package to one of the projects in your solution.

Traits

You will notice this attribute added to the test class.

[Trait("Category", "Airplane")]

This attribute helps you organize your unit tests. If you organize the tests in the Test Explorer by ‘trait’, you will see the tests above listed under the heading of “Category [Airplane]”.

Constructor

The constructor for this class “WhenICreateAnAirplaneWithNoName” is executed before each of the tests run. It creates the test data and then executes the IsValid() method, saving its result in a global variable to be asserted later.

Fact

You will notice this attribute added to the two methods in the class.

[Fact]

This specifies the method as a test and adds it to the Test Explorer. When you select this test to be ran, it executes the constructor then the Fact method.

The nice thing about these tests is that if you read the constructor name followed by the name of the Fact method, you can understand what the test is checking, “When I create an airplane with no name, it should return one error message”.

Up next

In the next post I will show you how implementing interfaces can make your application code more testable. Then I’ll show you how to mock those interfaces so that we can test code that interacts with other applications.

Leave a Reply

Your email address will not be published. Required fields are marked *