Usando tests para validar convenciones

Cuando hablamos de utilizar tests automatizados casi siempre pensamos en formas de poder validar que el funcionamiento es el correcto y para ello podemos usar distintos tipos de tests: unitarios, de extremo a extremo, de aprobación, basados en propiedades, etc.

Sin embargo, hay otras cosas que podemos validar con tests y que, si bien no forman parte del comportamiento observable por el usuario de la aplicación, son fundamentales para el correcto funcionamiento de la misma. Entre ellas, encontramos el uso de convenciones que se puso tan de moda desde el surgimiento de frameworks como Rails y su convention over configuration.

Las convenciones nos permiten escribir menos código y menos configuración porque nos ayudan a definir de forma implícita determinadas relaciones. Por ejempo, toda clase cuyo nombre acabe en Controller debe ser registrada en el contenedor IoC, o todas las propiedades de tipo string en una entidad se deben mapear como NVarChar(200) en nuestro ORM.

Un problema que puede existir con este tipo de convenciones es que nos podemos olvidar de aplicarlas correctamente y no es fácil darse cuenta hasta que es demasiado tarde y estamos realizando pruebas manuales o, peor aún, tenemos la aplicación desplegada en producción.

Por suerte, podemos usar tests automatizados para validar muchas de estas convenciones y detectar los errores temprano. Aunque la manera de hacerlo depende mucho del proyecto, la idea es utilizar reflection (o incluso algo más potente como Mono.Cecil para inspeccionar assemblies) para comprobar que se cumplen las convenciones.

Un ejemplo: comprobando la definición de permisos en una aplicación

Si necesitamos un mecanismmo de autorización sencillo, podemos encapsular las operaciones en commands y colocarles un atributo con el permiso necesario para ejecutarlos, de forma que luego podamos validar que el usuario actual tiene permisos para ejecutar el command antes de invocarlo:

[Requires(Permission.PlaceOrder)]
public class PlaceOrderCommand : ICommand
{
  // ...
}

public class CommandExecutor
{
    public void Execute(ICommand command)
    {
        var permission = command.GetType().GetCustomAttribute<RequiresAttribute<().Permission;

        if (!CurrentUser.HasPermission(permission))
            throw new SecurityException();

        command.Execute();
    }
}

El código es lo de menos, pero nos sirve para tener un contexto sobre el que trabajar en el ejemplo. Lo que está claro es que al final hay que acordarse de poner el permiso en el Command y, créeme, antes o después se te olvidará. Lo mejor para evitar ese problema es tener un test tal que así:

[Test]
public void AllCommandsHaveRequiresAttribute()
{
    var invalidCommands = typeof (ICommand).Assembly.GetTypes()
        .Where(x => typeof (ICommand).IsAssignableFrom(x))
        .Where(x => x.GetCustomAttribute<RequiresAttribute>() == null)
        .Select(x => x.Name)
        .ToArray();

    if (invalidCommands.Any())
        Assert.Fail("Los siguientes commmands no tienen permiso asociado: {0}", 
                     string.Join(",", invalidCommands));
}

Probablemente el siguiente problema que tengamos sea que para crear un command acabemos copiando uno antiguo y se nos olvide cambiar el permiso asociado. Es fácil evitarlo también con un test:

[Test]
public class AllCommandsHaveDifferentPermissions()
{
    var duplicated = typeof(ICommand).Assembly.GetTypes()
        .Where(x => typeof(ICommand).IsAssignableFrom(x))
        .Where(x => x.GetCustomAttribute<RequiresAttribute>() != null)
        .GroupBy(x => x.GetCustomAttribute<RequiresAttribute>().Permission)
        .Where(x => x.Count() > 1)
        .ToArray();

    if (duplicated.Any())
      Assert.Fail("Los siguientes permisos están duplicados: {0}",
                  string.Join(",", duplicated.Select(x => x.Key)));
}

Esto es sólo un ejemplo de lo que se puede hacer con este tipo de tests. Con un poco de imaginación, puedes llegar a adecuarlos a muchas situaciones distintas y, mezclados con tests de aprobación pueden montar un sistema de diagnóstico bastante completo y fácil de mantener.

Conclusiones

Los tests automatizados no sólo nos sirven para validar el comportamiento «funcional» de la aplicación; también podemos usarlos para detectar rápidamente problemas en nuestras convenciones.

La ventaja de automatizar estas pruebas es que, en general, los problemas con las convenciones sólo los vamos a detectar en tiempo de ejecución y, probablemente, sólo al acceder a determinadas partes de la aplicación, por lo que es fácil pasarlos por alto.

Si en un proyecto comento dos veces el mismo error por no aplicar correctamente una convención (por ejemplo duplicar un permiso en el ejemplo anterior), siempre intento escribir un test que valide que no lo vuelvo a cometer. Son tests fáciles de escribir, fáciles de mantener y, a medio plazo, ahorran mucho trabajo.

Un comentario en “Usando tests para validar convenciones

  1. Pingback: Introducing IoCTesting |

Comentarios cerrados.