Aprendi Orientação a Objetos em Java.
Inicialmente me foi ensinado a utilizar o construtor para inicializar
todas dependências que o objeto necessita para funcionar.
Sendo assim, o código da classe
Sorteador é perfeitamente válido:
package di;
public class Sorteador {
private long limite;
public Sorteador(long limite) {
this.limite = limite;
}
public long sortear(){
double numero = Math.random()*this.limite;
return Math.round(numero);
}
}
Contudo, o problema dessa abordagem
consiste na dificuldade de testar o método sortear. Como ele depende
de um número aleatório, o máximo que se consegue é verificar se o
sorteio se encontra dentro dos limites esperados, conforme código
SorteadorTest:
package di;
import static org.junit.Assert.*;
import org.junit.Test;
public class SorteadorTest {
@Test
public void testSortear() {
Sorteador sorteador=new Sorteador(10);
long sorteio=sorteador.sortear();
assertTrue(sorteio>=0);
assertTrue(sorteio<=10);
}
}
Para contornar o problema, poderíamos
componentizar com a finalidade de alterar o comportamento no momento
de teste. Com esse objetivo, criei a interface Randomizador:
package di2;
public interface Randomizador {
double random();
}
Para continuar executando o
comportamento original, criei a classe RandomizadorReal:
package di2;
public class RandomizadorReal implements Randomizador {
@Override
public double random() {
return Math.random();
}
}
Dessa maneira alterei o código do
Sorteador . Com essa nova abordagem é possível alterar o componente
de randomização utilizando o construtor sobrecarregado. Nessa caso,
a dependência do objeto é injetada de fora para dentro. Por isso
esse conceito é chamado de “Inversão de Controle” ou também
“Injeção de Dependência”:
package di2;
public class Sorteador {
private long limite;
private Randomizador randomizador;
public Sorteador(long limite, Randomizador randomizador) {
super();
this.limite = limite;
this.randomizador = randomizador;
}
public Sorteador(long limite) {
this(limite, new RandomizadorReal());
}
public long sortear(){
double numero = randomizador.random()*this.limite;
return Math.round(numero);
}
}
Com o código modificado é possível
escrever um teste com mais controle. Para isso, basta trocar o
componente RandomizadorReal pela classe RandomizadorMock, conforme
código a seguir:
package di2;
import static org.junit.Assert.*;
import org.junit.Test;
class RandomizadorMock implements Randomizador{
public double retorno=0;
@Override
public double random() {
return retorno;
}
}
public class SorteadorTest {
@Test
public void testSortear() {
RandomizadorMock mock=new RandomizadorMock();
Sorteador sorteador=new Sorteador(10, mock);
mock.retorno=0;
long sorteio=sorteador.sortear();
assertEquals(0, sorteio);
mock.retorno=0.5;
sorteio=sorteador.sortear();
assertEquals(5, sorteio);
mock.retorno=0.54;
sorteio=sorteador.sortear();
assertEquals(5, sorteio);
mock.retorno=0.56;
sorteio=sorteador.sortear();
assertEquals(6, sorteio);
}
}
Esses objetos que se utilizamos para
substituir componentes na hora de testes costumam ser chamados de
Stub ou Mock. Foi com esses conceitos em mente que migrei para a
linguagem Python.
Nela posso escrever classe similar a
primeira versão de Sorteador:
from random import random
class Sorteador():
def __init__(self, limite):
self.limite = limite
def sortear(self):
return round(self.limite * random())
Poderia empregar a mesma técnica para
componentizar o método sorteio. Contudo, Python tem uma
particularidade interessante: tudo, absolutamente tudo, é um objeto.
Inclusive as bibliotecas importadas em um módulo.
Para provar isso, é muito simples.
Basta chamar a função dir para inspecionar os elementos do módulo
di.py:
>>> import di
>>> [elemento for elemento in dir(di) if not elemento.startswith('__')]
['Sorteador', 'random']
Assim, é possível observar a função
random importada como sendo um de seus elementos. Ou seja,
bibliotecas importadas em um módulo funcionam como variáveis. Sendo
assim, em vez de injetar dependência, a biblioteca alvo pode ser
simplesmente alterada na hora do teste:
from unittest import TestCase
import di
class SorteadorTestes(TestCase):
def teste_sortear(self):
sorteador = di.Sorteador(10)
di.random = lambda: 0.5
sorteio = sorteador.sortear()
self.assertEqual(5, sorteio)
di.random = lambda: 0.54
sorteio = sorteador.sortear()
self.assertEqual(5, sorteio)
di.random = lambda: 0.56
sorteio = sorteador.sortear()
self.assertEqual(6, sorteio)
De início passei a utilizar essa
técnica mas achava que estava fazendo gambiarra. No entanto
descobri a biblioteca Mock do Python. Sua função patch serve
exatamente para isso, trocar uma biblioteca em tempo de teste por
outra. E ela ainda vai além, tomando o cuidado de retornar o
componente para seu objeto original após o teste, evitando assim
efeitos colaterais indesejados em outros testes.
Enfim, eu gostei muito dessa abordagem
distinta do Python. E você o que acha? É gambiarra ou apenas uma
forma prática de se fazer Injeção de Dependência?