Os testes de integração pretendem repetir todos os testes de unidade?

36

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::APIobjetos 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)?

Filip Bartuzi
fonte
3
Bem, um dos pontos é que, ao zombar / testar a unidade, você pode isolar qualquer problema no seu código. Se um teste de integração falhar, você não sabe qual código está quebrado, o seu ou a API.
22416 Chris Wohlert
9
Apenas quatro testes? Você tem seis idades de fronteira você deve estar testando: 17, 18, 19, 20, 21, 22 ...;)
David Arno
22
@FilipBartuzi, presumo que o método esteja verificando se um homem tem mais de 21 anos, por exemplo? Como está escrito atualmente, não faz isso, só é verdade se tiverem mais de 22 anos. "Mais de 21" em inglês significa "21+". Portanto, há um erro no seu código. Esses erros são capturados testando os valores-limite, ou seja, 20, 21, 22 para um homem, 17,18,19 para uma mulher neste caso. Portanto, são necessários pelo menos seis testes.
David Arno
6
Sem mencionar os casos de 0 e -1. O que significa uma pessoa ter -1 anos de idade? O que seu código deve fazer se sua API retornar algo sem sentido?
precisa
9
Seria muito mais fácil testar se você passasse um objeto de pessoa como parâmetro.
Jeffo

Respostas:

72

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_enoughdados com diferentes idades, certamente aquelas próximas ao limiar, possivelmente todas com idades humanas. Depois de escrever esse teste, a integridade de am_i_old_enoughnunca 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.

Kilian Foth
fonte
6
"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". Obrigado. Adoro quando alguém aborda os testes automatizados com sanidade.
precisa saber é
11
JB Rainsberger tem uma boa conversa sobre testes e a explosão combinatória que você está escrevendo no último parágrafo, chamada "Testes integrados são uma farsa" . Não se trata tanto de testes de integração, mas ainda é bastante interessante.
Bart van Nierop
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 momento
Filip Bartuzi
14

A 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:

  • Procurando um com Personbase em seu nome. Isso requer teste de integração, para garantir que ele possa encontrar Personobjetos que, presumivelmente, foram criados / armazenados em outro lugar.
  • Calculando se a Persontem 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:

def is_old_enough?(person)
   if person.male?
      return person.age > 21
   else 
      return person.age > 18
   end
end

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:

def person_from_name(name = 'filip')
   return Person::API.new(name)
end

No entanto, neste caso, a funcionalidade é tão próxima Person::API.newque 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(ou person_from_name) tudo o que você precisa se preocupar é se você recebe de volta o esperado Person; todos os cálculos baseados em idade são resolvidos em outro lugar, para que seus testes de integração possam ignorá-los.

Warbo
fonte
11

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.

Jon Raynor
fonte
não se aplica a linguagens dinâmicas (ou seja, sem fase de construção)
Filip Bartuzi