Este post é um complemento ao post anterior “Testando código legado”. Portanto, recomendo a leitura daquele antes de seguir adiante (sugiro que o mantenha aberto).
Lido? Vamos, então, discutir melhor a solução apresentada.
REVISANDO A SOLUÇÃO
Primeiramente, vamos entender melhor o que estamos tentando testar na classe “RelatorioDeDespesas”, utilizada como exemplo.
Percebam que esta classe é (1) um serviço sem estado e (2) o método que estamos querendo testar não possui retorno. Dados esses 2 fatos, a única coisa a se testar é que “algo aconteceu como side-effect” ao chamar o método. Em outras palavras, devemos testar que “o relatório foi impresso com o conteúdo correto”.
A solução apresentada no artigo anterior cumpre, em partes, essa função: para o teste passar, o conteúdo deve ser o correto e o código deve ter chamado o método Imprimir() da classe “RelatorioDeDespesas”.
De fato, a solução cobre a maior parte do método testado – que corresponde à iteração na lista de despesas e montagem do conteúdo – e, portanto, já é útil para que possamos refatorar essa parte do código.
No entanto, nada garante que a classe-colaboradora “Impressora” realmente esteja sendo utilizada, uma vez que removemos o seu uso na sub-classe de teste. Isso significa que, se removermos, por ex, a linha “impressora.Imprimir(..)” do código (claramente, um bug), o teste continuará passando! (*)
(*) essa “técnica” de estragar o seu código de produção para validar a corretude do código de teste é chamada de “Devil’s Advocate” por Mark Seemann.
UMA NOVA SOLUÇÃO
Para garantirmos que “impressora.Imprimir(…)” foi chamado, precisamos de um teste de interação.
Vejamos como podemos modificar o design de classes para permitir esse tipo de teste (no passo-a-passo):
1 – As primeiras e inofensivas modificações:
public class RelatorioDeDespesas { private Impressora _impressora; public RelatorioDeDespesas() : this(new Impressora()) { } public RelatorioDeDespesas(Impressora impressora) { _impressora = impressora; } public void ImprimirRelatorio(IEnumerable despesas) { // início do método omitido Imprimir(conteudo.ToString()); } protected virtual void Imprimir(string conteudo) { _impressora.Imprimir(conteudo.ToString()); } }
Vejam que: (1) movemos a declaração do objeto “Impressora” para um field da classe (_impressora); (2) criamos um construtor que recebe um objeto “Impressora” como dependência (constructor injection) e (3) criamos um construtor sem parâmetros que chama o outro construtor passando uma instância de Impressora.
Embora (3) não seja obrigatório, adicionei-o como uma técnica para não quebrar os consumidores da classe. Se só fizéssemos (2), poderíamos ter o trabalho de alterar todos os lugares onde “RelatorioDeDespesas” fosse instanciado.
Rodando o teste, ele certamente continuará passando pois ainda continuamos usando nele a sub-classe de testes.
2 – Agora, extraímos uma interface para a classe Impressora e fazemos os ajustes nos construtores criados:
public interface Impressora { void Imprimir(string textoParaImpressao); } public class ImpressoraLaser : Impressora { public void Imprimir(string textoParaImpressao) { // omitido } } public class RelatorioDeDespesas { private Impressora _impressora; public RelatorioDeDespesas() : this(new ImpressoraLaser()) { } public RelatorioDeDespesas(Impressora impressora) { _impressora = impressora; } // restante da classe omitido }
OBS.: notem que fiz uma pequena alteração de nomenclatura, renomeando a classe “Impressora” para “ImpressoraLaser” e deixando a nova interface com o nome de “Impressora”.
OBS 2: nada impactando compilação nem o teste original.
3 – Agora podemos alterar o teste original para utilizar um mock:
[Fact] public void Deve_imprimir_relatorio_de_despesas_com_conteudo_desejado() { var despesas = new List() { new Despesa(Despesa.TipoDeDespesa.Almoco, 100), new Despesa(Despesa.TipoDeDespesa.Jantar, 200), new Despesa(Despesa.TipoDeDespesa.PassagensAereas, 500) }; var impressoraMock = new Mock(); var relatorio = new RelatorioDeDespesas(impressoraMock.Object); relatorio.ImprimirRelatorio(despesas); var conteudoEsperado = (new StringBuilder()) .AppendLine("Relatório de Despesas") .AppendLine($" Almoço - R$100,00") .AppendLine($" Jantar - R$200,00") .AppendLine($" Passagens aéreas - R$500,00") .AppendLine($"Despesas com alimentação: R$300,00") .AppendLine($"Total das despesas: R$800,00") .ToString(); impressoraMock.Verify(mock => mock.Imprimir(conteudoEsperado), Times.Once); }
Notem que agora o teste instancia um objeto “RelatorioDeDespesas”, usando o construtor com parâmetro para passar um mock de “Impressora”. (A sub-classe de testes “RelatorioDeDespesasTestavel” usada originalmente pode ser excluída.)
A última linha do teste foi alterada para fazer a asserção pelo mock, satisfazendo o propósito inicial de toda essa brincadeira!
4 – Com o teste pronto, podemos chamar “_impressora.Imprimir(…)” inline no método e excluir da classe o método “protected virtual void Imprimir(…)”:
public class RelatorioDeDespesas { // inicio omitido public void ImprimirRelatorio(IEnumerable despesas) { // início do método omitido // chamada trazida de volta para o lugar original _impressora.Imprimir(conteudo.ToString()); } }
Algumas considerações finais:
- A técnica mostrada foi bem rápida e simples de implementar.
- A classe ficou melhor testada.
- A classe ficou com 2 construtores públicos, o que pode causar confusão/mau-uso de um deles (deve-se ponderar isso).
- Esta foi UMA solução alternativa e não a única existente.
CONCLUSÃO
Este post apresentou uma forma alternativa e robusta de implementar o teste apresentado no post anterior.
Vale reforçar que, especialmente no legado:
- Deve-se fazer o menor refactoring possível para possibilitar a escrita dos testes e, só com os testes escritos, partir para o refactoring “de verdade”.
- Nem sempre o design da classe sob teste ficará “perfeito” (vale mais o teste escrito do que um ou outro “problema” no design.)
- A técnica escolhida para quebrar dependências vai depender muito do código da classe a testar, o impacto das mudanças, tempo disponível, etc.
Muito bom o artigo! Parabéns!
CurtirCurtir
Obrigado!
CurtirCurtir
Ótimo artigo Robson, parabéns!
Legal que você citou o Mark Seeman e Michael Feathers, que na minha opinião, são dois autores essenciais quando o assunto é teste unitário.
Outro que eu gosto bastante é o Rob Osherov, autor de “The Art of Unit Testing With Examples In C#”, conhece?
CurtirCurtir
Obrigado, Carlos!
Sim. Conheço. Li esse livro há muitos anos (a 1a ed ainda), quando estava começando com testes. É excelente (está na minha lista de livros indicados).
[]s
CurtirCurtir