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