TDD Vermelho-Verde-Refator e se / como testar métodos que se tornam privados

91

até onde eu entendo, a maioria das pessoas parece concordar que os métodos privados não devem ser testados diretamente, mas sim através de quaisquer métodos públicos que os chamem. Eu posso ver o ponto deles, mas eu tenho alguns problemas com isso quando tento seguir as "Três Leis de TDD", e uso o ciclo "Refator - vermelho - verde". Eu acho que é melhor explicado por um exemplo:

Neste 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. Eu acho que provavelmente há algumas ferramentas simples disponíveis para fazer isso, mas eu decidi implementá-lo a partir do zero, principalmente porque eu achei que poderia ser um projeto bom e limpo para eu praticar um pouco com o TDD.

Primeiramente, eu coloco o "red hat on", ou seja, preciso de um teste que falhe. Eu imaginei que 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 em compilar imediatamente, então eu começo a escrever a função em si, e depois de alguns ciclos para trás e para frente (vermelho / verde) eu tenho uma função de trabalho e um teste completo. >

Em seguida, continuo com uma função, "gatherNonNumericColumns" que lê o arquivo, uma linha por vez, e chama minha função "findNonNumericFields" em cada linha para reunir todas as colunas que devem ser removidas. Um par de ciclos vermelho-verde, e estou pronto, tendo novamente uma função de trabalho e um teste completo.

Agora, acho que devo refatorar. Como meu método "findNonNumericFields" foi criado apenas porque imaginei que precisaria dele ao implementar "gatherNonNumericColumns", parece-me que seria razoável deixar "findNonNumericFields" tornar-se privado. No entanto, isso quebraria meus primeiros testes, pois eles não teriam mais acesso ao método que estavam testando.

Então, acabo com um método privado e um conjunto de testes que o testam. Uma vez que muitas pessoas aconselham que métodos privados não sejam testados, parece que eu me pintei em um canto aqui. Mas onde exatamente eu falhei?

Eu percebi que poderia ter começado em um nível mais alto, escrevendo um teste que acabaria se tornando meu método público (isto é, findAndFilterOutAllNonNumericalColumns), mas isso se contrapõe ao ponto de TDD (pelo menos de acordo com Tio Bob): Você deve alternar constantemente entre escrever testes e código de produção e, a qualquer momento, todos os seus testes funcionaram no último minuto. Porque se eu começar escrevendo um teste para um método público, haverá vários minutos (ou horas, ou até mesmo dias em casos muito complexos) antes de obter todos os detalhes nos métodos privados para trabalhar de forma que o teste teste o público método passa.

Então, o que fazer? O TDD (com o ciclo refator vermelho-verde rápido) simplesmente não é compatível com métodos privados? Ou há alguma falha no meu design?

    
por Henrik Berg 15.04.2015 / 10:51
fonte

15 respostas

42

Unidades

Acho que posso identificar exatamente onde o problema começou:

I figured, I'll need a method that finds all the non-numerical fields in a line.

Isso deve ser imediatamente seguido com a pergunta "Isso será uma unidade testável separada para gatherNonNumericColumns ou parte do mesmo?"

Se a resposta for " sim, separado ", o seu curso de ação é 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ê percebeu que a resposta é " não, parte do mesmo ". Neste ponto, seu plano não deve mais ser escrever e testar completamente findNonNumericFields então escreva gatherNonNumericColumns . Em vez disso, deve ser simplesmente escrever gatherNonNumericColumns . Por enquanto, findNonNumericFields deve ser apenas uma parte provável do destino que você tem em mente quando estiver escolhendo seu próximo caso de teste vermelho e fazendo sua refatoração. 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

Fazer o acima deve não levar aos problemas que você descreve no seu penúltimo parágrafo:

Because if I start out by writing a test for a public method, there will be several minutes (or hours, or even days in very complex cases) before I get all the details in the private methods to work so that the test testing the public method passes.

Em nenhum momento esta técnica exige que você escreva um teste vermelho que só ficará verde quando você implementar a totalidade de findNonNumericFields do zero. É muito mais provável que findNonNumericFields comece como um código in-line no método público que você está testando, que será compilado ao longo de vários ciclos e, eventualmente, extraído durante uma refatoração.

Roteiro

Para fornecer um roteiro aproximado para esse exemplo específico, não sei os casos de teste exatos que você usou, mas diga que você estava escrevendo gatherNonNumericColumns como seu método público. Então, provavelmente, os casos de teste seriam os mesmos que você escreveu para findNonNumericFields , cada um usando uma tabela com apenas uma linha. Quando esse cenário de uma linha foi totalmente implementado e você quis escrever um teste para forçá-lo a extrair o método, você escreveria um caso de duas linhas que exigiria que você adicionasse sua iteração.

    
por 15.04.2015 / 17:01
fonte
66

Muitas pessoas pensam que o teste unitário é baseado em métodos; não é. Deve ser baseado em torno da 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ê estará chamando métodos na classe, mas você deve estar pensando que os testes se aplicam ao objeto de caixa preta que você tem, então você deve ser capaz de ver que quaisquer operações lógicas que sua classe forneça; estas são as coisas que você precisa testar. Se sua classe é tão grande que a operação lógica é muito complexa, então 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 de um método ser chamado, por exemplo, uma classe de rede que precisa de uma configuração de conexão antes de enviar dados. O método de envio de dados não pode ser considerado independentemente de toda a classe.

Então você deve ver que os métodos privados são irrelevantes para o teste. Se você não puder exercitar seus métodos privados chamando a interface pública de sua classe, esses métodos privados serã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 longe demais. Martin Fowler diz

Although I start with the notion of the unit being a class, I often take a bunch of closely related classes and treat them as a single unit

que faz muito sentido para um sistema orientado a objeto, objetos sendo projetados para serem unidades. Se você quiser testar métodos individuais, talvez você deva criar um sistema processual como C, ou uma classe composta inteiramente de funções estáticas.

    
por 15.04.2015 / 11:24
fonte
51

O fato de seus métodos de coleta de dados serem complexos o suficiente para merecer testes e separam-se o suficiente do seu objetivo principal de serem métodos próprios, em vez de fazer parte de alguns pontos de loop: métodos não privados, mas membros de alguma outra outra classe que fornece funcionalidade de coleta / filtragem / tabulação.

Em seguida, você escreve testes para os aspectos burros de dados da classe auxiliar (por exemplo, "distinguindo números de caracteres") em um local e testa seu objetivo principal (por exemplo, "obter os números de vendas") em outro lugar. e você não precisa repetir os testes básicos de filtragem nos testes para sua lógica comercial normal.

Geralmente, se a sua classe que faz uma coisa contém código extenso para fazer outra coisa que é necessária, mas separada da sua finalidade principal, esse código deve viver em outra classe e ser chamado através de métodos públicos. Ele não deve estar oculto em cantos privados de uma classe que apenas acidentalmente contém esse código. Isso melhora a testabilidade e a compreensibilidade ao mesmo tempo.

    
por 15.04.2015 / 10:58
fonte
29

Pessoalmente, sinto que você foi muito longe na mentalidade de implementação quando escreveu os testes. Você assumiu que você precisaria de certos métodos. Mas você realmente precisa deles para fazer o que a classe deve fazer? A aula falharia se alguém aparecesse e as refatorasse internamente? Se você estivesse usando a classe (e essa deveria ser a mentalidade do testador em minha opinião), você poderia se importar menos se houvesse 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 contra a interface pública, você nunca encontrará o problema que encontrou. Você pode criar casos de teste para a interface pública que cobrem seus métodos privados (ótimo) ou não. Nesse caso, talvez seja hora de pensar seriamente sobre os métodos privados e, talvez, descartá-los se não puderem ser encontrados de qualquer maneira.

    
por 15.04.2015 / 11:28
fonte
11

Você não faz o TDD com base no que espera que a turma faça internamente.

Seus casos de teste devem ser baseados no que a classe / funcionalidade / programa precisa fazer para o mundo externo. No seu exemplo, o usuário sempre estará chamando sua classe de leitora com find all the non-numerical fields in a line?

Se a resposta for "não", então é um teste ruim para escrever em primeiro lugar. Você quer escrever o teste na funcionalidade em um nível de classe / interface - não no nível "o que o método de classe terá que implementar para que isso funcione", que é o seu teste. / p>

O fluxo de TDD é:

  • vermelho (o que a classe / objeto / função / etc faz ao mundo externo)
  • verde (escreva o código mínimo para fazer esse trabalho externo funcionar)
  • refatorar (qual é o código melhor para fazer isso funcionar)

NÃO é necessário "porque precisarei do X no futuro como um método privado, deixe-me implementar e teste primeiro. "Se você está fazendo isso, você está fazendo o estágio" vermelho "incorretamente. Este parece ser o seu problema aqui.

Se você está frequentemente escrevendo testes para métodos que se tornam métodos privados, você está fazendo uma das poucas coisas:

  • Não entender corretamente seus casos de uso de interface / nível público o suficiente para escrever um teste para eles
  • Mudando drasticamente seu design e refatorando vários testes (o que pode ser bom, dependendo se essa funcionalidade é testada em testes mais recentes)
por 16.04.2015 / 05:08
fonte
9

Você está encontrando um equívoco comum com os testes em geral.

A maioria das pessoas que são novas nos testes começam a pensar assim:

  • escreva um teste para a função F
  • implementar F
  • escreva um teste para a função G
  • implemente G usando uma chamada para F
  • escreva um teste para uma função H
  • implemente H usando uma chamada para G

e assim por diante.

O problema aqui é que você de fato não tem teste unitário para a função H. O teste que deve testar H está testando H, G e F ao mesmo tempo.

Para resolver isso, você tem que perceber que as unidades testáveis nunca devem depender umas das outras, mas sim de suas interfaces . No seu caso, onde as unidades são funções simples, as interfaces são apenas sua assinatura de chamada. Você deve, portanto, implementar G de tal forma que ele possa ser usado com qualquer qualquer função que tenha a mesma assinatura de 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 elas) como argumentos para outras funções. Isso permitirá que você teste cada função isoladamente.

    
por 15.04.2015 / 17:53
fonte
8

Os testes que você escreve durante o Desenvolvimento Orientado a Testes devem garantir que uma classe implemente corretamente sua API pública, garantindo ao mesmo tempo 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 que eles mereçam testes independentes - mas não fazem sentido como parte da API pública da classe original. Bem, isso provavelmente significa que eles deveriam ser métodos públicos em alguma outra classe - uma que sua classe original aproveita em sua própria implementação.

Testando apenas a API pública, você está facilitando muito a modificação dos detalhes da implementação no futuro. Testes inúteis só vão aborrecê-lo mais tarde, quando precisarem ser reescritos para apoiar alguma refatoração elegante que você acabou de descobrir.

    
por 15.04.2015 / 19:49
fonte
4

Acho que a resposta certa é a conclusão que você chegou sobre os métodos públicos. Você começaria escrevendo um teste que chama esse método. Ele falharia, então você cria um método com esse nome que não faz nada. Então você pode corrigir um teste que verifica um valor de retorno.

(Eu não estou totalmente claro sobre o que sua função faz. Ela retorna uma string com o conteúdo do arquivo com os valores não numéricos removidos?)

Se o seu método retornar uma string, você verificará esse valor de retorno. Então você apenas continua a construir.

Acho que tudo o que acontece em um método particular deve estar no método público em algum momento durante o processo e, depois, ser movido para o método privado como parte de uma etapa de refatoração. A refatoração não requer testes com falha, até onde eu sei. Você só precisa de testes de falha ao adicionar funcionalidade. Você só precisa executar seus testes após o refatorador para garantir que eles passem.

    
por 16.04.2015 / 00:04
fonte
3

it feels like I've painted myself into a corner here. But where exactly did I fail?

Há um velho ditado.

When you fail to plan, you plan to fail.

As pessoas parecem pensar que quando você TDD, você apenas se senta, escreve testes, e o design só vai acontecer magicamente. Isso não é verdade. Você precisa ter um plano de alto nível. Descobri que obtive meus melhores resultados com o TDD ao criar a interface (API pública) primeiro. Pessoalmente, eu crio um interface real que define a classe primeiro.

gasp Eu escrevi alguns "códigos" antes de escrever qualquer teste! Bem não. Eu não fiz. Eu escrevi um contrato para ser seguido, um design . Eu suspeito que você poderia obter resultados semelhantes anotando um diagrama UML em papel milimetrado. O ponto é, você deve ter um plano. O TDD não é uma licença para ir, por acaso, hackear um pedaço de código.

Eu realmente sinto que "Teste primeiro" é um equívoco. Teste de Design First e .

Claro, por favor, siga o conselho que outros deram sobre extrair mais classes do seu código. Se você sente a necessidade de testar os componentes internos de uma classe, extraia os componentes internos em uma unidade facilmente testável e injete-a.

    
por 17.04.2015 / 03:37
fonte
2

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 descartar alguns testes correspondentes para essa "funcionalidade perdida" (complexidade reduzida da AKA).

Outros disseram que seu método privado será chamado como parte de seus outros testes de API ou será inacessível e, portanto, excluído. Na verdade, as coisas são mais refinadas se pensarmos em caminhos de execução .

Por exemplo, se tivermos um método público que executa a divisão, poderemos querer testar o caminho que resulta em divisão por zero. Se tornarmos o método privado, teremos uma escolha: ou podemos considerar o caminho de divisão por zero, ou podemos eliminar esse caminho considerando como ele é chamado pelos outros métodos.

Desta forma, podemos descartar alguns testes (por exemplo, dividir por zero) e refatorar os outros em termos da API pública restante. É claro que, em um mundo ideal, os testes existentes cuidam de todos os caminhos remanescentes, mas a realidade é sempre um compromisso;)

    
por 15.04.2015 / 16:33
fonte
2

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 privados que não sejam thread-safe e deixar a classe em um estado temporário. Esses métodos podem ser movidos para uma classe separada que é mantida em particular pela sua primeira turma. Portanto, se sua classe for uma Fila, você poderá ter uma classe InternalQueue que tenha métodos públicos e a classe Queue manterá a instância InternalQueue de forma privada. Isso permite testar a fila interna e também deixa claro quais são as operações individuais no InternalQueue.

(Isso é mais óbvio quando você imagina que não existe uma classe List e se você tentou implementar as funções List como métodos privados na classe que as utiliza.)

    
por 15.04.2015 / 20:49
fonte
0

Eu me pergunto por que sua linguagem tem apenas dois níveis de privacidade, totalmente públicos e totalmente privados.

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 construção excluirá testes ao criar um binário de lançamento.

É claro que às vezes você precisa ter métodos verdadeiramente privados, não acessíveis a qualquer coisa, exceto à classe definidora. Espero que todos esses métodos sejam muito pequenos. Em geral, manter os métodos pequenos (por exemplo, abaixo de 20 linhas) ajuda muito: o teste, a manutenção e o simples entendimento do código se tornam mais fáceis.

    
por 15.04.2015 / 17:01
fonte
0

Ocasionalmente, optei por métodos privados para protegidos para permitir testes mais precisos (mais rígidos do que a API pública exposta). Esta deve ser a exceção (esperançosamente muito rara) em vez da 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 uma "trapaça" que pode ser usada em softwares de uso interno nessas situações raras.

    
por 15.04.2015 / 18:49
fonte
0

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ê tiver escrito um conjunto de testes, digamos 5, para expulsar algumas funcionalidades, você não precisará manter todos esses testes , especialmente quando isso se tornar parte de outra coisa.

Por exemplo, muitas vezes tenho:

  • teste de baixo nível 1
  • código para atendê-lo
  • teste de baixo nível 2
  • código para atendê-lo
  • teste de baixo nível 3
  • código para atendê-lo
  • teste de baixo nível 4
  • código para atendê-lo
  • teste de baixo nível 5
  • código para atendê-lo

então eu tenho

  • teste de baixo nível 1
  • teste de baixo nível 2
  • teste de baixo nível 3
  • teste de baixo nível 4
  • teste de baixo nível 5

No entanto, se eu adicionar funções de nível mais alto que o chamam, que têm muitos testes, eu posso ser capaz de reduzir os testes de baixo nível para apenas:

  • teste de baixo nível 1
  • teste de baixo nível 5

O diabo está nos detalhes e a capacidade de fazer isso dependerá das circunstâncias.

    
por 19.04.2015 / 13:27
fonte
-2

O sol gira em torno da terra ou da terra ao redor do sol? De acordo com Einstein, a resposta é sim, ou ambos, já que ambos os modelos diferem apenas pelo ponto de vista, da mesma forma que o encapsulamento e o desenvolvimento orientado a testes estão em conflito apenas porque pensamos que são. Nós nos sentamos aqui como Galileu e o papa, insultando uns aos outros: idiota, você não vê que os métodos privados também precisam ser testados; herege, não quebre o encapsulamento! Da mesma forma, quando reconhecemos que a verdade é mais grandiosa do que pensamos, podemos tentar algo como encapsular os testes para as interfaces privadas, de modo que os testes para as interfaces públicas não quebrem o encapsulamento.

Tente isto: adicione dois métodos, um que não tenha entrada, mas justs retorne o número de testes privados e um que use um número de teste como parâmetro e retorne aprovação / reprovação.

    
por 18.04.2015 / 16:52
fonte