Testando código legado

Working Effectively with Legacy CodeQuando o assunto é unit testing em software legado, logo alguém menciona o famoso livro “Working Effectively with Legacy Code” (ao lado), que, sem sombra de dúvidas, é a “bíblia” para o referido tema.

Neste post, farei uma introdução ao assunto e mostrarei uma estratégia muito útil para testar código legado (uma entre dezenas catalogadas no livro).

A DIFICULDADE DE SE TESTAR O LEGADO

Acho que nem preciso dizer, né? Muitos de vocês estão ou já estiveram trabalhando com software legado e, na imensa maioria dos casos, a situação do código é de ruim para péssima (ou de péssima para me-tirem-daqui-pelo-amor-de-deus).

No legado, prevalece a “arquitetura” Big Ball of Mud ou, na melhor das hipóteses, uma tentativa de implementação de uma arquitetura em 3 camadas.

Por consequência desse descuido com a separação de responsabilidades do software, o código acaba sendo “macarrônico”, com métodos e classes gigantes, realizando todo o tipo de tarefa, desde validações da UI, passando pelas regras de negócio e chegando à persistência.

O cenário acima é fruto, dentre vários fatores, de código projetado sem testabilidade em mente. (Lembrando que “testabilidade” é a capacidade do código de ser testado por meio de testes de unidade.)

Por isso tudo, retroalimentar a base de código legado com testes de unidade é o pior caso possível para a introdução de testes.

POR QUE TESTAR O LEGADO?

Sabendo que código legado não é testável e sim detestável, por que nos preocupar em testá-lo?

Resposta rápida: gestão do cagaço.

Quem nunca se preocupou ou resmungou por ter de dar manutenção naquela classe medonha dentro do código? O risco de alterar o código é muito grande pois, muitas vezes, não se sabe se a alteração realizada quebrará alguma outra parte do software!

Além disso, o medo e a insegurança acabam inibindo o programador de praticar refactoring. Ele só irá mexer em algo quando realmente for necessário para o cliente (solicitações novas ou correção de bugs) e ainda assim seguirá o “padrão” do código atual, fazendo o mínimo de alterações possíveis e, até mesmo, seguindo prováveis gambiarras existentes.

Em resumo, o código ruim, difícil de entender e dar manutenção, continuará apodrecendo e, os custos com manutenção, altos.

Se houvesse um meio mais seguro de alterar o código, faríamos isso com menos risco e com menos medo, podendo, inclusive, praticar refactoring, melhorando o código aos poucos, tornando-o mais limpo e coeso.

A parte boa é que esse meio existe: cobrir o código com testes! Desta forma, temos o feedback imediato sobre as alterações realizadas.

COMO TESTAR O LEGADO?

Michael Feathers escreveu um livro inteiro (acima), mostrando técnicas para realizar esta árdua tarefa.

Antes de por a “mão na massa”, é preciso ter ciência do que Feathers batizou de “Characterization Tests”. Feathers define um “characterization test” como “um teste que caracteriza o comportamento ATUAL de um pedaço de código”.

Em outras palavras, devemos testar o que o código faz hoje e não o que gostaríamos que ele fizesse. Sem isso em mente, tentaríamos, ao escrever o teste, corrigir algum bug ou fazer alguma alteração drástica no design, perdendo o foco do objetivo principal.

It’s all about dependencies, baby

Saber gerenciar as dependências que seu código possui em relação a outro código (por ex, outra classe, em OO) é uma tarefa fundamental durante a construção do software.

Como dito anteriormente, código legado normalmente não é escrito pensando-se em testabilidade. Isso significa que o código é altamente acoplado com dependências de baixo nível (classes que fazem I/O, classes que precisam de um contexto em runtime para executarem corretamente, etc.).

Vejamos um exemplo:

public class RelatorioDeDespesas
{
  public void ImprimirRelatorio(IEnumerable<Despesa> despesas)
  {
    var totalAlimentacao = 0.0;
    var total = 0.0;
    var conteudo = new StringBuilder();
    conteudo.AppendLine("Relatório de Despesas");

    // iterar "despesas" para montar "conteudo" com as informacoes
    // das despesas
    // ...

    conteudo.AppendLine($"Despesas com alimentação: {totalAlimentacao:C}");
    conteudo.AppendLine($"Total das despesas: {total:C}");

    var impressora = new Impressora();
    impressora.Imprimir(conteudo.ToString());
  }
}

A parte que nos interessa neste artigo é aquela onde o código cria e usa um objeto “Impressora”. Estamos assumindo aqui que o método “Imprimir” realmente acessa uma impressora para imprimir o conteúdo montado. Sendo assim, o método do exemplo (“ImprimirRelatorio”) NÃO é testável.

Para quebrar essa dependência de baixo nível, é fundamental pensarmos no conceito de “seam”, definido por Michael Feathers como “… a place where you can alter behaviour in your program without editing in that place.”

No legado, é preciso, em muitos casos, criar esses “seams” aplicando algumas técnicas de refactoring, como a que veremos a seguir, chamada de Extract and Override:

public class RelatorioDeDespesas
{
  public void ImprimirRelatorio(IEnumerable<Despesa> despesas)
  {
    var totalAlimentacao = 0.0;
    var total = 0.0;
    var conteudo = new StringBuilder();
    conteudo.AppendLine("Relatório de Despesas");

    // iterar "despesas" para montar "conteudo" com as informacoes
    // das despesas
    // ...

    conteudo.AppendLine($"Despesas com alimentação: {totalAlimentacao:C}");
    conteudo.AppendLine($"Total das despesas: {total:C}");

    Imprimir(conteudo.ToString());
  }

  protected virtual void Imprimir(string conteudo)
  {
    var impressora = new Impressora();
    impressora.Imprimir(conteudo);
  }
}

Notem que a única modificação foi criamos um seam ao extrair a dependência para o método “Imprimir(string conteudo)” localizado ao final da classe.

A classe permanece não-testável, mas agora podemos substituir o comportamento indesejado (chamar a impressora) por um outro mais adequado para a realização do teste.

Fazemos isso, criando uma sub-classe de testes:

public class RelatorioDeDespesasTestavel : 
    RelatorioDeDespesas
{
  public string Conteudo { get; private set; }

  protected override void Imprimir(string conteudo)
  {
     Conteudo = conteudo; 
  }
}

Percebam que a classe acima herda de “RelatorioDeDespesas” e sobrescreve o método “Imprimir” (nosso seam), substituindo o uso da “Impressora” por uma simples atribuição, que será usada na asserção do teste.

Agora podemos escrever nosso teste de unidade usando a sub-classe como SUT ao invés da classe base:

[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)
  };
  // instanciamos no teste a sub-classe
  // ao invés da classe original
  var relatorio = new RelatorioDeDespesasTestavel();

  // exercitamos o método
  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();
  // a asserção usa a propriedade Conteudo da sub-classe
  // para verificar se "imprimiu" com o conteudo desejado
  Assert.Equal(conteudoEsperado, relatorio.Conteudo);
}

Prontinho. Com o teste escrito (e demais testes cobrindo cenários diferentes), é possível refatorar a classe RelatorioDeDespesas com maior segurança!

Algumas considerações:

  1. Para escrever a sub-classe, tivemos que criar um seam, escrevendo um método “protected virtual” na classe original. Não estamos dessa forma “sujando” a classe só por causa do teste e ainda quebrando o encapsulamento da mesma?
    R.: Sim, estamos. Para podermos testar o legado, em algumas situações, seremos obrigados a fazer esse tipo de coisa (e isso pode ser só um passo intermediário para um solução definitiva).
  2. A sub-classe de teste “RelatorioDeDespesasTestavel” é uma classe utilitária para os testes, ou seja, deve ficar no mesmo pacote/library dos testes e não junto do código de produção.
  3. Uma parte do conteúdo montado no teste é feito na iteração das despesas na classe “RelatorioDeDespesas”, omitido aqui para não fugirmos do foco do artigo.

OK, MAS E TAL DA INJEÇÃO DE DEPENDÊNCIA?

Você deve estar se perguntando: “Não seria melhor injetar a dependência Impressora e substituí-la no teste por um mock?”.

Utilizar as várias práticas de DI (Dependency Injection) trás diversos benefícios para o design/arquitetura do software e devem ser consideradas em primeiro lugar no seu código.

No entanto, no legado, pode ser muito trabalhoso fazer certas alterações nas classes (principalmente as que envolvam alterações nos construtores) e, por ex, colocar um container de DI em funcionamento.

Portanto, a decisão irá depender de diversos fatores como a experiência do time, o impacto das prováveis mudanças no código e o tempo disponível para realizar tais mudanças.

(Eu escrevi uma série só sobre Dependency Injection, que começa com <<este artigo>>.)

CONCLUINDO

Testar o legado não é uma tarefa das mais fáceis e poderá exigir o emprego de diversas técnicas de refactoring, como Extract and Override (vista neste artigo), com o objetivo principal de quebrar depêndencias, tornando o código testável.

Se você quiser se aprofundar no assunto, recomendo fortemente a leitura do livro “Working Effectively with Legacy Code”, citado anteriormente.

Gostou do artigo? Dúvidas? Fique à vontade para comentar!
[]s

Anúncios

10 comentários em “Testando código legado

  1. Excelente post.
    Eu li o livro do Martin Fowler sobre refatoração e ele ensina várias dessas técnica de refatoração para tornar o código mais claro e testável.E posso dizer que isso funciona MUITO bem, eu apliquei em um legado que era cheio de bug e isso diminuiu drasticamente a abertura de chamados, trazendo estabilidade para o produto.

  2. Sensacional! Eu já estava de olho nesse livro e agora só tive a certeza de comprá-lo.

    Não tenho domínio dessas técnicas, mas não seria mais simples criar um método GerarConteudo, deixá-lo como public e testá-lo?

    1. Oi, Kevin.
      Vale muito a pena ler o livro. Recomendo.

      Eu vejo um problema com essa sua abordagem. Seu teste, enquanto documentação da API, ficaria confuso, uma vez que o teste estaria exercitando algo como “relatorio.GerarConteudo()”, que NÃO é usado pelo codigo de produção, ao invés de documentar/testar o método “ImprimirRelatorio()”, que é o verdadeiro método usado.

      Sacou?

      No entanto, sua pergunta me alertou para um problema no exemplo (muito, muito obrigado!). Irei postar uma errata amanhã ok?
      []s

    2. Oi, Kevin
      Quanto à solução do artigo, aceite-a como “quase” ok por enquanto. Não há nenhuma bobagem ali não.
      Postarei outro artigo com uma segunda alternativa (ficaria muito grande se juntasse neste).
      []s

      1. Entendi perfeitamente Robson, obrigado pela explicação!

        Vou ficar esperando o novo post com certeza.

        P.S: Esse aqui já me ajudou a fazer a mesma coisa em um projeto legado 🙂

  3. Boiei no início, tive que ler umas 3 vezes até captar o alvo do teste mas acho que consegui entender. Eu tentei começar a ler esse livro mas como os exemplos eram em C++ deixei pra depois porque não ia absorver muita coisa. Técnica muito interessante, mas pelo que entendi o alvo do teste é o conteúdo e a totalização das despesas feita pelo método ImprimirRelatorio…então eu talvez tivesse chamado o teste de ‘Deve_totalizar_despesas_do_conteudo_e_imprimir_relatorio’ ou algo parecido.
    .
    Só que como esse nome é meio grande e sugere um método que faz mais de uma coisa (deve_fazer_algo_*e*_mais_algo) eu talvez tentasse escrever um teste pra totalização e outro pra interpolação de strings, o que já não seria possível porque as variáveis são locais e eu precisaria transformar em properties ou usar alguma outra estratégia que não consegui enxergar ainda.
    .
    Muita pira? 😛 (e acho que a partir de amanhã vou tentar aplicar seams em algumas coisinhas no trabalho hehe)

    1. Opa, Kenji!

      Então, sobre o livro, há muitos exemplo em C++ mesmo (e as soluções dadas levam em conta fatores específicos da linguagem). Por isso, esses exemplos não serão de muita utilidade para quem não trabalha com C++. Essas partes acabam sendo meio chatas, mas não acho que prejudique a leitura no geral. Existe muito conteúdo válido para linguagens como C#/Java (há vários exemplos em Java tb e alguns poucos em C#). Vale a pena!

      Quanto ao teste do artigo, o objetivo real é testar que o relatório é impresso com o conteúdo desejado.
      É claro que “conteúdo desejado” envolve várias coisas como totalizar (e no legado sempre poderá envolver mil coisas no mesmo método).
      O importante é cobrir com testes (lembre-se de “characterization tests” e DEPOIS partir para refatorações mais pesadas, como totalizar em outra classe, etc…

      Resolvi publicar em outro artigo o “problema” que eu mencionei contigo. Aceite o que foi mostrado como verdade (está tudo – quase – ok).
      []s

  4. Terminei de ler esse livro há poucos meses, e achei ele sensacional! Eu sempre fiquei tão focado em técnicas como TDD que foi como uma revelação divina o momento em que o Michael Feathers apresentou os caractherization tests. Também gosto muito como o livro lentamente te deixa mais confiante/corajoso para aplicar as refatorações que levam à quebra de dependências.

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