Qual deve ser a granularidade dos testes TDD?

18

Durante o treinamento de TDD baseado no caso do software médico, estamos implementando a seguinte história: "Quando o usuário pressiona o botão Salvar, o sistema deve adicionar um paciente, adicionar um dispositivo e adicionar registros de dados do dispositivo".

A implementação final será mais ou menos assim:

if (_importDialog.Show() == ImportDialogResult.SaveButtonIsPressed)
{
   AddPatient();
   AddDevice();
   AddDeviceDataRecords();
}

Temos duas maneiras de implementá-lo:

  1. Três testes nos quais cada um verifica um método (AddPatient, AddDevice, AddDeviceDataRecords) foram chamados
  2. Um teste que verifica todos os três métodos foi chamado

No primeiro caso, se algo de errado acontecer com a condição da cláusula if, todos os três testes falharão. Mas, no segundo caso, se o teste falhar, não temos certeza do que está exatamente errado. De que maneira você prefere?

SiberianGuy
fonte

Respostas:

8

Mas, no segundo caso, se o teste falhar, não temos certeza do que está exatamente errado.

Eu acho que isso dependeria em grande parte de quão boas mensagens de erro o teste produz. Em geral, existem diferentes maneiras de verificar se um método foi chamado; por exemplo, se você usar um objeto simulado, ele fornecerá uma mensagem de erro precisa descrevendo qual método esperado não foi chamado durante o teste. Se você verificar que o método foi chamado através da detecção dos efeitos da chamada, é sua responsabilidade produzir uma mensagem de erro descritiva.

Na prática, a escolha entre as opções 1 e 2 também depende da situação. Se eu vir o código que você mostra acima em um projeto herdado, escolho a abordagem pragmática do Caso 2 apenas para verificar se cada um dos três métodos é chamado corretamente quando a condição é atendida. Se eu estiver desenvolvendo esse trecho de código agora, as três chamadas de método provavelmente serão adicionadas uma a uma, em momentos separados (possivelmente dias ou meses um do outro), então eu adicionaria um novo teste de unidade separado para verificar cada chamada.

Observe também que, de qualquer maneira, você também deve ter testes de unidade separados para verificar se cada um dos métodos individuais faz o que deve fazer.

Péter Török
fonte
Você não acha razoável, eventualmente, combinar esses três testes em um?
SiberianGuy
@Idsa, pode ser uma decisão razoável, embora na prática raramente me incomode com esse tipo de refatoração. Por outro lado, estou trabalhando com código legado, onde as prioridades são diferentes: nos concentramos em aumentar a cobertura de teste do código existente e manter a quantidade crescente de testes de unidade que podem ser mantidos.
Péter Török
30

A granularidade no seu exemplo parece ser a diferença entre testes de unidade e aceitação.

Um unittest testa uma única unidade de funcionalidade, com o menor número possível de dependências. No seu caso, poderia haver 4 unittests

  • AddPatient adiciona um paciente (ou seja, chama as funções relevantes do banco de dados)?
  • AddDevice adiciona um dispositivo?
  • AddDeviceDataRecords adiciona os registros?
  • a função principal não corrigida em seu exemplo chama AddPatient, AddDevice e AddDeviceFunctions

Os unittests são para os desenvolvedores , para que eles obtenham confiança, que seu código esteja tecnicamente correto

Os testes de aceitação devem testar a funcionalidade combinada, da perspectiva do usuário. Eles devem ser modelados ao longo das histórias de usuários e ter o mais alto nível possível. Portanto, você não precisa verificar se as funções são chamadas, mas se um benefício visível para o usuário é alcançado:

quando o usuário digita os dados, clica em ok e ...

  • ... vai para a lista de pacientes, ele deve ver um novo paciente com o nome
  • ... vai para a lista de dispositivos, ele deve ver um novo dispositivo
  • ... vai aos detalhes do novo dispositivo, ele deve ver novos registros de dados

testes de aceitação são para os clientes ou para construir uma melhor comunicação com eles.

Para responder à sua pergunta "o que você prefere": qual é um problema maior para você agora, bugs e regressão (=> mais unittests) ou entender e formalizar o cenário geral (=> mais testes de aceitação)

keppla
fonte
13

Temos duas maneiras de implementá-lo:

Isso é falso.

Três testes nos quais cada um verifica um método (AddPatient, AddDevice, AddDeviceDataRecords) foram chamados

Você deve fazer isso para garantir que funcione.

Um teste que verifica todos os três métodos foi chamado

Você também deve fazer isso para garantir que a API funcione.

A classe - como uma unidade - deve ser completamente testada. Cada método.

Você pode começar com um teste que cubra todos os três métodos, mas não diz muito.

se o teste falhar, não temos certeza do que está exatamente errado.

Corrigir. É por isso que você testa todos os métodos.

Você deve testar a interface pública. Como essa classe faz três coisas mais uma (mesmo que estejam agrupadas em um método por causa da história do usuário), você deve testar todas as quatro coisas. Três pacotes de baixo nível e um.

S.Lott
fonte
2

Escrevemos nossos testes de unidade para frases significativas de funcionalidade que muitas vezes são mapeadas para um método (se você escreveu bem o seu código), mas às vezes ficam maiores, abrangendo muitos métodos.

Por exemplo, imagine que adicionar um paciente ao seu sistema precise que algumas sub-rotinas (funções filho) sejam chamadas:

  1. VerifyPatientQualification
  2. GuaranteDoctorExistence
  3. CheckInsuranceHistory
  4. GuaranteEmptyBed

Também podemos escrever teste de unidade para cada uma dessas funções.

Saeed Neamati
fonte
2

Uma regra simples que eu tenho seguido é nomear o teste para que ele descreva com precisão o que o teste faz. Se o nome do teste ficar muito complexo, é um sinal de que o teste talvez esteja fazendo muito. Por exemplo, nomear um teste para fazer o que você propõe na opção 2 pode se parecer com PatientIsAddedDeviceIsAddedAndDeviceDataRecordsWhenSaved, que é muito mais complexo do que três testes separados PatientIsAddedWhenSaved, DeviceIsAddedWhenSaved, DataRecordsWhenSaved. Também acho que as lições que podem ser aprendidas com o BDD são bastante interessantes, onde cada teste é realmente representativo de um único requisito que pode ser descrito em uma linguagem natural.

jpierson
fonte