La creación de pruebas unitarias requiere al menos lo siguiente:
- Un framework de pruebas unitarias que debemos dominar. Por lo general son muy simples.
- Código testeable. Típicamente esto implica código susceptible de ser “aislado”.
Nada que decir con respecto al primer punto. Ahora, en cuanto al segundo punto, ¿qué significa que el código pueda ser “aislado”?. Bueno, esto significa que sus dependencias deben poder ser reemplazadas. Esto lo logramos bien por medio de inyectarle sus dependencias o bien utilizando un patrón ServiceLocator (a algún framework con un contenedor de dependencias).
Entonces, una vez resuelto, necesitamos proveer al código objeto de prueba, dependencias “falsas”. Esto nuevamente requiere o bien que las creemos a manos o que utilicemos algún isolation framework para crearlas (por lo general en runtime).
Hasta ahora nada que no sepamos todos ¿verdad? Pero… ¡Vamos de nuevo! Veámoslo en código por favor. Imaginemos que tenemos el siguiente método pero no podemos probarlo porque su dependencia con el LogHelper (una clase con métodos estáticos) nos lo impiden:
public class TransferManager
{
public void Tranfer(Account from, Account to, Money amount)
{
// Perform the required actions
LogHelper.Inform("Done!");
}
}
Si no quisiéramos enredarnos mucho, lo que haríamos sería algo como lo que sigue:
public class TransferManager
{
private readonly ILogger _logger;
public TransferManager(ILogger logger)
{
this._logger = logger;
}
public void Tranfer(Account from, Account to, Money amount)
{
// Perform the required actions
_logger.Inform("Done!");
}
}
Y la prueba, se parecería a lo que sigue:
[TestMethod]
public void Perform_a_valid_tranfer()
{
var aFakeLogger = new FakeLogger();
var tranferManager = new TransferManager(aFakeLogger);
// Do and Assert here
}
Lo que falta:
interface ILogger
{
void Inform(string messageToLog);
}
class FakeLogger : ILogger
{
public void Inform(string messageToLog)
{
// Nothing to do
}
}
¿Que debimos hacer para probar esa pieza de código? Bueno, varias cosas: en primer lugar fue necesario crear una interface hasta el momento innecesaria (ILogger), tuvimos que agregar un constructor para inyectarle esta dependencia juntamente con un campo para almacenarla; debimos además, y aunque no se muestre aquí, hacer un wrapper para el LogHelper que implemente la interface ILogger. Por otro lado, en el proyecto de prueba, debimos crear una clase falsa (la FakeLogger).
Creamos FakeLogger de manera manual y con su implementación más sencilla (no hace absolutamente nada) pero bien podríamos haberla creado con un framework de aislamiento y especificarle algún comportamiento.
Es decir que el código es ahora más complejo que antes ya que tiene, al menos en este ejemplo, un campo más (_logger), un constructor más, una interface más (la ILogger) y una clase más (el wrapper del LogHelper). El código que crea una instancia de esta clase (TranferManager) también se ve afectado. Este es el costo que lleva asociado ineludiblemente el probar, mediante pruebas unitarias, cualquier código.
Claro, la pregunta es: ¿Valdrá la pena?