Por Luis Rodríguez
En el mundo del testing, tarde o temprano necesitaremos mockear algo (red, acceso a ficheros, …) para poder focalizarnos en la propia lógica de negocio, y aislarnos de la infraestructura o mecanismo de entrega. Normalmente mockeamos interfaces, porque por definición establecen los contratos de lo que va a ser el comportamiento, pero y ¿si no tenemos la necesidad de mockear una interfaz?
En LeanMind acompañamos a empresas para hacer software de una forma más sostenible. Antes de continuar, simplemente comentar que los ejemplos se hacen bajo el ecosistema de dotnet y la librería para mockear es NSubstitute y el framework de tests es xUnit.
Esta semana estaba haciendo pair-programming con otro leanminder, desarrollando una feature que necesitaba un cliente al que estamos ayudando. Teníamos la necesidad de tener un objeto en el que su responsabilidad era tener algunos valores de configuración. Esos valores estaban en un fichero de configuración que viene determinado por la interfaz IConfiguration. Lo hemos encapsulado en la clase Configuration, porque IConfiguration está fuertemente vinculado al fichero de configuracion, y queríamos que esa dependencia no se propagara por otras capas de la aplicación, por ello, lo hemos escondido en la clase Configuration. De esta manera, si tenemos que cambiar la forma en la que se obtienen los valores de configuración, o los obtenemos de otra fuente, será más fácil el cambio. Lo podemos ver a continuación:
public class Configuration
{
private readonly IConfiguration configuration;
public Configuration(IConfiguration configuration)
{
this.configuration = configuration;
}
public int GetMaxAllowedNumber()
{
return Convert.ToInt32(configuration["MaxAllowedNumber"]);
}
}
Por otra parte, tenemos un servicio que tiene como colaborador la clase configuración y que dependiendo de esa configuración tiene un comportamiento u otro, como podemos apreciar en la siguientes líneas:
public class Service
{
private readonly Configuration configuration;
public Service(Configuration configuration)
{
this.configuration = configuration;
}
public void Execute(int number)
{
if (configuration.GetMaxAllowedNumber() < number)
throw new Exception();
}
}
En LeanMind, parte de nuestra forma de hacer las cosas, es tener el código que va a ir a producción respaldado por unos buenos tests, es un sello de nuestra identidad como organización comprometidos con la calidad del código y el buen funcionamiento del software. Un ejemplo de ello son las siguientes líneas de código:
public class ServiceShould
{
private Configuration configuration;
private Service service;
public ServiceShould()
{
configuration = Substitute.For<Configuration>();
service = new Service(configuration);
}
[Fact]
public void ThrowAnExceptionWhenTheNumberIsBiggerThanAllowedNumber()
{
const int maxAllowed = 3;
const int aNumber = 4;
configuration.GetMaxAllowedNumber().Returns(maxAllowed);
Assert.Throws<Exception>(() => service.Execute(aNumber));
}
[Fact]
public void DoNothingWhenTheNumberIsEqualToAllowedNumber()
{
const int maxAllowed = 4;
const int aNumber = 4;
configuration.GetMaxAllowedNumber().Returns(maxAllowed);
var exception = Record.Exception(() => service.Execute(aNumber));
Assert.Null(exception);
}
[Fact]
public void DoNothingWhenTheNumberIsSmallerThanAllowedNumber()
{
const int maxAllowed = 4;
const int aNumber = 3;
configuration.GetMaxAllowedNumber().Returns(maxAllowed);
var exception = Record.Exception(() => service.Execute(aNumber));
Assert.Null(exception);
}
}
Podemos observar que hemos tenido que mockear la clase Configuration. En este escenario no tiene sentido crear una interfaz para poder leer los parámetros de configuración, porque solo tiene una única implementación.
Cuando ejecutamos los tests, nos encontramos con el siguiente error:
NSubstitute.Exceptions.CouldNotSetReturnDueToNoLastCallException : Could not find a call to return from.
Make sure you called Returns() after calling your substitute (for example: mySub.SomeMethod().Returns(value)),
and that you are not configuring other substitutes within Returns() (for example, avoid this: mySub.SomeMethod().Returns(ConfigOtherSub())).
If you substituted for a class rather than an interface, check that the call to your substitute was on a virtual/abstract member.
Return values cannot be configured for non-virtual/non-abstract members.
Lo que nos está queriendo decir el error, es que para poder mockear un método de una clase, tiene que estar abierto. Esto es así, porque los diseñadores del lenguaje lo hicieron para que los métodos de las clases fueran cerrados.
Por lo tanto, para solucionar este error hacemos lo siguiente:
public class Configuration
{
private readonly IConfiguration configuration;
public Configuration(IConfiguration configuration)
{
this.configuration = configuration;
}
public virtual int GetMaxAllowedNumber()
{
return Convert.ToInt32(configuration["MaxAllowedNumber"]);
}
}
Podemos ver que el método GetMaxAllowedNumber le hemos añadido la palabra reservada virtual, para hacer este método abierto.
Al volver a ejecutar los tests, nos volvemos a encontrar con el siguiente error:
System.ArgumentException : Can not instantiate proxy of class: Client.Project.Configuration.
Could not find a parameterless constructor. (Parameter 'constructorArguments')
---- System.MissingMethodException : Constructor on type 'Castle.Proxies.ConfigurationProxy' not found.
Lo que nos quiere decir este error, es que necesitamos definir un constructor sin parámetros, para poder mockearlo de una forma adecuada, como se puede observar a continuación:
public class Configuration
{
private readonly IConfiguration configuration;
//For testing purposes
public Configuration()
{
}
public Configuration(IConfiguration configuration)
{
this.configuration = configuration;
}
public virtual int GetMaxAllowedNumber()
{
return Convert.ToInt32(configuration["MaxAllowedNumber"]);
}
}
De esta forma al volver a ejecutar los tests, obtenemos un verde.
Conclusión:
Como podemos observar, no siempre hay que mockear interfaces para obtener el comportamiento deseado. Es verdad que C# tiene la particularidad por diseño del lenguaje de establecer las clases cerradas, como hemos podido ver. Pero teniendo en cuenta este pequeño truco, ya podemos mockear las clases.
Espero que haya sido este artículo de vuestro agrado Happy coding and happy mocking!!!!
¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.
Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?
Pero espera 🖐 que tenemos un conflicto interno. A nosotros las newsletter nos parecen 💩👎👹 Por eso hemos creado la LEAN LISTA, la primera lista zen, disfrutona y que suena a rock y reggaeton del sector de la programación. Todos hemos recibido newsletters por encima de nuestras posibilidades 😅 por eso este es el compromiso de la Lean Lista