lunedì 10 settembre 2012

Perchè il test dovrebbe verificare i comportamenti?

Quando un oggetto interagisce con altri oggetti, il test dovrebbe verificare il comportamento / l'interazione tra gli oggetti. Per seguire questo consiglio bisogna rispettare le seguenti regole:
  • le dipendenze dell'oggetto da testare devono essere iniettate;
  • l'oggetto deve dipendere da interfacce e non da altri oggetti;
  • suddividere le responsabilità;
  • utilizzare un framework di mocking;
Facciamo qualche esempio:

public class Foo
{
   private readonly IRepository _repository;
   private readonly IOutput _output;

   public Foo(IRepository repository, IOutput output)
   {
      _repository = repository;
      _output = output;
   }

   public void DoSomething(int value)
   {
      IEnumerable<Person> items = _repository.Load(value);
      foreach (Person item in items)
      {
         if (item.Age < 10)
            continue;
         _output.Display(item.Age);
      }
   }
}

Prima di scrivere i test per verificare i comportamenti, impostiamo il setup:

[SetUp]
public void Setup()
{
   _repositoryMock = new Mock<IRepository>();
   _outputMock = new Mock<IOutput>();
   _sut = new Foo(_repositoryMock.Object, _outputMock.Object);
}

Scriviamo un test per verificare di chiamare il metodo load.

[Test]
public void When_call_DoSomething_should_load_list_of_persons()
{
   _repositoryMock.Setup(m => m.Load(It.IsAny<int>()))
      .Returns(new Person[0]);
   _sut.DoSomething(1);
   _repositoryMock.Verify(m => m.Load(It.Is<int>(p=>p == 1)));
}

Scriviamo un test per verificare che viene chiamato il metodo Display perchè Load restituisce un item con la property Age maggiore di 9:

[Test]
public void When_age_is_greater_than_nine_years_should_create_output()
{
   _repositoryMock
      .Setup(m => m.Load( It.IsAny<int>() ))
      .Returns(new[] { new Person { Age = 10 } });
   _sut.DoSomething( It.IsAny<int>() );
   _outputMock.Verify(m => m.Display( It.Is<int>(p=>p == 10)));
}

Scriviamo un test per verificare che non viene chiamato il metodo Display perché Load restituisce un solo item con la property Age minore di 10:

[Test]
public void When_age_is_less_than_ten_years_should_not_create_output()
{
   _repositoryMock
      .Setup(m => m.Load(It.IsAny<int>()))
      .Returns(new[] {new Person {Age = 9}});
   _outputMock
      .Setup(m => m.Display(It.IsAny<int>()))
      .Throws(new AssertionException("Cannot call Display"));
   _sut.DoSomething(It.IsAny<int>());
}

Rispondiamo alla domanda con delle considerazioni. Se scriviamo un test pensando ai comportamenti che dobbiamo verificare, risulterà piú naturale scrivere le interfacce che esprimono quei comportamenti. Le responsabilità saranno suddivise senza necessariamente scrivere le classi che implementano le interfacce. Il codice risulterà più semplice e leggibile (sia per la classe che per il test).