Aproveite o mês das
carreiras na Alura

Até 44% OFF

Falta pouco!

00

DIAS

00

HORAS

00

MIN

00

SEG

Herança e testes de unidade

Alura
lacerdaph
lacerdaph

Compartilhe

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; } } 
Banner da Imersão de IA da Alura com Google Gemini. Participe de aulas gratuitas online com certificado. Domine as inovações mais recentes da IA.

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á!!!!

Veja outros artigos sobre Programação