A veces siento que soy muy enrevesado expresándome pero es lo que hay, no hay una manera menos enrevesada de explicarlo. El desenrevesador que lo desenrevese buen desenrevesador será. XD!
public interface ISumCalculatorService
{
int SumaDosNumeros(int num1, int num2);
}
public interface IRestCalculatorService
{
int RestaDosNumeros(int num1, int num2);
}
public interface ICalculator
{
int SumaDosNumeros(int num1, int num2);
int RestaDosNumeros(int num1, int num2);
}
Bien, pues ya va siendo hora de empezar a pensar en tests. Como decía, lo primero que hay que probar es la construcción del objeto calculadora, la inyección de dependencias, y para ello voy a crear una clase WhenCreatingCalculadora que, para cumplir con la metodología AAA heredará de UnitTestBase. Como recordatorio voy a pegar el código de UnitTestBase pero ya estaba disponible en “Introducción a los Tests Unitarios, TDD y Mocking“:
using Microsoft.VisualStudio.TestTools.UnitTesting;
/* *
*
* Los tests unitarios tienen un comportamiento muy simple:
*
* 1º Arrange() -> Organizar las precondiciones
* 2º Act() -> Actuar, es decir, ejecutar lo que se quiere probar
* 3º Assert() -> Verificar que se han cumplido las postcondiciones
*
* Esta visión simplista junto con una buena nomenclatura harán tu TDD mucho más sencillo.
*
* Juan García Carmona
*
* */
namespace ArrangeActAssert
{
[TestClass]
public abstract class UnitTestBase
{
[TestInitialize]
public void Init()
{
Arrange();
Act();
}
protected virtual void Arrange()
{
}
protected abstract void Act();
[TestCleanup]
public void Cleanup()
{
System.Windows.Threading.Dispatcher.CurrentDispatcher.InvokeShutdown();
}
}
}
using ArrangeActAssert;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using PrimerosPasosConMoq.Ejemplo1.Client;
using PrimerosPasosConMoq.Ejemplo1.Server;
namespace PrimerosPasosConMoqTests.Ejemplo1.Client.CalculadoraTests
{
[TestClass]
public class WhenCreatingCalculadora : UnitTestBase
{
protected Calculadora _objectoToTest;
protected Mock<IRestCalculatorService> RestCalculatorServiceMock;
protected Mock<ISumCalculatorService> SumCalculatorServiceMock;
protected override void Arrange()
{
base.Arrange();
RestCalculatorServiceMock = new Mock();
SumCalculatorServiceMock = new Mock();
}
// Act, como queremos que lance excepciones, lo voy a trasladar a cada método
protected override void Act(){}
}
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ItShouldThrowAnArgumentNullExceptionWhenSumCalculatorServiceIsNull()
{
_objectoToTest = new Calculadora(null, RestCalculatorServiceMock.Object);
}
public class Calculadora : ICalculator
{
public Calculadora (ISumCalculatorService sumService, IRestCalculatorService restService)
{
}
public int SumaDosNumeros(int num1, int num2)
{
throw new NotImplementedException();
}
public int RestaDosNumeros(int num1, int num2)
{
throw new NotImplementedException();
}
}
public Calculadora (ISumCalculatorService sumService, IRestCalculatorService restService)
{
if (sumService == null)
throw new ArgumentNullException("sumService");
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ItShouldThrowAnArgumentNullExceptionWhenRestCalculatorServiceIsNull()
{
_objectoToTest = new Calculadora(SumCalculatorServiceMock.Object,null);
}
De nuevo modifico el constructor para que pasen las pruebas y me queda de la siguiente forma:
public Calculadora (ISumCalculatorService sumService, IRestCalculatorService restService)
{
if (sumService == null)
throw new ArgumentNullException("sumService");
if (restService == null)
throw new ArgumentNullException("restService");
}
[TestMethod]
public void ItShouldImplementICalculator()
{
_objectoToTest = new Calculadora(SumCalculatorServiceMock.Object, RestCalculatorServiceMock.Object);
Assert.IsNotNull(_objectoToTest as ICalculator);
}
[TestMethod]
public void ItShouldReturnACalculatorThatImplementICalculatorWhenParamsAreOk()
{
_objectoToTest = new Calculadora(SumCalculatorServiceMock.Object, RestCalculatorServiceMock.Object);
// Es una calculadora, se ha creado correctamente
Assert.IsNotNull(_objectoToTest);
// y además implementa ICalculator
Assert.IsNotNull(_objectoToTest as ICalculator);
}
…sino que además los tests sirven de documentación sobre el código bajo pruebas, ¿no es genial? Dejo todo el código de la clase de pruebas con estos tres tests sobre el constructor:
using System;
using ArrangeActAssert;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using PrimerosPasosConMoq.Ejemplo1.Client;
using PrimerosPasosConMoq.Ejemplo1.Server;
namespace PrimerosPasosConMoqTests.Ejemplo1.Client.CalculadoraTests
{
[TestClass]
public class WhenCreatingCalculadora : UnitTestBase
{
protected Calculadora _objectoToTest;
protected Moc<IRestCalculatorService> RestCalculatorServiceMock;
protected Mock<ISumCalculatorService> SumCalculatorServiceMock;
protected override void Arrange()
{
base.Arrange();
RestCalculatorServiceMock = new Mock<IRestCalculatorService>();
SumCalculatorServiceMock = new Mock<ISumCalculatorService>();
}
// Act, como queremos que lance excepciones, lo voy a trasladar a cada método
protected override void Act() { }
[TestMethod]
public void ItShouldReturnACalculatorThatImplementICalculatorWhenParamsAreOk()
{
_objectoToTest = new Calculadora(SumCalculatorServiceMock.Object, RestCalculatorServiceMock.Object);
// Es una calculadora, se ha creado correctamente
Assert.IsNotNull(_objectoToTest);
// y además implementa ICalculator
Assert.IsNotNull(_objectoToTest as ICalculator);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ItShouldThrowAnArgumentNullExceptionWhenSumCalculatorServiceIsNull()
{
_objectoToTest = new Calculadora(null, RestCalculatorServiceMock.Object);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ItShouldThrowAnArgumentNullExceptionWhenRestCalculatorServiceIsNull()
{
_objectoToTest = new Calculadora(SumCalculatorServiceMock.Object, null);
}
}
}
[TestClass]
public abstract class WhenUsingTheCalculator : UnitTestBase
{
protected Calculadora _objectToTest;
protected Mock RestCalculatorServiceMock;
protected Mock SumCalculatorServiceMock;
protected override void Arrange()
{
base.Arrange();
RestCalculatorServiceMock = new Mock();
SumCalculatorServiceMock = new Mock();
// Esto ya lo hemos probado y sabemos que funciona:
_objectToTest = new Calculadora(SumCalculatorServiceMock.Object, RestCalculatorServiceMock.Object);
}
}
[TestClass]
public abstract class WhenAddingTwoNumbers : WhenUsingTheCalculator
{
protected abstract int X { get; }
protected abstract int Y { get; }
protected int result;
protected override void Arrange()
{
base.Arrange();
// Cuando al servicio 'falso' le pase dos números
// Que devuelva la suma de ambos
SumCalculatorServiceMock.Setup(x => x.SumaDosNumeros(X, Y)).Returns(X + Y);
}
protected override void Act()
{
result = _objectToTest.SumaDosNumeros(X, Y);
}
}
Veamos que sucede cuando sumemos dos números posirtivos:
WhenAddingTwoPositiveNumbers : WhenAddingTwoNumbers
[TestClass]
public class WhenAddingTwoPositiveNumbers : WhenAddingTwoNumbers
{
protected override int X { get { return 13; } }
protected override int Y { get { return 45; } }
[TestMethod]
public void ItShouldReturnTheCorrectResult()
{
Assert.AreEqual(58, result);
}
}
public class Calculadora : ICalculator
{
private readonly ISumCalculatorService _sumService;
public Calculadora (ISumCalculatorService sumService, IRestCalculatorService restService)
{
if (sumService == null)
throw new ArgumentNullException("sumService");
if (restService == null)
throw new ArgumentNullException("restService");
_sumService = sumService;
}
public int SumaDosNumeros(int num1, int num2)
{
return _sumService.SumaDosNumeros(num1, num2);
}
public int RestaDosNumeros(int num1, int num2)
{
throw new NotImplementedException();
}
}
Hagamos lo mismo pero para restar, crearé una clase base para la resta y una clase concreta con ciertos valores para restar en la que estableceré el comportamiento esperado de nuestro servicio falseado, en éste caso el de la resta:
[TestClass]
public abstract class WhensubtractingTwoNumbers : WhenUsingTheCalculator
{
protected abstract int X { get; }
protected abstract int Y { get; }
protected int result;
protected override void Arrange()
{
base.Arrange();
// Cuando al servicio 'falso' le pase dos números
// Que devuelva la resta de ambos
RestCalculatorServiceMock.Setup(x => x.RestaDosNumeros(X, Y)).Returns(X - Y);
}
protected override void Act()
{
result = _objectToTest.RestaDosNumeros(X, Y);
}
}
[TestClass]
public class WhenSubtractingTwoPositiveNumbers : WhensubtractingTwoNumbers
{
protected override int X { get { return 13; } }
protected override int Y { get { return 45; } }
[TestMethod]
public void ItShouldReturnTheCorrectResult()
{
Assert.AreEqual(-32, result);
}
}
public class Calculadora : ICalculator
{
private readonly ISumCalculatorService _sumService;
private readonly IRestCalculatorService _subsService;
public Calculadora (ISumCalculatorService sumService, IRestCalculatorService restService)
{
if (sumService == null)
throw new ArgumentNullException("sumService");
if (restService == null)
throw new ArgumentNullException("restService");
_sumService = sumService;
_subsService = restService;
}
public int SumaDosNumeros(int num1, int num2)
{
return _sumService.SumaDosNumeros(num1, num2);
}
public int RestaDosNumeros(int num1, int num2)
{
return _subsService.RestaDosNumeros(num1, num2);
}
}