até onde eu entendo, a maioria das pessoas parece concordar que os métodos privados não devem ser testados diretamente, mas através de métodos públicos. Eu entendo o argumento deles, mas tenho alguns problemas com isso quando tento seguir as "Três Leis do TDD" e usar o ciclo "Vermelho - verde - refatorador". Eu acho que é melhor explicado por um exemplo:
No momento, preciso de um programa que possa ler um arquivo (contendo dados separados por tabulações) e filtrar todas as colunas que contêm dados não numéricos. Acho que provavelmente já existem algumas ferramentas simples disponíveis para fazer isso, mas decidi implementá-lo do zero, principalmente porque achei que poderia ser um projeto agradável e limpo para eu praticar um pouco com o TDD.
Então, primeiro, "coloco o chapéu vermelho", ou seja, preciso de um teste que falhe. Imaginei, precisarei de um método que encontre todos os campos não numéricos em uma linha. Então, eu escrevo um teste simples, é claro que ele falha na compilação imediatamente, então começo a escrever a função em si e, depois de alguns ciclos para frente e para trás (vermelho / verde), tenho uma função de trabalho e um teste completo.
Em seguida, continuo com uma função "gatherNonNumericColumns" que lê o arquivo, uma linha de cada vez, e chama minha função "findNonNumericFields" em cada linha para reunir todas as colunas que eventualmente devem ser removidas. Um par de ciclos vermelho-verde, e eu terminei, tendo novamente uma função de trabalho e um teste completo.
Agora, acho que devo refatorar. Como meu método "findNonNumericFields" foi projetado apenas porque achei que seria necessário ao implementar "gatherNonNumericColumns", parece-me que seria razoável permitir que "findNonNumericFields" se tornasse privado. No entanto, isso interromperia meus primeiros testes, pois eles não teriam mais acesso ao método que estavam testando.
Então, acabo com métodos particulares e um conjunto de testes que o testam. Como muitas pessoas aconselham que métodos privados não devem ser testados, parece que eu me pintei de canto aqui. Mas onde exatamente eu falhei?
Acho que poderia ter começado em um nível mais alto, escrevendo um teste que testa o que acabará se tornando meu método público (ou seja, findAndFilterOutAllNonNumericalColumns), mas isso parece um pouco contrário ao ponto inteiro do TDD (pelo menos de acordo com o tio Bob) : Que você deve alternar constantemente entre escrever testes e código de produção e que, a qualquer momento, todos os seus testes funcionaram no último minuto ou mais. Porque se eu começar escrevendo um teste para um método público, haverá vários minutos (ou horas ou mesmo dias em casos muito complexos) antes que eu obtenha todos os detalhes nos métodos privados para que o teste teste o público método passa.
Então o que fazer? O TDD (com o rápido ciclo de refator vermelho-verde) simplesmente não é compatível com métodos particulares? Ou há uma falha no meu design?
fonte
private
se fizesse sentido.Respostas:
Unidades
Acho que posso identificar exatamente onde o problema começou:
Isso deve ser seguido imediatamente com a pergunta "Será uma unidade testável separada
gatherNonNumericColumns
ou parte da mesma?"Se a resposta for " sim, separe ", seu curso de ação será simples: esse método precisa ser público em uma classe apropriada, para que possa ser testado como uma unidade. Sua mentalidade é algo como "Eu preciso testar um método e também preciso testar outro método"
Pelo que você diz, você achou que a resposta é " não, faz parte da mesma ". Nesse ponto, seu plano não deve mais ser o de escrever e testar completamente e
findNonNumericFields
depois escrevergatherNonNumericColumns
. Em vez disso, deve ser simplesmente escrevergatherNonNumericColumns
. Por enquanto,findNonNumericFields
deve ser apenas uma parte provável do destino que você tem em mente ao escolher seu próximo caso de teste vermelho e refatorar. Desta vez, sua mentalidade é "Preciso testar um método e, enquanto faço isso, devo ter em mente que minha implementação final provavelmente incluirá esse outro método".Mantendo um ciclo curto
O procedimento acima não deve levar aos problemas que você descreve em seu penúltimo parágrafo:
Em nenhum momento, essa técnica exige que você escreva um teste vermelho que só ficará verde quando você implementar a totalidade
findNonNumericFields
do zero. Muito provavelmente,findNonNumericFields
começará como um código em linha no método público que você está testando, que será construído ao longo de vários ciclos e eventualmente extraído durante uma refatoração.Roteiro
Para fornecer um roteiro aproximado para este exemplo em particular, não conheço os casos de teste exatos que você usou, mas diga que estava escrevendo
gatherNonNumericColumns
como seu método público. Então provavelmente os casos de teste seriam os mesmos para os quais você escreveufindNonNumericFields
, cada um usando uma tabela com apenas uma linha. Quando esse cenário de uma linha foi totalmente implementado e você queria escrever um teste para forçar a extração do método, você escreveria um caso de duas linhas que exigiria a adição de sua iteração.fonte
Muitas pessoas pensam que o teste de unidade é baseado em métodos; não é. Deve ser baseado na menor unidade que faz sentido. Para a maioria das coisas, isso significa que a classe é o que você deve testar como uma entidade inteira. Não métodos individuais nele.
Agora, obviamente, você chamará métodos na classe, mas você deve pensar nos testes como aplicando-se ao objeto de caixa preta que possui, para poder ver que quaisquer operações lógicas que sua classe ofereça; estas são as coisas que você precisa testar. Se sua classe é tão grande que a operação lógica é muito complexa, você tem um problema de design que deve ser corrigido primeiro.
Uma classe com mil métodos pode parecer testável, mas se você testar apenas cada método individualmente, não estará realmente testando a classe. Algumas classes podem precisar estar em um determinado estado antes que um método seja chamado, por exemplo, uma classe de rede que precisa de uma conexão configurada antes de enviar dados. O método de envio de dados não pode ser considerado independentemente de toda a classe.
Portanto, você deve ver que os métodos privados são irrelevantes para o teste. Se você não pode exercitar seus métodos particulares chamando a interface pública de sua classe, esses métodos privados são inúteis e não serão usados de qualquer maneira.
Acho que muitas pessoas tentam transformar métodos privados em unidades testáveis porque parece fácil executar testes para elas, mas isso leva a granularidade do teste muito longe. Martin Fowler diz
o que faz muito sentido para um sistema orientado a objetos, objetos sendo projetados para serem unidades. Se você quiser testar métodos individuais, talvez esteja criando um sistema procedural como C ou uma classe composta inteiramente de funções estáticas.
fonte
O fato de seus métodos de coleta de dados serem suficientemente complexos para merecer testes e se separarem o suficiente de seu objetivo principal para serem métodos próprios, e não parte de alguns pontos de loop da solução: torne esses métodos não privados, mas membros de alguma outra classe que fornece funcionalidade de coleta / filtragem / tabulação.
Em seguida, você escreve testes para os aspectos estúpidos de troca de dados da classe auxiliar (por exemplo, "distinguir números de caracteres") em um local e testa seu objetivo principal (por exemplo, "obter os números de vendas") em outro local, e você não é necessário repetir testes básicos de filtragem nos testes para sua lógica comercial normal.
Geralmente, se sua classe que faz uma coisa contém código extenso para fazer outra coisa necessária, mas separada de seu objetivo principal, esse código deve viver em outra classe e ser chamado por métodos públicos. Ele não deve estar oculto nos cantos particulares de uma classe que contém acidentalmente esse código. Isso melhora a testabilidade e a compreensão ao mesmo tempo.
fonte
Pessoalmente, sinto que você foi muito longe na mentalidade de implementação quando escreveu os testes. Você assumiu que precisaria de certos métodos. Mas você realmente precisa que eles façam o que a classe deve fazer? A turma falharia se alguém aparecesse e os refatorasse internamente? Se você estivesse usando a classe (e essa deveria ser a mentalidade do testador na minha opinião), você realmente se importaria menos se houver um método explícito para verificar números.
Você deve testar a interface pública de uma classe. A implementação privada é privada por um motivo. Não faz parte da interface pública porque não é necessário e pode mudar. É um detalhe de implementação.
Se você escrever testes na interface pública, nunca encontrará o problema que encontrou. Você pode criar casos de teste para a interface pública que abrangem seus métodos particulares (ótimo) ou não é possível. Nesse caso, talvez seja hora de pensar muito sobre os métodos privados e talvez descartá-los completamente, se não puderem ser alcançados.
fonte
Você não faz TDD com base no que espera que a classe faça internamente.
Seus casos de teste devem se basear no que a classe / funcionalidade / programa tem que fazer com o mundo externo. No seu exemplo, o usuário chamará sua classe de leitor com para
find all the non-numerical fields in a line?
Se a resposta for "não", é um teste ruim para escrever em primeiro lugar. Você deseja escrever o teste sobre a funcionalidade no nível de classe / interface - e não no nível "o que o método de classe precisará implementar para que isso funcione", que é o seu teste.
O fluxo do TDD é:
NÃO é necessário "porque precisarei do X no futuro como método privado, deixe-me implementá-lo e testá-lo primeiro". Se você estiver fazendo isso, está fazendo o estágio "vermelho" incorretamente. Esse parece ser o seu problema aqui.
Se você se encontra frequentemente escrevendo testes para métodos que se tornam métodos privados, está fazendo uma de algumas coisas:
fonte
Você está encontrando um equívoco comum com os testes em geral.
A maioria das pessoas que são novas nos testes começa a pensar assim:
e assim por diante.
O problema aqui é que, de fato, você não possui um teste de unidade para a função H. O teste que deveria testar H, na verdade, está testando H, G e F ao mesmo tempo.
Para resolver isso, você deve perceber que as unidades testáveis nunca devem depender umas das outras, mas de suas interfaces . No seu caso, onde as unidades são funções simples, as interfaces são apenas sua assinatura de chamada. Portanto, você deve implementar G de tal maneira que possa ser usado com qualquer função com a mesma assinatura que F.
Como exatamente isso pode ser feito depende da sua linguagem de programação. Em muitos idiomas, você pode passar funções (ou ponteiros para eles) como argumentos para outras funções. Isso permitirá que você teste cada função isoladamente.
fonte
Os testes que você escreve durante o Test Driven Development devem garantir que uma classe implemente corretamente sua API pública, enquanto simultaneamente garantem que essa API pública seja fácil de testar e usar.
Você pode, por todos os meios, usar métodos privados para implementar essa API, mas não há necessidade de criar testes por meio do TDD - a funcionalidade será testada porque a API pública funcionará corretamente.
Agora, suponha que seus métodos privados sejam complicados o suficiente para merecer testes independentes - mas não fazem sentido como parte da API pública da sua classe original. Bem, isso provavelmente significa que eles realmente devem ser métodos públicos em alguma outra classe - uma que sua classe original aproveita em sua própria implementação.
Ao testar apenas a API pública, você está facilitando muito a modificação dos detalhes da implementação no futuro. Os testes inúteis só o incomodam mais tarde quando precisam ser reescritos para oferecer suporte a alguma refatoração elegante que você acabou de descobrir.
fonte
Eu acho que a resposta certa é a conclusão que você chegou sobre o início dos métodos públicos. Você começaria escrevendo um teste que chama esse método. Falha na criação de um método com esse nome que não faz nada. Então você pode corrigir um teste que verifica se há um valor de retorno.
(Não estou totalmente claro quanto ao que sua função faz. Ele retorna uma sequência com o conteúdo do arquivo com os valores não numéricos eliminados?)
Se o seu método retornar uma string, você verificará esse valor de retorno. Então você apenas continua construindo.
Eu acho que tudo o que acontece em um método privado deve estar no método público em algum momento durante o processo e, em seguida, somente foi movido para o método privado como parte de uma etapa de refatoração. A refatoração não exige testes com falha, até onde eu sei. Você só precisa falhar nos testes ao adicionar funcionalidade. Você só precisa executar seus testes após o refator para garantir que todos sejam aprovados.
fonte
Há um velho ditado.
As pessoas parecem pensar que, quando você faz TDD, você apenas senta, escreve testes e o design simplesmente acontece magicamente. Isso não é verdade. Você precisa ter um plano de alto nível. Descobri que obtive meus melhores resultados com o TDD ao projetar a interface (API pública) primeiro. Pessoalmente, crio um real
interface
que define a classe primeiro.suspiro Eu escrevi um "código" antes de escrever qualquer teste! Bem não. Eu não fiz. Escrevi um contrato a ser seguido, um design . Suspeito que você possa obter resultados semelhantes anotando um diagrama UML em papel milimétrico. O ponto é que você deve ter um plano. O TDD não é uma licença para piratear um pedaço de código.
Eu realmente sinto que "Test First" é um nome impróprio. Design Primeiro , teste.
Obviamente, siga os conselhos de outras pessoas sobre como extrair mais classes do seu código. Se você sentir fortemente a necessidade de testar os internos de uma classe, extraia esses internos em uma unidade facilmente testada e injete-a.
fonte
Lembre-se de que os testes também podem ser refatorados! Se você tornar um método privado, estará reduzindo a API pública e, portanto, é perfeitamente aceitável jogar fora alguns testes correspondentes para essa "funcionalidade perdida" (AKA reduziu a complexidade).
Outros disseram que seu método privado será chamado como parte de outros testes de API ou será inacessível e, portanto, deletável. De fato, as coisas são mais refinadas se pensarmos nos caminhos de execução .
Por exemplo, se tivermos um método público que realiza divisão, podemos querer testar o caminho que resulta em divisão por zero. Se tornarmos o método privado, teremos uma escolha: podemos considerar o caminho da divisão por zero ou podemos eliminá-lo, considerando como é chamado pelos outros métodos.
Dessa forma, podemos jogar fora alguns testes (por exemplo, dividir por zero) e refatorar os outros em termos da API pública restante. Obviamente, em um mundo ideal, os testes existentes cuidam de todos os caminhos restantes, mas a realidade é sempre um compromisso;)
fonte
Há momentos em que um método privado pode se tornar um método público de outra classe.
Por exemplo, você pode ter métodos particulares que não são seguros para threads e deixar a classe em um estado temporário. Esses métodos podem ser movidos para uma classe separada, que é realizada em particular pela sua primeira classe. Portanto, se sua classe for uma Fila, você poderá ter uma classe InternalQueue com métodos públicos, e a classe Queue manterá a instância InternalQueue em particular. Isso permite testar a fila interna e também esclarece quais são as operações individuais no InternalQueue.
(Isso é mais óbvio quando você imagina que não havia classe List e se você tentou implementar as funções List como métodos privados na classe que os utiliza.)
fonte
Eu me pergunto por que seu idioma tem apenas dois níveis de privacidade, totalmente público e completamente privado.
Você pode organizar seus métodos não públicos como acessíveis por pacotes ou algo assim? Em seguida, coloque seus testes no mesmo pacote e aproveite para testar o funcionamento interno que não faz parte da interface pública. Seu sistema de compilação excluirá testes ao criar um binário de versão.
É claro que às vezes você precisa ter métodos verdadeiramente privados, não acessíveis a nada além da classe que define. Espero que todos esses métodos sejam muito pequenos. Em geral, manter os métodos pequenos (por exemplo, abaixo de 20 linhas) ajuda muito: testar, manter e apenas entender o código se torna mais fácil.
fonte
Ocasionalmente, troquei métodos privados para protegidos para permitir testes mais refinados (mais rigorosos que a API pública exposta). Essa deve ser a exceção (espero que muito rara) e não a regra, mas pode ser útil em certos casos específicos que você pode encontrar. Além disso, isso é algo que você não gostaria de considerar ao criar uma API pública, mais "truques" que se pode usar no software de uso interno nessas raras situações.
fonte
Eu experimentei isso e senti sua dor.
Minha solução foi:
pare de tratar testes como construir um monólito.
Lembre-se de que quando você escreve um conjunto de testes, digamos 5, para eliminar algumas funcionalidades, não precisa manter todos esses testes por perto , especialmente quando isso se torna parte de outra coisa.
Por exemplo, muitas vezes tenho:
então eu tenho
No entanto, se agora adicionar funções de nível superior que a chamam, com muitos testes, agora posso reduzir os testes de baixo nível para:
O diabo está nos detalhes e a capacidade de fazer isso dependerá das circunstâncias.
fonte
O sol gira em torno da terra ou a terra em torno do sol? De acordo com Einstein, a resposta é sim, ou ambos, como os dois modelos diferem apenas pelo ponto de vista, da mesma forma o encapsulamento e o desenvolvimento orientado a testes estão apenas em conflito porque pensamos que estão. Ficamos sentados aqui, como Galileu e o papa, lançando insultos uns aos outros: tolo, você não vê que métodos privados também precisam ser testados; herege, não quebre o encapsulamento! Da mesma forma, quando reconhecemos que a verdade é maior do que pensávamos, podemos tentar algo como encapsular os testes para as interfaces privadas, para que os testes para as interfaces públicas não quebrem o encapsulamento.
Tente o seguinte: adicione dois métodos, um que não tem entrada, mas apenas retorna o número de testes particulares e outro que aceita um número de teste como parâmetro e retorna aprovado / reprovado.
fonte