Onde está a linha entre a lógica do aplicativo de teste de unidade e as construções de linguagem desconfiada?

87

Considere uma função como esta:

function savePeople(dataStore, people) {
    people.forEach(person => dataStore.savePerson(person));
}

Pode ser usado assim:

myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);

Vamos supor que ele Storetenha seus próprios testes de unidade ou seja fornecido pelo fornecedor. De qualquer forma, confiamos Store. E vamos assumir ainda que o tratamento de erros - por exemplo, erros de desconexão de banco de dados - não é de responsabilidade de savePeople. De fato, suponhamos que a loja em si seja um banco de dados mágico que não pode ter erros de forma alguma. Dadas essas suposições, a questão é:

Deveria savePeople()ser testado em unidade, ou esses testes equivaleriam a testar a construção de forEachlinguagem interna?

É claro que poderíamos passar uma zombaria dataStoree afirmar que dataStore.savePerson()é chamada uma vez para cada pessoa. Você certamente poderia argumentar que esse teste fornece segurança contra alterações na implementação: por exemplo, se decidirmos substituir forEachpor um forloop tradicional ou algum outro método de iteração. Portanto, o teste não é totalmente trivial. E, no entanto, parece muito perto ...


Aqui está outro exemplo que pode ser mais proveitoso. Considere uma função que não faz nada além de coordenar outros objetos ou funções. Por exemplo:

function bakeCookies(dough, pan, oven) {
    panWithRawCookies = pan.add(dough);
    oven.addPan(panWithRawCookies);
    oven.bakeCookies();
    oven.removePan();
}

Como uma função como essa deve ser testada em unidade, supondo que você pense que deveria? É difícil para mim imaginar qualquer tipo de teste de unidade que não é simplesmente zombar dough, pane oven, em seguida, afirmar que os métodos são chamados sobre eles. Mas esse teste nada mais faz do que duplicar a implementação exata da função.

Essa incapacidade de testar a função de maneira significativa em caixa preta indica uma falha de design com a própria função? Se sim, como poderia ser melhorado?


Para dar ainda mais clareza à pergunta que motivou o bakeCookiesexemplo, adicionarei um cenário mais realista, que encontrei ao tentar adicionar testes e refatorar o código legado.

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);
}

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.

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.

Jonah
fonte
44
Depois que ele for executado. Você tem cookies
Ewan
6
em relação à sua atualização: por que você gostaria de zombar de uma panela? ou massa? isso soa como objetos simples na memória que devem ser triviais de criar e, portanto, não há razão para você não testá-los como uma unidade. lembre-se, a "unidade" em "teste de unidade" não significa "uma única classe". significa "a menor unidade de código possível que é usada para fazer algo". uma panela é provavelmente nada mais do que um recipiente para objetos de massa, por isso estaria planejado para testá-lo em isolamento em vez de apenas test-drive o método bakeCookies de fora para dentro.
sara
11
No final do dia, o princípio fundamental em ação aqui é que você escreva testes suficientes para garantir a si mesmo que o código funciona e que é um "canário na mina de carvão" adequado quando alguém muda alguma coisa. É isso aí. Não há encantamentos mágicos, suposições formuladas ou afirmações dogmáticas, e é por isso que 85% a 90% da cobertura do código (não 100%) é amplamente considerada excelente.
Robert Harvey
5
O @RobertHarvey, infelizmente, banalidades formidáveis ​​e trechos sonoros de TDD, embora com certeza lhe dê um entusiasmo de concordância, não ajuda a resolver problemas do mundo real. para isso você precisa sujar as mãos e arriscar responder a uma pergunta real
Jonah
4
Teste de unidade em ordem decrescente de complexidade ciclomática. Confie em mim, você vai correr fora do tempo antes de chegar a esta função
Neil McGuigan

Respostas:

118

Deve savePeople()ser testado em unidade? Sim. Você não está testando se dataStore.savePersonfunciona ou se a conexão db funciona, ou mesmo se foreachfunciona. Você está testando que savePeoplecumpre a promessa que faz por meio de seu contrato.

Imagine este cenário: alguém faz um grande refator da base de código e remove acidentalmente a forEachparte da implementação para que ela sempre salve apenas o primeiro item. Você não gostaria de um teste de unidade para pegar isso?

Bryan Oakley
fonte
20
@RobertHarvey: Há muita área cinzenta, e fazer a distinção IMO não é importante. Você está certo - não é realmente importante testar se "ele chama as funções certas", mas sim "ele faz a coisa certa", independentemente de como o faz. O importante é testar se, dado um conjunto específico de entradas para a função, você obtém um conjunto específico de saídas. No entanto, posso ver como essa última frase pode ser confusa, então a removi.
Bryan Oakley
64
"Você está testando que savePeople cumpre a promessa que faz através de seu contrato." Este. Tanto isso.
Lovis
2
A menos que você tenha um teste do sistema "de ponta a ponta" que o cubra.
Ian
6
Os testes de ponta a ponta não substituem os testes de unidade, eles são gratuitos. Só porque você pode ter um teste de ponta a ponta que garante que você salve uma lista de pessoas não significa que você não deve ter um teste de unidade para cobri-lo.
Vincent Savard
4
@VincentSavard, mas o custo / benefício de um teste de unidade é reduzido se o risco for o controle de outra maneira.
Ian
36

Normalmente, esse tipo de pergunta surge quando as pessoas desenvolvem o "teste após". Aborde este problema do ponto de vista do TDD, onde os testes vêm antes da implementação, e faça a si mesmo essa pergunta novamente como um exercício.

Pelo menos na minha aplicação do TDD, que geralmente é de fora para dentro, eu não estaria implementando uma função como savePeopledepois de ter implementado savePerson. As funções savePeoplee savePersoncomeçariam como uma e sendo testadas a partir dos mesmos testes de unidade; a separação entre os dois surgiria após alguns testes, na etapa de refatoração. Esse modo de trabalho também colocaria a questão de onde a função savePeopledeveria estar - se é uma função livre ou parte da dataStore.

No final, os testes não só verificar se você pode salvar corretamente uma Personno Store, mas também muitas pessoas. Isso também me levaria a questionar se outras verificações são necessárias, por exemplo: "Preciso garantir que a savePeoplefunção seja atômica, salvando tudo ou nenhum?", "Isso pode, de alguma forma, retornar erros para as pessoas que não conseguiram ' t ser salvo? Como seriam esses erros? "e assim por diante. Tudo isso equivale a muito mais do que apenas verificar o uso de uma forEachou outras formas de iteração.

No entanto, se o requisito de salvar mais de uma pessoa de uma só vez surgir apenas depois de savePersonjá ter sido entregue, eu atualizaria os testes existentes savePersonpara executar a nova função savePeople, certificando-me de que ainda possa salvar uma pessoa simplesmente delegando primeiro, depois, teste o comportamento de mais de uma pessoa através de novos testes, pensando se seria necessário tornar o comportamento atômico ou não.

MichelHenrich
fonte
4
Teste basicamente a interface, não a implementação.
Snoop
8
Pontos justos e perspicazes. No entanto, sinto que, de alguma forma, minha verdadeira pergunta está sendo esquivada :) Sua resposta está dizendo: "No mundo real, em um sistema bem projetado, não acho que essa versão simplificada do seu problema exista". Mais uma vez, é justo, mas eu criei especificamente esta versão simplificada para destacar a essência de um problema mais geral. Se você não consegue superar a natureza artificial do exemplo, talvez possa imaginar outro exemplo no qual tenha um bom motivo para uma função semelhante, que faça apenas iteração e delegação. Ou talvez você pense que é simplesmente impossível?
Jonah
@Jonah updated. Espero que responda à sua pergunta um pouco melhor. Tudo isso é baseado em opiniões e pode ser contra o objetivo deste site, mas certamente é uma discussão muito interessante. A propósito, tentei responder do ponto de vista do trabalho profissional, onde devemos nos esforçar para deixar testes de unidade para todo o comportamento do aplicativo, independentemente de quão trivial a implementação possa ser, porque temos o dever de criar um ambiente bem testado e sistema documentado para novos mantenedores, se sairmos. Para projetos pessoais ou, digamos, não críticos (o dinheiro também é crítico), tenho uma opinião muito diferente.
MichelHenrich
Obrigado pela atualização. Como exatamente você testaria savePeople? Como eu descrevi no último parágrafo do OP ou de alguma outra maneira?
Jonah
1
Desculpe, eu não me deixei claro com a parte "sem zombaria envolvida". Eu quis dizer que não usaria uma simulação para a savePersonfunção, como você sugeriu, em vez disso, testaria através da mais geral savePeople. Os testes de unidade Storeseriam alterados para serem executados em savePeoplevez de serem chamados diretamente savePerson, portanto, para isso, nenhuma simulação é usada. Mas o banco de dados, é claro, não deve estar presente, pois gostaríamos de isolar os problemas de codificação dos vários problemas de integração que ocorrem com os bancos de dados reais; portanto, ainda temos uma simulação.
MichelHenrich
21

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 doughitens usados se encaixa panou afirme que os itens foram doughusados. Afirme que pancontém cookies após a chamada da função. Afirme que ovenestá vazio / no mesmo estado de antes.

Para testes adicionais, verifique os casos extremos: O que acontece se o ovenitem não estiver vazio antes da chamada? O que acontece se não houver o suficiente dough? Se o panjá 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 dataStoree 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 dataStoree 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 dataStoreque 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).

hoffmale
fonte
1
O problema é que, assim que você escreve, myDataStore.containsPerson('Joe')você assume a existência de um banco de dados de teste funcional. Depois de fazer isso, você está escrevendo um teste de integração e não um teste de unidade.
Jonah
Suponho que posso confiar em ter um armazenamento de dados de teste (não me importo se é real ou falso) e que tudo funcione conforme configurado (já que eu já deveria ter testes de unidade para esses casos). A única coisa que o teste deseja testar é que savePeople()realmente adicione essas pessoas a qualquer repositório de dados que você fornecer, desde que esse repositório implemente a interface esperada. Um teste de integração seria, por exemplo, verificar se meu wrapper de banco de dados realmente faz o banco de dados correto solicita uma chamada de método.
hoffmale
Para esclarecer, se você estiver usando uma simulação, tudo o que você pode fazer é afirmar que um método nessa simulação foi chamado , talvez com algum parâmetro específico. Você não pode afirmar o estado da zombaria posteriormente. Portanto, se você deseja fazer asserções sobre o estado do banco de dados após chamar o método em teste, como em myDataStore.containsPerson('Joe'), é necessário usar algum db funcional de algum tipo. Depois de dar esse passo, não é mais um teste de unidade.
Jonah
1
ele não precisa ser um banco de dados real - apenas um objeto que implementa a mesma interface que o armazenamento de dados real (leia-se: ele passa nos testes de unidade relevantes para a interface do armazenamento de dados). Eu ainda consideraria isso uma farsa. Deixe o mock armazenar tudo o que é adicionado por qualquer método para fazê-lo em uma matriz e verifique se os dados de teste (elementos de myPeople) estão na matriz. IMHO uma simulação ainda deve ter o mesmo comportamento observável esperado de um objeto real; caso contrário, você está testando a conformidade com a interface simulada, não com a interface real.
Hoffmale 22/06
"Deixe o mock armazenar tudo o que é adicionado por qualquer método para fazê-lo em uma matriz e verifique se os dados de teste (elementos de myPeople) estão na matriz" - ainda é um banco de dados "real", apenas um ad-hoc, na memória que você criou. "IMHO um mock ainda deve ter o mesmo comportamento observável esperado de um objeto real" - suponho que você possa advogar por isso, mas não é isso que "mock" significa na literatura de teste ou em qualquer uma das bibliotecas populares que eu ' eu vi. Um mock simplesmente verifica que os métodos esperados são chamados com os parâmetros esperados.
Jonah
13

O principal valor que esse teste fornece é que ele torna sua implementação refatorável.

Eu costumava fazer muitas otimizações de desempenho em minha carreira e frequentemente encontrava problemas com o padrão exato que você demonstrava: para salvar N entidades no banco de dados, executar N inserções. Geralmente é mais eficiente fazer uma inserção em massa usando uma única instrução.

Por outro lado, também não queremos otimizar prematuramente. Se você normalmente salva apenas 1 a 3 pessoas por vez, a gravação de um lote otimizado pode ser um exagero.

Com um teste de unidade adequado, você pode escrevê-lo da maneira que o implementou acima e, se achar que precisa otimizá-lo, poderá fazê-lo com a rede de segurança de um teste automatizado para detectar erros. Naturalmente, isso varia de acordo com a qualidade dos testes; portanto, teste liberalmente e bem.

A vantagem secundária do teste unitário desse comportamento é servir como documentação para qual é seu objetivo. Este exemplo trivial pode ser óbvio, mas, dado o próximo ponto abaixo, pode ser muito importante.

A terceira vantagem, apontada por outros, é que você pode testar detalhes ocultos que são muito difíceis de testar com testes de integração ou aceitação. Por exemplo, se houver um requisito de que todos os usuários sejam salvos atomicamente, você poderá escrever um caso de teste para isso, o que lhe dará uma maneira de saber se ele se comporta conforme o esperado e também serve como documentação para um requisito que pode não ser óbvio para novos desenvolvedores.

Vou acrescentar um pensamento que recebi de um instrutor de TDD. Não teste o método. Teste o comportamento. Em outras palavras, você não testa se savePeoplefunciona, está testando que vários usuários podem ser salvos em uma única chamada.

Eu descobri que minha capacidade de fazer testes de unidade de qualidade e o TDD melhoram quando parei de pensar nos testes de unidade para verificar se um programa funciona, mas eles verificam se uma unidade de código faz o que eu esperava . Aqueles são diferentes. Eles não verificam se funciona, mas verificam se faz o que acho que faz. Quando comecei a pensar assim, minha perspectiva mudou.

Brandon
fonte
O exemplo de refatoração de pastilhas em massa é bom. O possível teste de unidade que sugeri no OP - que uma simulação do dataStore o savePersonchamou para cada pessoa da lista - seria interrompido com uma refatoração de inserção em massa, no entanto. O que para mim indica que é um teste de unidade ruim. No entanto, não vejo uma alternativa que passe nas implementações em massa e de uma economia por pessoa, sem usar um banco de dados de teste real e afirmar com base nisso, o que parece errado. Você poderia fornecer um teste que funcione para as duas implementações?
Jonah
1
@ jpmc26 Que tal um teste que testa que as pessoas foram salvas ...?
22716 immibis
1
@immibis Eu não entendo o que isso significa. Presumivelmente, o armazenamento real é apoiado por um banco de dados, então você deve zombar ou stub para um teste de unidade. Portanto, nesse ponto, você testaria se seu mock ou stub pode armazenar objetos. Isso é totalmente inútil. O melhor que você pode fazer é afirmar que o savePersonmétodo foi chamado para cada entrada e, se você substituísse o loop por uma inserção em massa, não chamaria mais esse método. Então seu teste seria interrompido. Se você tem algo em mente, estou aberto a isso, mas ainda não o vejo. (E não vendo que era o meu ponto.)
jpmc26
1
@immibis Eu não considero isso um teste útil. Usar o armazenamento de dados falsos não me dá nenhuma confiança de que funcionará com a coisa real. Como sei que meus trabalhos falsos são reais? Prefiro deixar que um conjunto de testes de integração cubra isso. (Eu provavelmente deveria esclarecer que eu quis dizer "qualquer unidade de teste" no meu primeiro comentário aqui.)
jpmc26
1
@immibis Na verdade, estou reconsiderando minha posição. Fiquei cético sobre o valor dos testes de unidade por causa de problemas como esse, mas talvez eu esteja subestimando o valor mesmo se você fingir uma entrada. Eu não sei que os testes de entrada / saída tende a ser muito mais útil do que os testes pesados simulados, mas talvez uma recusa para substituir uma entrada com um falso é realmente parte do problema aqui.
Jpmc26
6

Deve bakeCookies()ser testado? Sim.

Como uma função como essa deve ser testada em unidade, supondo que você pense que deveria? É difícil para mim imaginar qualquer tipo de teste de unidade que não zombe simplesmente de massa, panela e forno e depois afirme que os métodos são chamados.

Na verdade não. Observe atentamente o que a função deve fazer - ela deve definir o ovenobjeto para um estado específico. Observando o código, parece que os estados dos objetos pane doughrealmente não importam muito. Portanto, você deve passar um ovenobjeto (ou zombar dele) e afirmar que ele está em um estado específico no final da chamada da função.

Em outras palavras, você deve afirmar que bakeCookies()assou os cookies .

Para funções muito curtas, os testes de unidade podem parecer pouco mais que tautologia. Mas não se esqueça, seu programa vai durar muito mais do que o tempo que você está empregado escrevendo-o. Essa função pode ou não mudar no futuro.

Os testes de unidade têm duas funções:

  1. Testa que tudo funciona. Esse é o teste de unidade de função menos útil que serve e parece que você parece considerar apenas essa funcionalidade ao fazer a pergunta.

  2. Ele verifica se futuras modificações do programa não quebram a funcionalidade implementada anteriormente. Essa é a função mais útil dos testes de unidade e evita a introdução de erros em programas grandes. É útil na codificação normal ao adicionar recursos ao programa, mas é mais útil na refatoração e otimizações em que os principais algoritmos que implementam o programa são dramaticamente alterados sem alterar o comportamento observável do programa.

Não teste o código dentro da função. Em vez disso, teste se a função faz o que diz que faz. Quando você olha para os testes de unidade dessa maneira (funções de teste, não código), percebe que nunca testa construções de linguagem ou mesmo lógica de aplicativo. Você está testando uma API.

slebetman
fonte
Olá, obrigado pela sua resposta. Você se importaria de olhar para a minha segunda atualização e dar sua opinião sobre como testar a função da unidade nesse exemplo?
Jonah
Acho que isso pode ser eficaz quando você pode usar um forno real ou um forno falso, mas é significativamente menos eficaz com um forno simulado. As zombarias (pelas definições de Meszaros) não têm nenhum estado, exceto um registro dos métodos que foram chamados nelas. Minha experiência é que, quando funções como bakeCookiessão testadas dessa maneira, elas tendem a quebrar durante refatores que não afetariam o comportamento observável do aplicativo.
22416 James_pic #
@James_pic, exatamente. E sim, essa é a definição simulada que estou usando. Então, dado o seu comentário, o que você faz em um caso como esse? Renunciar ao teste? Escreva o teste frágil e repetitivo de implementação, afinal? Algo mais?
Jonah
@Jonah Em geral, examinarei o teste desse componente com testes de integração (descobri que os avisos de que é mais difícil depurar sejam exagerados, possivelmente devido à qualidade das ferramentas modernas) ou tentarei criar um falso semi-convincente.
23416 James_pic
3

O savePeople () deve ser testado em unidade ou esses testes equivalem a testar a construção de linguagem forEach interna?

Sim. Mas você poderia fazer isso de uma maneira que apenas testasse novamente a construção.

O que deve ser observado aqui é como essa função se comporta quando uma savePersonfalha ocorre no meio? Como é que isso funciona?

Esse é o tipo de comportamento sutil que a função fornece que você deve aplicar nos testes de unidade.

Telastyn
fonte
Sim, concordo que condições sutis de erro devem ser testadas, mas não é uma pergunta interessante - a resposta é clara. Daí a razão pela qual afirmei especificamente que, para fins da minha pergunta, savePeoplenão deve ser responsável pelo tratamento de erros. Para esclarecer novamente, supondo que savePeopleseja responsável apenas pela iteração na lista e delegação do salvamento de cada item em outro método, ele ainda deve ser testado?
Jonah
@Jonah: Se você insistir em limitar seu teste de unidade apenas à foreachconstrução, e não a quaisquer condições, efeitos colaterais ou comportamentos fora dela, então você está certo; o novo teste de unidade não é realmente tão interessante.
Robert Harvey
1
@jonah - deve iterar e salvar o maior número possível ou parar por erro? O único salvamento não pode decidir isso, pois não pode saber como está sendo usado.
Telastyn
1
@jonah - bem-vindo ao site. Um dos principais componentes do nosso formato de perguntas e respostas é que não estamos aqui para ajudá- lo . Sua pergunta ajuda, é claro, mas também ajuda muitas outras pessoas que acessam o site à procura de respostas para suas perguntas. Eu respondi a pergunta que você fez. Não é minha culpa se você não gosta da resposta ou prefere mudar os postes. E, francamente, parece que as outras respostas dizem a mesma coisa básica, embora com mais eloquência.
Telastyn
1
@ Telastyn, estou tentando obter informações sobre o teste de unidade. Como minha pergunta inicial não era clara o suficiente, estou adicionando esclarecimentos para direcionar a conversa para a minha pergunta real . Você está escolhendo interpretar isso como eu, de alguma forma, enganando você no jogo de "estar certo". Passei centenas de horas respondendo perguntas sobre revisão de código e SO. Meu objetivo é sempre ajudar as pessoas que estou atendendo. Se o seu não for, é sua escolha. Você não precisa responder minhas perguntas.
Jonah
3

A chave aqui é a sua perspectiva sobre uma função específica como trivial. A maior parte da programação é trivial: atribua um valor, faça algumas contas, tome uma decisão: se isso acontecer, continue um loop até ... Isoladamente, tudo trivial. Você acabou de ler os 5 primeiros capítulos de qualquer livro que ensinam uma linguagem de programação.

O fato de escrever um teste ser tão fácil deve ser um sinal de que seu design não é tão ruim assim. Você prefere um design que não seja fácil de testar?

"Isso nunca vai mudar." é assim que a maioria dos projetos com falha começa. Um teste de unidade determina apenas se a unidade funciona conforme o esperado em um determinado conjunto de circunstâncias. Faça-o passar e, em seguida, você poderá esquecer os detalhes de implementação e apenas usá-lo. Use esse espaço cerebral para a próxima tarefa.

Saber que as coisas funcionam como esperado é muito importante e não é trivial em grandes projetos e, especialmente, em grandes equipes. Se há algo que os programadores têm em comum, é o fato de todos termos lidado com o terrível código de outra pessoa. O mínimo que podemos fazer é fazer alguns testes. Em caso de dúvida, escreva um teste e siga em frente.

JeffO
fonte
Obrigado pelo seu feedback. Bons pontos. A pergunta que realmente quero responder (acabei de adicionar mais uma atualização para esclarecer) é a maneira apropriada de testar funções que nada mais fazem do que chamar uma sequência de outros serviços por meio de delegação. Nesses casos, parece que os testes de unidade apropriados para "documentar o contrato" são apenas uma reafirmação da implementação da função, afirmando que os métodos são chamados em várias simulações. No entanto, o teste ser idêntica à implementação nesses casos parece errado ....
Jonah
1

O savePeople () deve ser testado em unidade ou esses testes equivalem a testar a construção de linguagem forEach interna?

Isso já foi respondido por @BryanOakley, mas eu tenho alguns argumentos extras (eu acho):

Primeiro, um teste de unidade é para testar o cumprimento de um contrato, não a implementação de uma API; o teste deve definir pré-condições, ligar e, em seguida, verificar efeitos, efeitos colaterais, invariáveis ​​e pós-condições. Quando você decide o que testar, a implementação da API não importa (e não deve) .

Segundo, seu teste estará lá para verificar os invariantes quando a função mudar . O fato de não mudar agora não significa que você não deva fazer o teste.

Terceiro, é importante ter implementado um teste trivial, tanto em uma abordagem de TDD (que o exige) quanto fora dela.

Ao escrever C ++, para minhas aulas, costumo escrever um teste trivial que instancia um objeto e verifica invariantes (atribuíveis, regulares etc.). Achei surpreendente quantas vezes esse teste foi interrompido durante o desenvolvimento (por exemplo - adicionando um membro não móvel em uma classe, por engano).

utnapistim
fonte
1

Acho que sua pergunta se resume a:

Como teste unitário uma função nula sem que seja um teste de integração?

Se alterarmos a função de assar cookies para retornar cookies, por exemplo, torna-se imediatamente óbvio qual deve ser o teste.

Se precisarmos chamar pan.GetCookies depois de chamar a função, podemos questionar se é realmente um teste de integração ou se é apenas um teste de objeto pan.

Eu acho que você está certo em ter testes de unidade com tudo zombado e apenas verificar as funções xy e z foram chamadas de "falta de valor".

Mas! Eu diria que, nesse caso, você deve refatorar suas funções nulas para retornar um resultado testável OU usar objetos reais e fazer um teste de integração

--- Atualização para o exemplo createNewUser

  • um novo registro de usuário precisa ser criado no banco de dados
  • um email de boas-vindas precisa ser enviado
  • o endereço IP do usuário precisa ser registrado para fins de fraude.

OK, desta vez o resultado da função não é retornado facilmente. Queremos mudar o estado dos parâmetros.

É aqui que fico um pouco controverso. Crio implementações simuladas concretas para os parâmetros com estado

por favor, queridos leitores, tentem controlar sua raiva!

tão...

var validatedUserData = new UserData(); //we can use the real object for this
var emailService = new MockEmailService(); //a simple mock which saves sentEmails to a List<string>
var dataStore = new MockDataStore(); //a simple mock which saves ips to a List<string>

//run the test
target.createNewUser(validatedUserData, emailService, dataStore);

//check the results
Assert.AreEqual(1, emailService.EmailsSent.Count());
Assert.AreEqual(1, dataStore.IpsRecorded.Count());
Assert.AreEqual(1, dataStore.UsersSaved.Count());

Isso separa os detalhes de implementação do método em teste do comportamento desejado. Uma implementação alternativa:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.bulkInsedrtUserRecords(new [] {validateduserData});
  emailService.addEmailToQueue(validatedUserData);
  emailService.ProcessQueue();
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Ainda passará no teste de unidade. Além disso, você tem a vantagem de poder reutilizar os objetos simulados nos testes e também injetá-los em seu aplicativo para testes de interface do usuário ou integração.

Ewan
fonte
não é um teste de integração simplesmente porque você menciona os nomes de duas classes concretas ... os testes de integração são sobre testes de integrações com sistemas externos, como IO de disco, banco de dados, serviços da Web externos e assim por diante. -memory, rápido, verifica o que nos interessa, etc. Concordo que, com o método retornando os cookies, parece um design melhor.
Sara
3
Esperar. Para todos nós sabemos pan.getcookies envia um email para um cozinheiro pedindo-lhes para levar os biscoitos do forno quando receber uma chance
Ewan
Eu acho que é teoricamente possível, mas seria um nome bastante enganador. quem já ouviu falar de equipamentos de forno que enviaram e-mails? mas eu entendo você, depende. Suponho que essas classes colaboradoras sejam objetos de folha ou apenas coisas simples na memória, mas se eles fizerem coisas sorrateiras, será necessário cuidado. Acho que o envio de e-mails definitivamente deve ser feito em um nível mais alto do que isso. isso parece ser a lógica comercial descendente e suja das entidades.
Sara
2
Era uma pergunta retórica, mas: "quem já ouviu falar em equipamentos de forno que enviavam e-mails?" Você está aqui
Página Inicial
Oi Ewan, acho que essa resposta está chegando mais perto do que realmente estou perguntando. Acho que o seu ponto de bakeCookiesdevolução dos biscoitos está no ponto certo, e pensei um pouco depois de publicá-lo. Então eu acho que mais uma vez não é um ótimo exemplo. Adicionei mais uma atualização que, esperançosamente, fornece um exemplo mais realista do que está motivando minha pergunta. Agradeço sua opinião.
Jonah
0

Você também deve testar bakeCookies- o que deveria / deveria e..g bakeCookies(egg, pan, oven)render? Ovo frito ou uma exceção? Por si só, pannem ovense importam com os ingredientes reais, pois nenhum deles deveria, mas bakeCookiesgeralmente deve produzir cookies. De modo mais geral, pode depender da forma como doughé obtido e se existe é alguma chance de se tornar mera eggou, por exemplo, waterem vez disso.

Tobias Kienzler
fonte