Journey

Writing no tests

Nobody starts programming with writing tests right away. You always start with a Hello World followed by heaps of pretty bad and untestable code. (I still remember coding a Tic-tac-toe game by writing thousands of ifs for every possible move.)

Let’s fantasize, things go different way. That after just one project everyone discovers the idea of unit testing. Just imagine everyone creates tests for every piece of code they write. The software shines, never breaks and nothing can go to /r/badcode. Seems like a perfect world.

Unfortunately, just as with code, you can write good unit tests or bad unit tests. With code, however, people agree on the definition of good, with unit tests - not so much.

Writing tests with everything mocked

During half a year of work in my previous company I used to write many tests. Not only me but my whole team tried to write as many tests as possible.

Don’t get me wrong, these tests surely helped with spotting many nasty bugs, but most of them had one major issue in them.

I bet 10 monies that you cannot find the issue.

Example of an unit test I wrote in my previous job
using FederationGatewayApi.Services;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace DIGNDB.App.SmitteStop.Testing.ServiceTest.Gateway
{
    [TestFixture]
    public class GatewayKeyProviderTests
    {
        private readonly Mock<IX509StoreWrapper> _x509StoreWrapperMock = new Mock<IX509StoreWrapper>();
        private readonly Mock<ISha256Wrapper> _sha256WrapperMock = new Mock<ISha256Wrapper>();
        private readonly Mock<IBitConverterWrapper> _bitConverterWrapperMock = new Mock<IBitConverterWrapper>();
        private readonly Mock<IPrivateKeyFactoryWrapper> _privateKeyFactoryWrapperMock = new Mock<IPrivateKeyFactoryWrapper>();
        private readonly Mock<IX509CertificateParserWrapper> _x509CertificateParserWrapperMock = new Mock<IX509CertificateParserWrapper>();

        private const string AuthenticationCertificateFingerprint = "A3C3E533CC9FEACA026F99F688F4488B5FC16BD0E6A80E6E0FC03760983DBF3F";
        private const string SigningCertificateFingerprint = "979673B55DB0B7E2B35B12CF2A342655F059314BC46323C43BCD3BFC82374BFB";

        [Test]
        public void Should_Read_Certificates_From_UserStore_First_Then_Local_Machine()
        {
            int callOrder = 0;

            _x509StoreWrapperMock.Setup(mock => mock.Initialize(StoreName.My, StoreLocation.CurrentUser))
                .Callback(() =>
                {
                    callOrder++;
                    callOrder.Should().Be(1);
                });
            _x509StoreWrapperMock.Setup(mock => mock.Initialize(StoreName.My, StoreLocation.LocalMachine))
                .Callback(() =>
                {
                    callOrder++;
                    callOrder.Should().Be(2);
                });

            try
            {
                var gatewayKeyProvider = new GatewayKeyProvider(
                    AuthenticationCertificateFingerprint,
                    SigningCertificateFingerprint,
                    _x509StoreWrapperMock.Object,
                    _sha256WrapperMock.Object,
                    _bitConverterWrapperMock.Object,
                    _privateKeyFactoryWrapperMock.Object,
                    _x509CertificateParserWrapperMock.Object);
            }
            catch (AssertionException)
            {
                throw;
            }
            catch (CryptographicException)
            {
                throw;
            }
            catch (Exception e)
            {
                // ignored because testing only interaction with X509Store
            }
        }
    }
}

You gave up? Good! The problem emerges when we have everything mocked. With everything mocked you basically have to write too much logic for the mocks. The test definitely should not care about this logic. It couples the test to the inner implementation details. In other words it breaks encapsulation because it exposes classes, which usually you should hide.

Why is it so bad? Well, many times when you refactor the code (change implementation details without changing the behavior) the tests break. A change, as simple as adding another parameter to one implementation class, could break many mocks and in result break many tests.

It happened to me so many times that I started wondering am I doing something wrong?.

Testing in a BDD style

The general gist is to test only public API of a module and keep the implementation details private. This strategy not only makes you focus on behaviours of your module (requirements), but it also makes you write fewer tests. Your tests will not break that often when they depend on the API because it should not change that often.

Ian Cooper explores this idea in the following video:

He also recommends a book: Test Driven Development: By Example by Kent Beck, which also explains this idea in-depth.

Go watch Ian and read Kent’s book to understand the topic better.

Proof of concept project

Now time for the example.

Firstly you need to decide, which classes represent the API and, which represent implementation details. Mark API classes as public and for implementation classes use the internal keyword.

using System;

namespace TestDrivenExample.ExampleModule.Internal
{
    internal class TemperatureArgumentValidator : ITemperatureArgumentValidator
    {
        public void ValidateCelsiusToKelvinArgument(double celsiusDegrees)
        {
            if (celsiusDegrees < -273.15)
                throw new ArgumentException("Temperature cannot be below absolute zero.", nameof(celsiusDegrees));
        }
    }
}

How to use those internal classes when using an IoC container? Extension methods come to rescue. Each module could expose an extension method for registering its internal classes to the container. AddExampleModule(this IServiceCollection services) would add all the classes for ExampleModule module.

using Microsoft.Extensions.DependencyInjection;
using TestDrivenExample.ExampleModule.Internal;
using TestDrivenExample.ExampleModule.PublicClasses;

namespace TestDrivenExample.ExampleModule.Configuration
{
    public static class ServicesConfiguration
    {
        public static void AddExampleModule(this IServiceCollection services)
        {
            services.AddScoped<ITemperatureConverter, TemperatureConverter>();
            services.AddScoped<IConversionRates, ConversionRates>();
            services.AddScoped<IDoubleAdder, DoubleAdder>();
            services.AddScoped<ITemperatureArgumentValidator, TemperatureArgumentValidator>();
        }
    }
}

By using an IoC container with the internal classes, they don’t get coupled to the API classes (TemperatureConverter in this case.).

using TestDrivenExample.ExampleModule.Internal;
using TestDrivenExample.ExampleModule.PublicClasses;

namespace TestDrivenExample.ExampleModule
{
    public class TemperatureConverter : ITemperatureConverter
    {
        private readonly IDoubleAdder _doubleAdder;
        private readonly IConversionRates _conversionRates;
        private readonly ITemperatureArgumentValidator _temperatureArgumentValidator;

        public TemperatureConverter(
            IDoubleAdder doubleAdder,
            IConversionRates conversionRates,
            ITemperatureArgumentValidator temperatureArgumentValidator)
        {
            _doubleAdder = doubleAdder;
            _conversionRates = conversionRates;
            _temperatureArgumentValidator = temperatureArgumentValidator;
        }

        public double ConvertFromCelsiusToKelvin(double celsiusDegrees)
        {
            _temperatureArgumentValidator.ValidateCelsiusToKelvinArgument(celsiusDegrees);

            var conversionRate = _conversionRates.GetCelsiusToKelvinConversionRate();

            return _doubleAdder.Add(celsiusDegrees, conversionRate);
        }
    }
}

You can later use this API class in any framework e.g., .NET WebAPI.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using TestDrivenExample.ExampleModule.PublicClasses;

namespace TestDrivenExample.API.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private readonly ITemperatureConverter _temperatureConverter;

        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(
            ILogger<WeatherForecastController> logger,
            ITemperatureConverter temperatureConverter)
        {
            _logger = logger;
            _temperatureConverter = temperatureConverter;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index =>
                {
                    int r = rng.Next(-20, 55);

                    return new WeatherForecast
                    {
                        Date = DateTime.Now.AddDays(index),
                        TemperatureC = r,
                        TemperatureKelvins = _temperatureConverter.ConvertFromCelsiusToKelvin(r),
                        Summary = Summaries[rng.Next(Summaries.Length)]
                    };
                })
                .ToArray();
        }
    }
}

To register the module’s classes just call services.AddExampleModule(); on the container object.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using TestDrivenExample.ExampleModule.Configuration;

namespace TestDrivenExample.API
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo {Title = "TestDrivenExample.API", Version = "v1"});
            });

            services.AddExampleModule();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TestDrivenExample.API v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
        }
    }
}

Lastly let’s look at the tests. To test an API class from a module just call the extension method of the module: serviceCollection.AddExampleModule(); and then just ask the container to initialize the class under test by calling serviceProvider.GetService<ITemperatureConverter>();.

Some might say that interacting with an IoC Container is an overkill, but it has another benefit. With the container, you don’t have to call the constructor in the test code so adding a new dependency via constructor injection won’t break the tests.

What about those internal classes? How to test them if you marked them as internal? You do not need to test them, the container will initialize them if needed, and the tests of API classes will cover them by the way.

using System;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using TestDrivenExample.ExampleModule.Configuration;
using TestDrivenExample.ExampleModule.PublicClasses;

namespace TestDrivenExample.Tests
{
    public class TemperatureConverterTests
    {
        private ITemperatureConverter _temperatureConverter;

        [SetUp]
        public void Setup()
        {
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddExampleModule();

            ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
            _temperatureConverter = serviceProvider.GetService<ITemperatureConverter>();
        }

        [TestCase(10, 283.15)]
        [TestCase(20, 293.15)]
        [TestCase(100, 373.15)]
        [TestCase(500, 773.15)]
        [TestCase(5000, 5273.15)]
        public void Should_Convert_Degrees_From_Celsius_To_Kelvin(double celsiusDegrees, double expectedResult)
        {
            var valueInKelvins = _temperatureConverter.ConvertFromCelsiusToKelvin(celsiusDegrees);

            valueInKelvins.Should().Be(expectedResult);
        }

        [TestCase(-273.16)]
        [TestCase(-373.15)]
        [TestCase(-1000)]
        public void Should_Throw_Argument_Exception_If_Input_Below_Absolute_Zero(double celsiusDegrees)
        {
            Action convertAction = () => _temperatureConverter.ConvertFromCelsiusToKelvin(celsiusDegrees);

            convertAction.Should().Throw<ArgumentException>();
        }
    }
}

Repository with the presented code