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.
0sem comentários ainda