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