Hablemos sobre .Net y Azure

Etiqueta: Asp.Net Core

Versionando APIs con .NET 5 (Net-Baires)

Uno de los grandes desafíos a la hora de diseñar y desarrollar REST APIs es cómo encarar aquellos cambios que afectan a los clientes, conocidos como breaking changes.

Para evitar los breaking changes es indispensable aplicar una estrategia de versionado clara y efectiva.

En esta sesión cuento todo lo que necesitamos saber para versionar nuestras APIs de forma eficiente y efectiva utilizando todo el poder que .NET 5 nos otorga.

El código completo de los ejemplos, como siempre disponible en mi GitHub.

Código más testeable con ISystemClock

Te propongo examinar juntos el siguiente fragmento de código:

public class BirthDayCalculator
{
    public int CalculateNextBirthDayInDays(
        int month,
        int day)
    {
        var today = DateTime.UtcNow;
        var birthDay = new DateTime(today.Year, month, day);

        if (birthDay < today)
            birthDay = birthDay.AddYears(1);

        return (int)(birthDay -  today).TotalDays;
    }
}

A primera vista se ve bien, dados un mes y un día, el método CalculateNextBirthDayInDays() calcula y retorna la cantidad de días restantes para el próximo cumpleaños.

Sin embargo, algo no huele del todo bien. Ese método no soporta test unitarios.

Problema

Dos de los requesitos estrictamente necesarios de un test unitario, es que debe tener absoluto control del contexto de ejecución y debe ser repetible.

La solución que vimos en la sección anterior, funciona, pero depende de la propiedad estática DateTime.UtcNow. Debido a que no podemos controlar los valores que dicha propiedad puede tomar, no tenemos forma de escribir casos de prueba controlados y repetibles.

Alternativa poco elegante

Una alternativa simple al problema planteado es que el método CalculateNextBirthDayInDays() reciba un parámetro adicional representando la fecha y hora actual. Algo más o menos así:

public class BirthDayCalculator
{
    public int CalculateNextBirthDayInDays(
        int month,
        int day,
        DateTime today)
    {
        var birthDay = new DateTime(today.Year, month, day);

        if (birthDay < today)
            birthDay = birthDay.AddYears(1);

        return (int)(birthDay -  today).TotalDays;
    }
}

Esta modificación, poco elegante pero efectiva, nos permite escribir pruebas como la siguiente:

[Fact]
public void CalculateNumberOfDays()
{
    // Arrange
    var sut = new BirthDayCalculator();

    // Act
    var actualDays = sut.CalculateNextBirthDayInDays(
        month: 3,
        day: 13, 
        today: DateTime.UtcNow);

    // Assert
    Assert.Equal(290, actualDays);
}

Esta implementación tiene los siguientes puntos en contra:

  • No tenemos una forma común o servicio para obtener la fecha actual en forma testeable y centralizada, generando casi con seguridad código repetido.
  • Cada vez que es necesario realizar cálculos con fechas, necesitamos agregar un parámetro adicional. Incluso en aquellos casos donde se haga uso de del método CalculateNextBirthDayInDays().
public int ConsumingCode(DateTime today)
{
    int month = 3, day = 13;
    var calculator = new BirthDayCalculator();

    return calculator.CalculateNextBirthDayInDays(
        month,
        day,
        today);
}

ISystemClock al rescate

El equipo de Microsoft ha creado una bonita interfaz junto con una implementación estándar que se encuentra disponible en el paquete Microsoft.AspNetCore.Authentication llamada ISystemClock y SystemClock respectivamente.

Esta interfaz nos permite solucionar los dos puntos anteriores. Por un lado, no necesitamos agregar parámetros extras a todos los métodos, por el otro, nos permite utilizar inyección de dependencias para centralizar una única implementación.

namespace Microsoft.AspNetCore.Authentication
{
    public interface ISystemClock
    {
        DateTimeOffset UtcNow { get; }
    }
}

¿Cómo la usamos?

Vamos a modificar la clase BirthDayCalculator para recibir por parámetro en el constructor mediante inyección de dependencias la interfaz ISystemClock.

Luego simplemente reemplazamos la llamada a DateTime.UtcNow por _systemClock.UtcNow.

public class BirthDayCalculator
{
    private readonly ISystemClock _systemClock;

    public BirthDayCalculator(ISystemClock systemClock)
    {
        _systemClock = systemClock;
    }

    public int CalculateNextBirthDayInDays(
        int month,
        int day)
    {
        var today = _systemClock.UtcNow;
        var birthDay = new DateTime(today.Year, month, day);

        if (birthDay < today)
            birthDay = birthDay.AddYears(1);

        return (int)(birthDay - today).TotalDays;
    }
}

¿Cómo lo testeamos?

Primero generaremos una clase fake, la cuál llamaremos FakeSystemClock, que implemente la interfaz y que reciba mediante un parámetro en el constructor la fecha y hora a retornar.

public class FakeSystemClock : ISystemClock
{
    private readonly DateTime _desiredDay;

    public FakeSystemClock(DateTime desiredDay)
    {
        _desiredDay = desiredDay;
    }

    public DateTimeOffset UtcNow => _desiredDay;
}

Finalmente, modificamos el test para generar una nueva instancia de FakeSystemClock y pasamos esa instancia a nuestro BirthDayCalculator.

[Fact]
public void CalculateNumberOfDays()
{
    // Arrange
    var desiredDay = new DateTime(2021, 5, 26, 10, 0, 0);
    var fakeSystemClock = new FakeSystemClock(desiredDay);
    var sut = new BirthDayCalculator(fakeSystemClock);

    // Act
    var actualDays = sut.CalculateNextBirthDayInDays(
        month: 3,
        day: 13);

    // Assert
    Assert.Equal(290, actualDays);
}

Si estuviésemos utilizando librerías como Moq o FakeItEasy, no necesitaríamos crear esta clase fake.

ISystemClock en Asp.Net Core

Si estamos en el contexto de aplicaciones Asp.Net Core, podemos utilizar la implementación estándar provista como parte del paquete.

Sólo necesitamos registrarla en nuestro contenedor IoC.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<ISystemClock, SystemClock>();

    // More services
    services.AddControllers();
}

Conclusión

De una forma muy simple y con poco refactoring, podemos eliminar las dependencias a DateTime.UtcNow y hacer nuestro código testeable gracias a ISystemClock.

Podrás encontrar el código completo de ejemplo en mi GitHub.

© 2021 Facu The Rock

Tema por Anders NorenArriba ↑