Teste Unitário?

ad: Há muito mais sobre a temática Test-First aqui no blog. Dê uma olhada nos posts mais recentes e assine o feed dos posts 😉

Ponto crucial para um possível futuro level up como Test-First e Test-Driven Development Design, o Teste Unitário de Unidade, em minha opinião, aparece como ponte entre fazer testes vs se frustar com eles.

O Teste Unitário de Unidade consta como principal forma de teste em várias literaturas que abordam o tema. Entender como criar testes de unidade para seu software é decisivo para adoção da divertida (e emprego keeper) forma de codificar software.

Primeiramente, é bom enfatizar que teste de unidade não é testar todo método de seu objeto, mas sim testar toda sua interface pública que contém regras de domínio. Vamos tomar o seguinte exemplo:

class User
  attr_reader :name, :birthdate

  def initialize(args)
    # do something to build object
  end

  def sent_packages
  end
end

No exemplo, temos o construtor (initialize), o método sent_packages além dos getters name e birthdate.

Para este caso, os getters retornarão o dado exatamente como ele foi inputado – prefiro o termo injetado – ou seja, os getters não contêm nenhum acréscimo para o negócio (software), por isto, não é necessário criar testes para eles, pois o Ruby tem o teste que garante o correto retorno para o dos valores em memória no attr_reader. Se o attr_reader já foi testado pela linguagem e o seu getter não tem nenhum acréscimo (formatação, contatenação, etc), não precisa-se criar teste para isto.

Um teste válido neste caso seria construir um usuário válido e verificar o retorno do sent_packages.

Vamos deixar a classe mais interessante:

class User
  attr_reader :name, :birthdate

  def initialize(args)
    # do something to build object
  end

  def sent_packages
    do_some_weird_stuff
    # do more things
  end

  private

  def do_some_weird_stuff
  end
end

Ainda assim, deve-se continuar com os testes anteriores, pois não é recomendado testar métodos privados/protegidos isoladamente. Devemos sim, testar a interface pública – o método público – que chama o método privado que no exemplo acima é o sent_packages.

Aqui vale um parênteses importante sobre métodos privados: Sandi Metz afirma em seu livro que devemos levar todo método privado como um contrato que nos diz claramente: você pode até usar meu método privado, mas lembre-se: eu não garanto que ele continuará retornando o que retorna hoje numa próxima versão. Ele poderá até ser removido. Essa afirmação é válida, pois em Ruby você pode acessar qualquer método de um objeto mesmo os privados. Isso porque em Ruby, que segue o que eu já ouvi falar por “Smalltalk OOP”, a orientação a objetos é pura e simplesmente troca de mensagens. Depois que adotei este mindset, a troca de mensagem e colaboração em minhas classes aumentaram significantemente.

Teste Unitário de Unidade não deixa haver colaboração entre outros objetos além do alvo do teste. O que isso significa em termos práticos?

class User
  attr_reader :name, :birthdate

  def initialize(args)
    # do something to build object
    @packages = []
  end

  def sent_packages
    packages.map { |package| package.sent? }
  end

  def packages
    # get user packages from somewhere (ORM, Repository, etc)
  end
end

class Package
  attr_reader :track_number, :post_date

  def sent?
    !!track_number
  end
end

O sent_packages acima pode te lembrar várias coisas: Rails ActiveRecord; Repository acessando dados de um DataMapper ORM ou uma simples iteração objeto-coleção.

Repare que a classe User precisa falar com a classe Package para verificar quais dos packages foram sent. Não podemos deixar que o teste da classe User permita que o objeto consiga falar com Package. Ou seja:

describe User do
  subject { User.new } # only to show how RSpec works to non-Ruby developers

  context "when User relates with Package" do
    let(:all_packages) do
      [
        double('Package', :sent? => true),
        double('Package', :sent? => true),
        double('Package', :sent? => false)
      ]
    end

    let(:sent_packages) { all_packages[0..1] }

    it ".sent_packages" do
      allow(subject).to receive(:packages).and_return(all_packages) # this is a stub!
      expect(subject.sent_packages).to be_equal sent_packages
    end
  end
end

Mesmo que você não entenda Ruby nem as DSL’s do RSpec, repare que no let(:all_packages) eu utilizo um recurso chamado double que nada mais é do que um objeto simples que pode ter qualquer nome. No caso, o chamei de ‘Package’. O sent?: boolean diz ao double que quando o método sent? dele for chamado, deverá retornar true dos dois primeiros e false no último. Isso é equivalente a:

class Double
  def sent?
    true # and false in the last one.
  end
end

Já o let(:sent_packages) retornará uma parte da Collection all_packages, no caso os dois primeiros doubles que retornam true no método sent?.

Finalmente, o it que é o teste, faz duas coisas: na primeira linha ele diz que User#packages deverá retornar a collection all_packages. Aqui aconteceu a mágica: com isso eu não deixo o User falar com o Package de verdade. Eu faço o User retornar uma collection que eu tenho total controle. Já explico o motivo disso.

Na segunda linha, eu crio uma expectativa que é: o método User#sent_packages deverá retornar apenas os Package que tenham sent? igual a true.

O motivo de utilizar o Stub (ali no allow...) é fazer que o método packages de User sempre retorne o que eu quero de uma forma controlada. Como o método package nada mais é do que um delegator para um Repositório ou mesmo um has_many :packages do Active Record, isto é, uma dependência externa (outra classe), eu posso stubar ou mockar.

Dica: não mock/stub método de sua própria classe/objeto a menos que seja um delegator puro para outra classe, como no exemplo.

Dica polêmica: Only Mock what you own. Mock apenas o que você domina. Ou seja, classes criadas para o software que você está trabalhando.

O que você testou afinal?

Neste teste eu quero apenas saber se o User#sent_packages sabe filtrar de todos os meus packages, apenas aqueles que foram sent e ele sabe!

def sent_packages
  packages.map { |package| package.sent? }
end

Isso porque sempre devemos presumir que a classe relacionada já está testada individualmente. Em outras palavras: devemos assumir que Package já está testada isoladamente (Teste de Unidade).

Porque não deixar User falar com Package?

Por quê isso que é Teste de Unidade. Se eu deixasse User falar com Package, deixaria de ser teste de uma unidade do sistema e passaria a ser duas – e não queremos isto, né? Pois isto criaria um acoplamento entre User e Package e não me evitaria a deixá-las isoladamente funcionando. Quando você não se atenta para esse tipo de coisa, você acaba deixando seu teste acessar o banco de dados ou aquele serviço REST ou SOAP. Isolamento como o acima, deve acontecer toda vez que o objeto que você está testando precisar falar com outra classe(objeto dela).

Concluindo

Teste de Unidade é um assunto longo, envolve muito mais do que simplesmente saber os do and don’t, pois Teste de Unidade é questão de design.

Às vezes, você ficará tentado a testar mais de uma unidade, deixar o teste acessar o database, a API e outras classes do seu domínio. A prática leva a perfeição, dizem.

Final Alternativo (e melhorado)

class User
  attr_reader :name, :birthdate, :repository

  def initialize(args, repository: DomainRepository.new)
    @repository = repository # hey, I am an injected dependency class
    @packages = []
  end

  def sent_packages
    packages.map { |package| package.sent? }
  end

  def packages
    repository.packages_for(self)
  end
end

class Package
  attr_reader :track_number, :post_date

  def sent?
    !!track_number
  end
end

Sem o ActiveRecord do Rails, o exemplo acima seria uma possibilidade: injeção do Repository via construtor ou setter. Desta forma, o mock não ficaria no método packages do User, mas sim, no Repository injetado o que é epic win. Veja:

describe User do
  context "when User relates with Package" do
    let(:all_packages) do
      [
        double('Package', :sent? => true),
        double('Package', :sent? => true),
        double('Package', :sent? => false)
      ]
    end

    let(:repository) { double('Repository', packages_for: all_packages) }
    subject { User.new({}, repository: repository) } # Stub Repository injected

    let(:sent_packages) { all_packages[0..1] }

    it ".sent_packages" do
      expect(subject.sent_packages).to be_equal sent_packages
    end
  end
end

Com a adição de let(:repository) que é injetado no subject {..}, foi possível remover aquele stub allow(subject).to ... do teste, deixando-o mais limpo, legível e plugável. Substituí uma injeção direta pelo stub da classe Repository. Este é o ideal, mas nem sempre é possível evitar que um Rails apareça com seu Active Record e deixe as coisas um pouco mais… complicadas. Não caia no engano de achar que apenas o Rails é vilão: até hoje não achei um framework ORM de Active Record que fosse plugável como um DataMapper é.

É isso sempre que buscamos com o Teste de Unidade: desacoplamento. Retirar coisas e torná-las injetáveis em nossas classes com o objetivo de passar e trocar mensagens entre os métodos de nossos objetos igualmente desacoplados. Isso é a base para criar seu design orientado a objetos.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s