Digamos que eu tenho uma função (escrita em Ruby, mas deve ser compreensível por todos):
def am_I_old_enough?(name = 'filip')
person = Person::API.new(name)
if person.male?
return person.age > 21
else
return person.age > 18
end
end
Nos testes de unidade, eu criaria quatro testes para cobrir todos os cenários. Cada um deles usará Person::API
objetos simulados com métodos stubbed male?
e age
.
Agora, trata-se de escrever testes de integração. Presumo que Person :: API não deva mais ser ridicularizado. Então, eu criaria exatamente os mesmos quatro casos de teste, mas sem zombar do objeto Person :: API. Isso está correto?
Se sim, então qual é o objetivo de escrever testes de unidade, se eu pudesse escrever testes de integração que me dêem mais confiança (enquanto trabalho em objetos reais, não stubs ou zombarias)?
unit-testing
testing
ruby
integration-tests
Filip Bartuzi
fonte
fonte
Respostas:
Não, os testes de integração não devem apenas duplicar a cobertura dos testes de unidade. Eles podem duplicar alguma cobertura, mas esse não é o ponto.
O objetivo de um teste de unidade é garantir que uma pequena parte específica da funcionalidade funcione exatamente e completamente como pretendido. Um teste de unidade testaria
am_i_old_enough
dados com diferentes idades, certamente aquelas próximas ao limiar, possivelmente todas com idades humanas. Depois de escrever esse teste, a integridade deam_i_old_enough
nunca mais deve ser questionada.O objetivo de um teste de integração é verificar se o sistema inteiro ou uma combinação de um número substancial de componentes faz a coisa certa quando usados juntos . O cliente não se importa com uma função de utilidade específica que você escreveu, ele se preocupa com o fato de o aplicativo da Web estar devidamente protegido contra o acesso de menores, porque, caso contrário, os reguladores terão seus direitos.
Verificar a idade do usuário é uma pequena parte dessa funcionalidade, mas o teste de integração não verifica se a função do utilitário usa o valor limite correto. Ele testa se o chamador toma a decisão correta com base nesse limite, se a função de utilitário é chamada, se outras condições de acesso são atendidas etc.
A razão pela qual precisamos dos dois tipos de testes é basicamente que há uma explosão combinatória de cenários possíveis para o caminho através de uma base de código que a execução pode levar. Se a função de utilitário tiver cerca de 100 entradas possíveis e houver centenas de funções de utilitário, verificar se a coisa certa acontece em todos os casos exigiria muitos milhões de casos de teste. Simplesmente verificando todos os casos em escopos muito pequenos e depois verificando combinações comuns, relevantes ou prováveis para esses escopos, assumindo que esses pequenos escopos já estejam corretos, como demonstrado pelo teste de unidade , podemos obter uma avaliação bastante confiante de que o sistema está fazendo o que deveria, sem se afogar em cenários alternativos para testar.
fonte
The customer doesn't care about a particular utility function you wrote, they care that their web app is properly secured against access by minors
-> Essa é uma mentalidade muito inteligente, obrigado! O problema é quando você projeta por si mesmo. É difícil dividir a sua mentalidade entre ser um programador e sendo um gerente de produto no mesmo momentoA resposta curta é não". A parte mais interessante é por que / como essa situação pode surgir.
Eu acho que a confusão está surgindo porque você está tentando aderir a práticas estritas de teste (testes de unidade vs testes de integração, zombaria etc.) para código que parece não aderir a práticas estritas.
Isso não quer dizer que o código esteja "errado" ou que práticas específicas sejam melhores que outras. Simplesmente algumas das suposições feitas pelas práticas de teste podem não se aplicar nessa situação e pode ajudar a usar um nível semelhante de "rigor" nas práticas de codificação e práticas de teste; ou pelo menos, reconhecer que eles podem estar desequilibrados, o que fará com que alguns aspectos sejam inaplicáveis ou redundantes.
O motivo mais óbvio é que sua função está executando duas tarefas diferentes:
Person
base em seu nome. Isso requer teste de integração, para garantir que ele possa encontrarPerson
objetos que, presumivelmente, foram criados / armazenados em outro lugar.Person
tem idade suficiente, com base em seu sexo. Isso requer teste de unidade, para garantir que o cálculo funcione conforme o esperado.Ao agrupar essas tarefas em um bloco de código, você não pode executar uma sem a outra. Quando você deseja testar os cálculos da unidade, é forçado a procurar um
Person
(a partir de um banco de dados real ou de um esboço / simulação). Quando você deseja testar se a pesquisa se integra ao resto do sistema, você também é obrigado a executar um cálculo da idade. O que devemos fazer com esse cálculo? Devemos ignorá-lo ou verificá-lo? Essa parece ser a situação exata que você está descrevendo na sua pergunta.Se imaginarmos uma alternativa, poderemos ter o cálculo por conta própria:
Como esse é um cálculo puro, não precisamos executar testes de integração nele.
Também podemos ficar tentados a escrever a tarefa de pesquisa separadamente:
No entanto, neste caso, a funcionalidade é tão próxima
Person::API.new
que eu diria que você deveria usá-lo (se o nome padrão for necessário, seria melhor armazenado em outro lugar, como um atributo de classe?).Ao escrever testes de integração para
Person::API.new
(ouperson_from_name
) tudo o que você precisa se preocupar é se você recebe de volta o esperadoPerson
; todos os cálculos baseados em idade são resolvidos em outro lugar, para que seus testes de integração possam ignorá-los.fonte
Outro ponto que gostaria de acrescentar à resposta de Killian é que os testes de unidade são executados muito rapidamente, para que possamos ter milhares deles. Um teste de integração geralmente leva mais tempo porque está chamando serviços da Web, bancos de dados ou alguma outra dependência externa; portanto, não podemos executar os mesmos testes (1000s) para cenários de integração, pois levariam muito tempo.
Além disso, os testes de unidade geralmente são executados no momento da construção (na máquina de construção) e os testes de integração são executados após a implantação em um ambiente / máquina.
Normalmente, é possível executar milhares de testes de unidade para cada build e, em seguida, nossos 100 ou mais testes de integração de alto valor após cada implantação. Podemos não levar cada compilação para implantação, mas tudo bem, porque a compilação que utilizamos para implantar os testes de integração será executada. Normalmente, queremos limitar a execução desses testes em 10 ou 15 minutos, porque não queremos atrasar a implantação por muito tempo.
Além disso, em uma programação semanal, podemos executar um conjunto de regressões de testes de integração que abrangem mais cenários no fim de semana ou outros períodos de inatividade. Isso pode levar mais de 15 minutos, pois mais cenários serão abordados, mas normalmente ninguém está trabalhando em Sat / Sun para que possamos dedicar mais tempo aos testes.
fonte