SavePeople () deve ser testado em unidade
Sim deveria. Mas tente escrever suas condições de teste de maneira independente da implementação. Por exemplo, transformando seu exemplo de uso em um teste de unidade:
function testSavePeople() {
myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);
assert(myDataStore.containsPerson('Joe'));
assert(myDataStore.containsPerson('Maggie'));
assert(myDataStore.containsPerson('John'));
}
Este teste faz várias coisas:
- verifica o contrato da função
savePeople()
- não se importa com a implementação de
savePeople()
- documenta o exemplo de uso de
savePeople()
Observe que você ainda pode simular / stub / fake o armazenamento de dados. Nesse caso, eu não verificaria chamadas de função explícitas, mas o resultado da operação. Dessa forma, meu teste está preparado para futuras alterações / refatores.
Por exemplo, a implementação do armazenamento de dados pode fornecer um saveBulkPerson()
método no futuro - agora, uma alteração na implementação do savePeople()
uso saveBulkPerson()
não interromperia o teste de unidade enquanto saveBulkPerson()
funcionasse conforme o esperado. E se saveBulkPerson()
de alguma forma não funciona como esperado, o teste de unidade vai pegar isso.
ou esses testes equivaleriam a testar a construção interna da linguagem forEach?
Como dito, tente testar os resultados esperados e a interface da função, não a implementação (a menos que você esteja executando testes de integração - é possível capturar chamadas de funções específicas). Se houver várias maneiras de implementar uma função, todas elas deverão funcionar com seu teste de unidade.
Em relação à sua atualização da pergunta:
Teste para alterações de estado! Por exemplo, parte da massa será usada. De acordo com sua implementação, afirme que a quantidade de dough
itens usados se encaixa pan
ou afirme que os itens foram dough
usados. Afirme que pan
contém cookies após a chamada da função. Afirme que oven
está vazio / no mesmo estado de antes.
Para testes adicionais, verifique os casos extremos: O que acontece se o oven
item não estiver vazio antes da chamada? O que acontece se não houver o suficiente dough
? Se o pan
já estiver cheio?
Você deve poder deduzir todos os dados necessários para esses testes dos próprios objetos de massa, panela e forno. Não há necessidade de capturar as chamadas de função. Trate a função como se sua implementação não estivesse disponível para você!
De fato, a maioria dos usuários do TDD escreve seus testes antes de escrever a função, para que não dependam da implementação real.
Para sua mais recente adição:
Quando um usuário cria uma nova conta, várias coisas precisam acontecer: 1) um novo registro do usuário precisa ser criado no banco de dados 2) um email de boas-vindas precisa ser enviado 3) o endereço IP do usuário precisa ser registrado por fraude propósitos.
Então, queremos criar um método que vincule todas as etapas do "novo usuário":
function createNewUser(validatedUserData, emailService, dataStore) {
userId = dataStore.insertUserRecord(validateduserData);
emailService.sendWelcomeEmail(validatedUserData);
dataStore.recordIpAddress(userId, validatedUserData.ip);
}
Para uma função como essa eu zombaria / stub / fake (o que parece mais geral) os parâmetros dataStore
e emailService
. Esta função não realiza transições de estado por conta própria, mas as delega a métodos de algumas delas. Eu tentaria verificar se a chamada para a função fez 4 coisas:
- ele inseriu um usuário no armazenamento de dados
- enviou (ou pelo menos chamou o método correspondente) um email de boas-vindas
- registrou o IP do usuário no armazenamento de dados
- delegou qualquer exceção / erro encontrado (se houver)
As três primeiras verificações podem ser feitas com zombarias, stubs ou falsificações de dataStore
e emailService
(você realmente não deseja enviar e-mails durante o teste). Desde que eu tive que procurar por alguns dos comentários, estas são as diferenças:
- Uma farsa é um objeto que se comporta da mesma forma que o original e é, até certo ponto, indistinguível. Seu código normalmente pode ser reutilizado nos testes. Isso pode, por exemplo, ser um banco de dados simples na memória para um wrapper de banco de dados.
- Um esboço implementa apenas o necessário para realizar as operações necessárias deste teste. Na maioria dos casos, um esboço é específico para um teste ou um grupo de testes que requer apenas um pequeno conjunto de métodos do original. Neste exemplo, poderia ser um
dataStore
que apenas implementa uma versão adequada de insertUserRecord()
e recordIpAddress()
.
- Um mock é um objeto que permite verificar como é usado (na maioria das vezes permitindo avaliar chamadas para seus métodos). Eu tentaria usá-los com moderação em testes de unidade, pois, ao usá-los, você realmente tenta testar a implementação da função e não a aderência à sua interface, mas eles ainda têm seus usos. Existem muitas estruturas de simulação para ajudá-lo a criar apenas a simulação de que você precisa.
Observe que, se algum desses métodos gerar um erro, queremos que o erro atinja o código de chamada, para que ele possa lidar com o erro como achar melhor. Se estiver sendo chamado pelo código da API, poderá converter o erro em um código de resposta HTTP apropriado. Se estiver sendo chamado por uma interface da web, poderá converter o erro em uma mensagem apropriada a ser exibida ao usuário, e assim por diante. O ponto é que essa função não sabe como lidar com os erros que podem ser gerados.
Exceções / erros esperados são casos de teste válidos: Você confirma que, no caso de um evento desse tipo, a função se comporta da maneira que você espera. Isso pode ser alcançado deixando o objeto mock / fake / stub correspondente ser lançado quando desejado.
A essência da minha confusão é que, para testar essa função por unidade, parece necessário repetir a implementação exata no próprio teste (especificando que os métodos são chamados em zombarias em uma certa ordem) e isso parece errado.
Às vezes, isso precisa ser feito (embora você se preocupe com isso principalmente nos testes de integração). Mais frequentemente, existem outras maneiras de verificar os efeitos colaterais / alterações de estado esperados.
Verificar as chamadas exatas das funções resulta em testes de unidade bastante frágeis: apenas pequenas alterações na função original causam falhas. Isso pode ser desejado ou não, mas requer uma alteração nos testes de unidade correspondentes sempre que você altera uma função (seja refatoração, otimização, correção de erros, ...).
Infelizmente, nesse caso, o teste de unidade perde parte de sua credibilidade: desde que foi alterado, ele não confirma a função após a alteração se comportar da mesma maneira que antes.
Por exemplo, considere alguém adicionando uma chamada para oven.preheat()
(otimização!) No seu exemplo de assadeira de biscoitos:
- Se você zombou do objeto do forno, ele não espera essa chamada e falha no teste, embora o comportamento observável do método não tenha mudado (você ainda tem uma bandeja de biscoitos, espero).
- Um stub pode ou não falhar, dependendo se você adicionou apenas os métodos a serem testados ou a interface inteira com alguns métodos fictícios.
- Um falso não deve falhar, pois deve implementar o método (de acordo com a interface)
Nos meus testes de unidade, tento ser o mais geral possível: se a implementação mudar, mas o comportamento visível (da perspectiva do chamador) ainda for o mesmo, meus testes deverão passar. Idealmente, o único caso em que preciso alterar um teste de unidade existente deve ser uma correção de bug (do teste, não da função em teste).