Testando código legado: o adendo!

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<Despesa> 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<Despesa>()
  {
    new Despesa(Despesa.TipoDeDespesa.Almoco, 100),
    new Despesa(Despesa.TipoDeDespesa.Jantar, 200),
    new Despesa(Despesa.TipoDeDespesa.PassagensAereas, 500)
  };
  var impressoraMock = new Mock<Impressora>();
  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<Despesa> 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.

CONCLUINDO

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.

Era isso! Comentem aí!

Anúncios

4 comentários em “Testando código legado: o adendo!

  1. Ó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?

    1. 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

Participe! Vamos trocar uma ideia sobre desenvolvimento de software!

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s