Herança e testes de unidade

Herança é um dos termos mais discutidos em orientação a objetos. Há uma discussão antiga sobre as vantagens e desvantagens com relação à Composição.  Em outro artigo, o Aniche trouxe o Príncipio da Substituição de Liskov. Além disso, uma outra discussão famosa e bem antiga é a possibilidade de DAOs genéricos. Por fim, também cito o uso da herança para lidar com chain of resposibility e o excelente Joshua Bloch.

Como podemos ver, a discussão é longa e o questionamento que eu trago é: como lidar com herança e testes de unidade?

Bom, para entender o post, além de Herança, você deve estar familiarizado com a importância de se fazer testes automatizados. Principalmente os testes de unidade, que é basicamente o foco do artigo. A ideia aqui é ver o quanto que o teste pode te ajudar a descobrir problemas de design no seu código.

No cenário proposto, se você tivesse controle sobre este código, provavelmente iria preferir utilizar a composição para resolver o problema, devido aos motivos já relatados no primeiro parágrafo. Todavia, vamos partir da premissa que seja necessário o uso de uma API de um terceiro e a herança é obrigatória. Como lidar com os testes? Vamos ao código!

//classe do Fabricante
abstract class AbstractService {
  public Service getService() {
    //faz algo que vc não tem controle consultar um WS, REST, BD,etc.
  }
}

class ServicoDeNotaFiscal extends AbstractService {
  private NotaFiscal notaFiscal = new NotaFiscal();
  
  public void run(){

    Service service = getService();

    if(service.isOperationInitialized()){

          notaFiscal.mudarParaEstado(Estado.CRIADA);          
    }else{

          notaFiscal.mudarParaEstado(Estado.SUSPENSA);
    } 
  }
 }


class ServicoDeNotaFiscalTest{
  private ServicoNotaFiscal servicoNotaFiscal;

  @Before
  public void init(){
    servicoNotaFiscal = new ServicoNotaFiscal();
  }

  @Test
  public void quandoOperacaoForIniciadaDeveColocarNotaFiscalComoCriada(){
     
    servicoNotaFiscal.run();
    assertEquals(Estado.CRIADA, servicoNotaFiscal.getNotaFiscal().getEstado());
  }
}

Isolando com objetos de mentira…

Obviamente há outras cenários de testes, mas o objetivo aqui não é explicar Coverage, vamos focar apenas na herança. Ao rodar os testes, você encontrará algum erro de comunicação relacionado à execução do método herdado getService. Não temos controle sobre ele, e como estamos fazendo Testes de Unidade, temos que arrumar alguma forma de isolá-lo. Ademais, precisamos definir algum comportamento para o método isOperationInitialized(). Afinal, se o retorno for true, nota fiscal deve ir para criada, caso contrário, suspensa.

Uma primeira solução seria transformar nossa objeto ServicoNotaFiscal no que algumas pessoas chamam de Stub. Nomenclaturas à parte, temos que definir um comportamento para o método getService.

class ServicoDeNotaFiscalTest{
  private ServicoNotaFiscalStub servicoNotaFiscal;

  @Before
  public void init(){
    servicoNotaFiscal = new ServicoNotaFiscalStub();
  }

}

class ServicoNotaFiscalStub extends ServicoNotaFiscal{
 public Service getService(){
   return new ServiceStub();
 } 
}
class ServiceStub extends Service{
  public boolean isOperationInitialized(){
    return true;
  }
} 

Frameworks podem ajudar…

Ao rodar os testes, os stubs serão executados ao invés da implementação real. O teste passa, porém é uma solução mais prolixa. Você poderia usar o conceito de SPY Object (já explicado anteriormente  –> “objeto real até que prove o contrário“) e um mock para Service.

 

When you’re doing testing like this, you’re focusing on one element of the software at a time -hence the common term unit testing. The problem is that to make a single unit work, you often need other units

 

class ServicoDeNotaFiscalTest{
  @Spy
  private ServicoNotaFiscal servicoNotaFiscal;
  @Mock
  private Service service;
   
  @Before
  public void init(){

    when(service.isOperationIniatilized()).thenReturn(true));
    doReturn(service).when(servicoNotaFiscal).getService();
  }
}

Só piorar um pouquinho…

Particularmente acho que o código fica mais limpo e fácil de entender. Contudo, você pode encontrar um cenário mais desafiador: se o método getService for final.

 

abstract class AbstractService{
  public final Service getService(){}
}

Agora a herança começa realmente a cobrar o seu acoplamento. Perceba que pelo método herdado ser final, não poderemos mais fazer os stubs. Além disso, o próprio spy do Mockito não funcionaria:

 

Watch out for final methods. Mockito doesn’t mock final methods so the bottom line is: when you spy on real objects + you try to stub a final method = trouble. Also you won’t be able to verify those method as well.

 

A solução aqui é quebrar um pouco o encapsulamento e extrair a chamada do getService para um método com visibilidade default. Depois, usaremos o Spy object para redefinir o comportamento deste novo método criado.

class ServicoDeNotaFiscal extends AbstractService {
  public void run(){
    Service service = buscarPorServico();
    //mais código aqui
  }

  Service buscarPorServico(){
    return getService();
  }
 }


class ServicoDeNotaFiscalTest{
  @Spy
  private ServicoNotaFiscal servicoNotaFiscal;
  @Mock
  private Service service;
   
  @Before
  public void init(){
    doReturn(service).when(servicoNotaFiscal).buscarPorServico();
  }
}

 

Concluindo, a atividade de automatizar os testes envolve uma série de desafios e herança é apenas um deles. No cenário citado, se ao invés de herdar de AbstractService ele recebesse como Injenção de Dependêcia um Service, seria muito mais fácil testar, inclusive mais fácil ver a sua consistência. Por isso que “há uma grande sinergia entre um bom design e testes automatizados” (Michael Feathers). Estes não vão fazer sua aplicação ter um bom Design da noite pro dia, entretanto a prática irá apontar os caminhos para melhorar.

 

E quer saber mais sobre Testes? Livro do Aniche é leitura obrigatória!  Corre lá!!!!

6 Comentários

  1. Clairton Rodrigo Heinzen 15/03/2016 at 08:02 #

    Muito bom Rafael, mas acho que um dos problemas do Java(ou do programador Java) é o uso em excesso do modificador “final”, e não da herança em si, inclusive, no caso de uso que usou para exemplificar.

  2. Rafael Ponte 16/03/2016 at 09:08 #

    Oi Raphael,

    Excelente post, não só pela simplicidade abordada no conteúdo, mas pela enorme quantidade de referências que você passa. Eu devo ter aberto umas 9 abas aqui 🙂

    Como você mesmo explicou, herança cedo ou tarde cobra seu uso. No caso do post, para quem escreve testes, o preço pode ser bem caro, principalmente se sua base de conhecimento de OO for pouca, pois tua solução tem muito a ver com design de classes.

    Eu já passei exatamente por este problema do post, e solução que usei foi *exatamente* a sua com @Spy e encapsulamento do método em outro para que eu pudesse mocká-lo.

    Enfim, se eu fosse simplificar seu post em um frase eu diria “nunca herde de classes de terceiros, prefira composição”.

    PS: adoro o vocabulário rebuscado que você usa nos posts, como palavras como “ademais”, “não obstante” e por aí :p

  3. Clairton Luz 17/03/2016 at 08:16 #

    Ótimo post Rafael, parabéns!

  4. Raphael Lacerda 21/03/2016 at 10:29 #

    Pois é Clairton! As vezes vc está usando a API de um terceiro e não tem como evitar isso!

    @Ponte! Ao fazer o post desconfiei que vc já teria passado pelo mesmo problema! hehehe!

    @Luz -> obrigado cara!

  5. Milico 22/03/2016 at 17:55 #

    Excelente post Raphael!

  6. Rodrigo Nery 06/07/2016 at 18:58 #

    Parabéns Lacerda!
    Texto muito bom, com observações importantes para o dia a dia.

Deixe uma resposta