No nosso último post sobre testabilidade mostramos como podemos usar fakes (um tipo de test double) para podermos "fingir" que um componente se comporta da forma esperada pelos nossos testes. Reforço, mais uma vez, que os test doubles são componentes que pertencem à nossa base de código de testes, e não à base de código de produção. No exemplo do post passado, criamos uma classe fake chamada FakeLogFile, cuja implementação simplesmente simulava que o componente estava realmente consumindo espaço de armazenamento em bytes de forma que a nossa classe sendo testada acreditasse que este era o cenário em andamento. Dessa forma, então, ela se comportaria da maneira adequada para esta situação. No caso do nosso exemplo, o comportamento esperado era que a classe Logger parasse de adicionar entradas ao log caso o arquivo de log por ela gerado chegasse a um limite específico de tamanho em bytes. Só para relembrar, aqui está o método de teste que verificava isso:
[Test]
public void should_stop_logging_when_space_consumption_hits_threshold()
{
_logger.SetLogFile(new FakeLogFile());
_logger.MaxLogSizeInBytes = 1000;
while (_logger.SizeInBytes < _logger.MaxLogSizeInBytes)
{
_logger.Info("Dumb Message");
}
long logLinesWhenStopped = _logger.LogLines;
_logger.Info("This message should not be logged");
Assert.That(_logger.LogLines, Is.EqualTo(logLinesWhenStopped));
}
A técnica de criação de stubs (e de test doubles em geral) é bastante válida em diversas ocasiões e deve sempre ser considerada no nosso dia-a-dia quando estamos desenvolvendo testes unitários. Mas atualmente temos recursos adicionais que nos ajudam a criar esse tipo de componente de forma muito mais rápida e produtiva. Esses recursos são chamados frameworks de mock, ou, usando o termo em inglês: Mock Frameworks. Um desses frameworks que vem percebendo grande atenção da comunidade .NET é o Rhino Mocks; e esse é o framework que usarei neste post para ilustrar como podemos escrever um teste unitário usando test doubles criados automaticamente pelo mock framework.
Vamos reescrever o método acima, mas desta vez usando um stub (também um test double) criado pelo Rhino Mocks:
[Test]
public void should_stop_logging_when_space_consumption_hits_threshold()
{
MockRepository mockRepository = new MockRepository();
var mockedLogFile = mockRepository.Stub<ILogFile>();
using (mockRepository.Record())
{
SetupResult
.For(mockedLogFile.LogFileSize)
.Return(20);
}
_logger.SetLogFile(mockedLogFile);
_logger.MaxLogSizeInBytes = 10;
_logger.Info("This message should not be logged");
Assert.That(_logger.LogLines, Is.EqualTo(0));
}
Veja que usamos a classe MockRepository do Rhino Mocks para nos prover uma implementação de ILogFile que sirva ao nosso propósito de teste. Como felizmente temos uma interface que determina o contrato de um arquivo de log, podemos ter várias implementações de ILogFile (como a implementação FakeLogFile que criamos no post anterior). Sendo assim, o Rhino Mocks cria dinamicamente uma classe que implementa a interface e a instancia para usarmos no teste. Veja que realmente não estamos interessados na classe que o framework cria para nós. Ela servirá somente para o teste em questão e assumirá um comportamento que nos é conveniente para este teste e só para ele.
Em seguida, chamamos o método Record() do repositório dentro de um bloco "using". Neste bloco podemos configurar o comportamento que desejamos para cada um dos métodos/propriedades do objeto stub. No caso, temos só uma configuração estabelecendo que toda vez que a propriedade (get) LogFileSize for chamada, ela retornará o valor 20. Neste caso, o próprio Rhino Mocks cuidará dos detalhes e se encarregará de retornar o valor que especificamos.
Finalmente, passamos a instância de ILogFile retornada pelo Rhino Mocks para o nosso objeto _logger, por injeção, através do método SetLogFIle() e passamos a usar normalmente a nossa classe sendo testada esperando que ela, ao interrogar a implementação de ILogFile, receba como retorno o valor 20.
Como configuramos MaxLogSizeInBytes para 10, a próxima chamada ao método Info() não deveria gerar uma nova entrada no log; e é isso que estamos assegurando na última linha do método de teste.
Pensar em desacoplamento entre classes, testabilidade e designs mais flexíveis e manuteníveis são tarefas diárias que nós, desenvolvedores, devemos incorporar ao nosso mindset e ao conjunto de best practices que regem o nosso processo de design e engenharia. O princípio de Dependency Inversion, quando realmente absorvido, fará com que, cada vez mais, tenhamos classes mais independentes de implementações concretas e mais dependentes de contratos (interfaces). Test Doubles são componentes fundamentais para que tenhamos nossos testes realmente focados na classe sendo testada e para que esses testes sejam executados de forma rápida e sem redundâncias. Obviamente, frameworks de mock como o mostrado nesse post podem agilizar e muito a tarefa de criar e usar esses componentes de teste.
Happy Testing!
