Blog da Improve It

Como Testar parte 2 - Mocks

Publicado por Marcos Tapajós há aproximadamente 1 mês.

Tem gente "chutando" Demeter.

Minha idéia era escrever um post dessa série por semana, mas infelizmente uma tendinite tem me atacado e está meio complicado ficar escrevendo muito. Por isso mesmo esse segundo artigo será bem compacto. Todos os códigos citados nesse post fazem parte de um projeto How Test que está disponível em: http://github.com/tapajos/how-test

Nesse post a minha idéia é mostrar de forma bem simples como funciona um mock object no Rspec e no Test::Unit(usando o mocha).

Para exemplificar eu vou fazer uma crítica a uma construção que eu tenho visto muito em diversos projetos e que viola a Lei de Demeter(Principle of Least Knowledge). Ou como dizia, o meu amigo, Bernardo, "Tem gente chutando Demeter!".

Vamos a um exemplo dessa violação:

Supondo que você tenha um modelo Account que se relaciona ao modelo User.

class Account < ActiveRecord::Base
  belongs_to :user
end

Freqüentemente eu vejo construções do tipo:

@account.user.name
@account.user.mail
@account.user.rg.number
@account.user.rg.state
@account.user.rg.city

O grande problema é que nesse tipo de construção você está "conhecendo" coisas demais e certamente vai pagar por isso num futuro breve, quando você precisar fazer um refactoring e tiver que mudar em vários lugares. Imagina se o User deixa de ter um "name" e passa a ter um "full_name".

Para resolver esse tipo de problema basta você concentrar esse "conhecimento" no seu modelo Account da seguinte forma:

class Account < ActiveRecord::Base
  belongs_to :user
  def user_name
    user.name
  end
end

OBS: Vou me concentrar apenas no problema com o nome e não vou me preocupar em validar se o relacionamento foi estabelecido.

Bem, finalizada a crítica a uma falha de design OO vamos ao objetivo desse post, mostrar como *EU* testaria esse problema.

Como eu falei no post anterior, não gosto muito de usar fixtures para testes unitários e por isso mesmo vou apelar aos mock objects. Se você não está muito familiarizado com mocks sugiro que pare por aqui e leia um pouco mais sobre isso. Uma referência rápida pode ser o wikipedia mas realmente sugiro que vá mais adiante.

Testando usando RSpec(usando o framework de mock padrão):

before(:each) do
  @account = Account.new    
  @user_mock = mock_model(User)
  @account.stub!(:user).and_return(@user_mock)
end

describe ".user_name" do

  it "should delegate to user.name" do
    @user_mock.should_receive(:name).and_return("Tapajós")
    @account.user_name.should == "Tapajós"
  end

end

Testando usando Test::Unit com mocha:

setup :create_model

def test_user_name
  @user_mock.expects(:name).returns("Tapajós")
  assert_equal "Tapajós", @account.user_name
end

private

  def create_model
    @account = Account.new
    @user_mock = mock("User")
    @account.stubs(:user).returns(@user_mock)
  end

Explicando...

Em ambos os casos eu preciso que a minha account simule o relacionamento com User e para isso eu vou retornar um mock object. Isso é feito pelas linhas:

RSpec:

@account.stub!(:user).and_return(@user_mock)

Test::Unit:

@account.stubs(:user).returns(@user_mock)

Após o setup ou o before, temos um modelo @account onde o @account.user retorna um mock.

Feito isso eu preciso configurar o meu mock, isto é, informar que ele receberá uma mensagem name (chamada do método .name) e essa retornará o meu nome(sim, sou egocêntrico). Isso é feito pelos métodos "should_receive" e "expects" conforme as linhas abaixo:

RSpec:

@user_mock.should_receive(:name).and_return("Tapajós")

Test::Unit:

@user_mock.expects(:name).returns("Tapajós")

Depois que nossos mocks foram devidamente configurados podemos, finalmente, fazer nossa verificação do retorno, isto é, simplesmente chamar nossos métodos conferir o retorno. Pronto teste feito com sucesso, sem precisar recorrer a banco de dados nem configurar fixtures.

Nesse momento deve ter surgido uma dúvida: "Porque uma hora você usa stub! e outra um should_receive?"

A resposta é bem simples, o stub! não faz um verify no final enquanto a outra chamada sim. Na pratica isso significa:

Stub! ou stubs

Quando eu uso stub!(ou um Stubs) eu estou configurando o meu modelo account para responder pelo método user retornando o @mock_user porém não me interessa quebrar esse teste caso você não chame esse método.

Should_receive ou expects

Quando eu uso should_receive(ou um expects) eu estou configurando o meu mock user para responder pelo método name porém caso esse método não seja chamado eu devo quebrar meu teste, pois isso seria um comportamento indesejável.

Porque usar mocks?

Ao contrário do que muita gente pensa o uso de mocks não é um bicho de 7 cabeças, é bem simples. Na verdade testar é algo simples, desde que você tenha domínio do ferramental e os mocks são realmente úteis em diversos casos.

Imagina que o seu sistema precise fazer consultas a uma api publica do Yahoo e para isso você tenha criado uma classe de consultas. Você não vai querer(nem o Yahoo vai gostar) ir lá no servidor toda vez que você rodar os seus testes. Isso tornaria os seus testes lentos e impossível roda-los offline. Nesse caso você resolve seu problema "mockando" essa classe.

Tags , , , , , ,  | 7 comentários

O que você achou? Coloque seus comentários e sugestões abaixo!

Acompanhe o RSS dessa página.

Comentários (7 até o momento)

  1. Levy disse 8 dias depois:

    Olá Tapajós, excelente série sobre o tema!

    Sobre a explicação acima sobre o método "stub!", acho que fica mais claro dizer que:

    Ao usar:

    
    @account.stub!(:user).and_return(@user_mock)
    

    ... estamos simplesmente programando a instância @account para ter uma resposta "enlatada". Ou seja, ao chamar @account.user ela sempre vai responder com o retorno que colocamos, no caso o @user_mock. Em outras palavras, estamos substituindo o retorno de um método.

    Quando isto seria útil? Por exemplo, se precisamos testar um envio de emails, e obviamente não queremos enviar um email de verdade, poderíamos fazer:

    
    Mailer.stub!(:send_mail).and_return(true)
    

    Ou seja, toda vez que se chamar Mailer.send_mail de dentro da cláusula "it" em questão, sempre será retornado "true". O email não é enviado. Existem formas melhores de testar, como contar quantos emails foram enviados ou checar o conteúdo e recipientes, mas essa é a idéia básica.

    Já ao usar o:

    
    @user_mock.should_receive(:name).and_return("Tapajós")
    

    ... estamos criando um "mock", que na realidade é um objeto onde existe uma programação de expectativas.

    Lembrando que existem 4 tipos de objetos "dublê": dummy, fake, stubs e mocks. Os 'stubs' dão respostas "programadas" e os mocks são objetos que possuem um mecanismo de expectativa. Você diz o que um mock deve esperar, e ao final do teste caso isto não tenha acontecido, ele dá o erro e você sabe que algo falhou em seus testes.

    Um artigo que fala sobre estes tipos de objetos "dublê" está em Mocks Aren't Stubs.

    Acho que é isso... Estou começando no estudo de testes, então se falei algo errado por favor me corrijam, pessoal :)

    Como disse o Lucas Húngaro, vamos discutir mais o tema de testes! É necessário! :)

    Abraço! Levy

  2. Sylvestre Mergulhão disse 11 dias depois:

    Fala Tapa!

    Não discordo completamente do uso de mocks em testes unitários, mas nesse caso específico que você descreveu existe um outro problema.

    Caso você mude o atributo name para full_name você terá que corrigir todos os lugares em que você fez mock do método name. O que é um problema, pode acontecer de testes passarem por ter mockado, mas ao rodar de verdade o código de crash.

    É claro que quem usa mocks tem que estar ciente disso. O importante é ter um teste em algum lugar que irá quebrar informando o problema.

    Escrevi um post no meu blog questinando sobre como utilizar o describe do rspec. Dê uma passada lá.

    PS: conseguiu resolver aquele problema que você me perguntou mais cedo?

    Um abraço!

  3. Tapajós disse 12 dias depois:

    Oi Sylvestre,

    Quando se usa mock realmente se cai nesse problema. No caso desse exemplo eu queria três coisas:

    1 - Mostrar o problema do conhecimento.

    2 - Me isolar do banco.

    3 - Criar um exemplo didático.

    Nessa série muita coisa será feita propositadamente de uma forma justamente para usar o que eu estiver querendo mostrar.

    Com relação ao problema que conversamos mais cedo ainda não pensei muito, vou tentar alguma forma e depois te conto.

    Vou olhar o seu post mais tarde e faço meus comentários.

    Um abração e te vejo na hora extra segunda.

    Um abraço

  4. daniel lopes disse 12 dias depois:

    Muito legal o post, só não concordo muito com uma coisa. Associantion proxies... não vejo nada de errado em usar @account.user.name, pelo contrário, se eu for colocar um método na classe account para cada atributo que usar vou estar duplicando o tamanho do meu código e tornando muito mais dificil de dar manutenção. Eu penso que se vc tem um sistema funcionando e muda os campos do DB em alguma tabela é esperado que você tenha erros e vc tem que estar preparado para isso.... acho que seria melhor testar a associação de account com user ao invés de implementar métodos dentro de account.

    Bem, essa é a minha opnião.

  5. Tapajós disse 14 dias depois:

    Daniel, usei o exemplo do ActiveRecord apenas para ilustrar. O problema grave mesmo é o excesso de conhecimento de um objeto ao outro.

    Um abraço

  6. Lucas de Castro disse 15 dias depois:

    Tapajós, queria saber como vocês testam finders mais complexos, como por exemplo envolvendo vários models e muitas 'conditions'. Vocês usam fixtures nesses casos ou fazem com mocks?

  7. Daniel Lopes disse 18 dias depois:

    hum... então também concordo. Isso é importante em qualquer linguagem, quanto mais caixa preta forem as coisas melhor.