quarta-feira, 3 de março de 2010

Objetos fake em diversas linguagens para divertir minha vó

Quase todas as linguagens que trabalhei possuem ferramentas para criar objetos fakes e assim auxiliar na construção de testes. Mas e se essas ferramentas não existissem? Estaríamos perdidos? Claro que não! Mocks e stubs podem ser criados de diversas formas diferentes nas diversas linguagens.

Esse é um ótimo assunto pra se discutir em um jantar de família. Quer coisa mais divertida que explicar pra sua avó como construir objetos fake em diversas linguagens? Por exemplo um simples stub que conta quantas vezes um método que notifica visualização de um filme em um serviço externo é chamado. Algo similar ao que o código abaixo faz. Diversão garantida!

servico.should_receive(:notificar_visualizacao!).with(filme).once

Em Ruby

Em ruby podemos simplesmente criar uma nova classe em tempo de execução ou adicionar um método a um objeto qualquer. É a maneira mais simples de garantir um sorrisão da sua vó tamanha facilidade. No caso do exemplo abaixo resolvi adicionar métodos a um objeto qualquer e ao final executo o teste utilizando o rspec.

servico_fake = Object.new
servico_fake.instance_eval do
def notificar_visualizacao!(filme)
@filme_visualizado = filme
@quantidade_visualizacoes = quantidade_visualizacoes + 1
end
def filme_visualizado
@filme_visualizado
end
def quantidade_visualizacoes
@quantidade or 0
end
end

filme = Filme.new
filme.servico = servico_fake
filme.visualizar!

servico_fake.filme.should be_equal(filme)
servico_fake.quantidade.should == 1

Em Python

Em Python é possivel alterar métodos de instancias em tempo de execução, mas não é possivel adicionar métodos a um objeto da classe object. Neste momento minha vozinha fica decepcionada. Para fazer algo semelhante ao que fizemos em ruby teríamos então que instanciar um objeto da classe original e só depois modificar o método. Ficaria mais ou menos assim:

filme_visualizado = None
quantidade_visualizacoes = 0

def notificar_visualizacao_fake(filme):
global filme_visualizado, filme_visualizado
filme_visualizado = filme
quantidade_visualizacoes += 1

servico_fake = ServicoExterno()
servico_fake.notificar_visualizacao = notificar_visualizacao_fake

Esta opção passa a ser ruim quando o construtor da classe original executa algumas tarefas que são custosas para o nosso teste. Por isso, acho que em python o melhor para o este caso é criar uma classe em tempo de execução, deixando minha vó um pouco mais alegre, conforme exemplo abaixo:

class ServicoFake(object):
def __init__(self):
self.filme_visualizado = None
self.quantidade_visualizacoes = 0
def notificar_visualizacao(self,filme):
self.filme_visualizado = filme
self.quantidade_visualizacoes += 1

servico_fake = ServicoFake()

filme = Filme()
filme.servico = servico_fake
filme.visualizar()

assert servico_fake.filme_visualizado is filme
assert servico_fake.quantidade_visualizacoes == 1

Em Java

Uma forma "simples" de fazer em Java é criando uma nova classe para herdar a original e sobrescrever somente o método desejado. Mas aí cairíamos no mesmo problema que discutimos sobre um possível construtor com código muito custoso na superclasse. E minha vó, que apesar de repetir muitas vezes as mesmas histórias, não gosta de ouvir as nossas repetidas.

Uma alternativa que temos é extrair uma interface da classe original para que em nosso teste a gente possa implementar essa interface da maneira que quisermos. Ficaria mais ou menos assim:

//ServicoExterno.java
public interface ServicoExterno {
void notificarVisualizacao(Filme filme);
}

//ServicoExternoHTTP.java
public class ServicoExternoHTTP implements ServicoExterno {
public ServicoExternoHTTP() {
//Faz um monte de coisas
}
public void notificarVisualizacao(Filme filme) {
//Faz mais coisas ainda
}
}

Então em nosso teste a gente cria uma classe implementando a interface recém criada:

public class ServicoExternoFake implements ServicoExterno {
public int quantidade_visualizacoes = 0;
public Filme filme_visualizado = null;

public void notificarVisualizacao(Filme filme) {
filme_visualizado = null;
quantidade_visualizacoes++;
}
}

E depois, mesmo que neste ponto minha vó já esteje dormindo, a gente utiliza a classe fake criada no teste:

ServicoExternoFake servicoFake = new ServicoExternoFake();

Filme filme = new Filme();
filme.setServico(servicoFake);
filme.visualizar();

assertSame(filme,servicoFake.filme_visualizado);
assertEquals(1,servicoFake.quantidade_visualizacoes);

Parece que minha avó não gostou da história. Ela começou tão dinâmica e foi ficando cadas vez mais devagar. Sem dúvida eu deveria ter contato ao contrário.