No último capítulo implementei a minhas controller, mas parei antes de escrever o seu teste. Fiz isto porque queria discutir um pouco mais as alternativas possíveis. Relembrando a situação, tanto o monitor de diretórios, quanto o executor de tarefas, já está sendo testado. Portanto, o que preciso testar na controller é apenas se ela está instanciando e configurando corretamente os objetos ao iniciar.
A melhor forma de testar isto, em um ambiente controlado, é injetando mocks das classes utilizadas pela controller, para depois validar se os métodos foram chamados adequadamente. A utilização de uma ferramenta de DI me facilita nesta tarefa, porque fica mais fácil injetar as instâncias de mocks. Para fazer isto, da forma como a aplicação está, tenho que escolher entre algumas alternativas:
- Criar um arquivo de configuração específico para testes, no qual defino que os objetos a serem instanciados pelos Windsor container serão Mocks ou Stubs, ao invés das minhas classes concretas.
- Abrir a possibilidade de injetar externamente instâncias do monitor e do executor na controller.
- Delegar a função de instanciar a controller para o Windsor. Desta forma, a responsabilidade de instanciar e configurar o container sai da controller e vai para a próxima camada, neste caso, o Windows service.
- Utilizar o TypeMock para injetar os mocks na controller durante os testes.
Vamos estudar estas opções um pouco mais a fundo.
A opção 1 tem alguns problemas que me incomodam. Sempre gosto de manter o mínimo de código possível para manutenção e, para mim, arquivos de configuração também são código, pois precisam de manutenção e podem gerar bugs. Além disso, esta solução exige a utilização de mocks ou stubs passivos. Ou seja, preciso escrever estes mocks, um a um, o que também resulta em mais código-fonte para manutenção. Eu prefiro utilizar uma ferramenta para gerar mocks dinâmicos, que exigem muito menos manutenção.
A opção 2 é uma alternativa interessante, posso criar um construtor opcional, que recebe as instâncias, ou então permitir a injeção destas instâncias via propriedades. O lado negativo é que estou aumentando a complexidade da solução com o único motivo de facilitar os testes.
Já a opção 3 é parecida com a anterior, mas um pouco mais profunda. Ao invés de criar pontos de injeção opcionais, eu torno estas injeções obrigatórias e delego para o Windsor a responsabilidade de instanciar a controller, e todas suas dependências.
A última opção é menos intrusiva, já que o TypeMock consegue injetar mocks em qualquer classe, mesmo que ela não ofereça pontos de injeção. O problema é que o TypeMock não combina muito com a utilização de containers de injeção de dependência. Mockar o container não parece ser a melhor opção. Em minha opinião, estas estratégias são mutuamente exclusivas. Ou utilizo o Windor para injetar as dependências e tornar a solução menos acoplada. Ou então mantenho as dependências e construções dos objetos internos a controller e utilizo o TypeMock para injetar os mocks.
Analisando os prós e contras, vou optar pela terceira opção, que se encaixa melhor na minha solução e me ajuda a atender meu requisito de desacoplamento entre os componentes. O único ponto negativo é deixar para o Windows service a responsabilidade de instanciar e configurar o container.
Então vamos deixar de papo e partir para a solução. O primeiro passo é refactorar a controller, tirando todo o código relacionado ao Windsor e alterando o construtor para receber instâncias do monitor e do executor. O código final fica assim:
public class WinSrvController
{
private readonly IBackgroundRunner backgroundRunner;
private readonly IFolderWatcher folderWatcher;
public WinSrvController(IBackgroundRunner backgroundRunner, IFolderWatcher folderWatcher)
{
this.backgroundRunner = backgroundRunner;
this.folderWatcher = folderWatcher;
}
public void Start()
{
backgroundRunner.Start();
folderWatcher.WatchFolder();
}
public void Stop()
{
backgroundRunner.Stop();
folderWatcher.StopWatching();
}
}
Os testes são implementados utilizando o Rhino Mocks para gerar os mocks. O Rhino funciona da seguinte forma: Ele cria mocks dinâmicos e fica em estado de gravação, decorando o comportamento esperado à medida que você vai chamando os métodos. Quando o método ReplayAll é chamado o Rhino passa para o estado de verificação, quando começa a validar se as chamadas esperadas estão sendo feitas corretamente. No final, ao executar o método VerifyAll, o Rhino compara esperado com realizado e dá seu veredicto.
[TestFixture]
public class WinSrvControllerTest
{
[Test]
public void Start_Test()
{
MockRepository mockery = new MockRepository();
//Cria os mocks
IFolderWatcher folderWatcherMock = mockery.CreateMock<IFolderWatcher>();
IBackgroundRunner backgroundRunnerMock = mockery.CreateMock<IBackgroundRunner>();
//Define o comportamento esperado
using(mockery.Record())
{
folderWatcherMock.WatchFolder();
backgroundRunnerMock.Start();
}
//Executa o método testado e verifica os mocks
using (mockery.Playback())
{
WinSrvController controller = new WinSrvController(backgroundRunnerMock, folderWatcherMock);
controller.Start();
}
mockery.VerifyAll();
}
}
Agora sim estamos prontos para o último capítulo da série, quando vamos implementar o Windows service e juntar todas as peças para a solução.
Fiz questão de separar este capítulo para mostrar um ponto importante. O design de software é a arte de entender as opções, analisar custos e benefícios e escolher a que atende melhor os requisitos funcionais e não-funcionais. Algumas destas decisões são simples, outras nem tanto. O importante é não aceitar a primeira opção disponível, estudar alternativas e tomar decisões levando em consideração o contexto da solução.
OBS: Neste artigo estou apenas mostrando COMO testar cada componente e apresentando um teste de exemplo, para garantir uma cobertura razoável do seu código é necessário cobrir vários outros cenários.
OBS 2: O pessoal da Elutian elaborou uma solução interessante, que eles chamaram de AutoMockingContainer, que facilita bastante a injeção de mocks.