Existe uma razão para que os testes não sejam escritos de acordo com o código que eles testam?


91

Eu tenho lido um pouco sobre programação alfabética recentemente, e isso me fez pensar ... Testes bem escritos, especialmente especificações no estilo BDD, podem fazer um trabalho melhor ao explicar o que o código faz do que a prosa e tem a grande vantagem de verificar sua própria precisão.

Eu nunca vi testes escritos em linha com o código que eles testam. Isso ocorre apenas porque os idiomas não tendem a facilitar a separação do código do aplicativo e do teste quando escritos no mesmo arquivo de origem (e ninguém facilitou), ou existe um motivo mais básico para que as pessoas separem o código de teste do código do aplicativo?


33
Algumas linguagens de programação como python com doctest permitem fazer isso.
Simon Bergot

2
Você pode achar que as especificações no estilo BDD são melhores do que escrever na explicação do código, mas isso não significa que a combinação das duas não seja melhor.
25413 JeffOf

5
Metade dos argumentos aqui também se aplicam à documentação embutida.
CodesInChaos

3
Os documentos do @Simon são muito simplistas para testes sérios, principalmente porque não foram projetados para isso. Eles foram projetados e se destacam em ter exemplos de código na documentação que podem ser verificados automaticamente. Agora, algumas pessoas os usam também para testes de unidade, mas ultimamente (como nos últimos anos) isso exigiu muita atenção, porque tende a terminar em bagunças frágeis, "documentação" excessivamente detalhada e outras bagunças.

7
O Design by Contract permite especificações em linha que tornam o teste simples.
Fuhrmanator

Respostas:


89

A única vantagem que posso pensar nos testes em linha seria reduzir o número de arquivos a serem gravados. Com os IDEs modernos, isso realmente não é grande coisa.

Existem, no entanto, várias desvantagens óbvias nos testes em linha:

  • Isso viola a separação de preocupações . Isso pode ser discutível, mas para mim testar a funcionalidade é uma responsabilidade diferente da implementação.
  • Você precisaria introduzir novos recursos de linguagem para distinguir entre testes / implementação ou correria o risco de desfocar a linha entre os dois.
  • Arquivos de origem maiores são mais difíceis de trabalhar: mais difíceis de ler, mais difíceis de entender, é mais provável que você tenha que lidar com conflitos de controle de origem.
  • Eu acho que seria mais difícil colocar seu chapéu de "testador", por assim dizer. Se você estiver olhando os detalhes da implementação, ficará mais tentado a pular a implementação de certos testes.

9
Isso é interessante. Eu acho que a vantagem que posso ver é que, quando você usa seu chapéu "codificador", deseja pensar em testes, mas é um bom ponto que o inverso não é verdadeiro.
22413 Chris Devereux

2
Nessa linha, é possível (e talvez desejável) ter uma pessoa criando os testes e uma segunda implementando o código. Colocar os testes em linha torna isso mais difícil.
Jim Nutt

6
iria votar se eu pudesse. Como isso é de alguma forma uma resposta? Implementadores que não estão escrevendo testes? Pessoas pulando testes se olharem para os detalhes da implementação? Conflitos "muito difíceis" em arquivos grandes ?? E como de alguma forma um teste poderia ser confundido com um detalhe de implementação ???
bharal

5
@bharal Além disso, escrevendo para "Just hard hard", o masoquismo é uma virtude de tolo. Quero que tudo seja fácil, exceto pelo problema que estou tentando resolver.
deworde

3
O teste de unidade pode ser considerado documentação. Isso sugere que os testes de unidade devem ser incluídos no código pelo mesmo motivo que os comentários - para melhorar a legibilidade. No entanto, o problema disso é que há muitos testes de unidade e muita sobrecarga de implementação de teste que não especifica os resultados esperados. Mesmo os comentários no código devem ser mantidos sucintos, com explicações maiores afastadas - para um bloco de comentários fora da função, para um arquivo separado ou talvez para um documento de design. Os testes de unidade raramente são IMO, se é que são curtos, o suficiente para manter o código testado como comentários.
precisa saber é o seguinte

36

Eu posso pensar em alguns:

  • Legibilidade. A intercalação de códigos e testes "reais" tornará mais difícil a leitura do código real.

  • Código inchaço. Misturar código "real" e código de teste nos mesmos arquivos / classes / o que provavelmente resultar em arquivos compilados maiores, etc. Isso é particularmente importante para idiomas com ligação tardia.

  • Você pode não querer que seus clientes vejam seu código de teste. (Não gosto desse motivo ... mas se você estiver trabalhando em um projeto de código fechado, é improvável que o código de teste ajude o cliente de qualquer maneira.)

Agora, existem soluções possíveis para cada um desses problemas. Mas na OMI, é mais simples não ir para lá em primeiro lugar.


Vale observar que, nos primeiros dias, os programadores Java costumavam fazer esse tipo de coisa; por exemplo, incluindo um main(...)método em uma classe para facilitar o teste. Essa idéia desapareceu quase completamente. É prática da indústria implementar testes separadamente usando algum tipo de estrutura de teste.

Também vale a pena observar que a programação alfabetizada (como concebida por Knuth) nunca se destacou na indústria de engenharia de software.


4
+1 Problemas de legibilidade - o código de teste pode ser proporcionalmente maior que o código de implementação, especialmente em projetos OO.
Fuhrmanator

2
+1 para apontar usando estruturas de teste. Não consigo imaginar usar uma boa estrutura de teste simultaneamente com o código de produção.
Joshin4colours

1
RE: Você pode não querer que seus clientes vejam seu código de teste. (Não gosto desse motivo ... mas se você estiver trabalhando em um projeto de código fechado, é improvável que o código de teste ajude o cliente de qualquer maneira.) - Pode ser desejável executar os testes na máquina do cliente. Execução dos testes pode ajudar a identificar rapidamente qual é o problema e diferenças id no env clientes ..
sixtyfootersdude

1
@sixtyfootersdude - essa é uma situação bastante incomum. E assumindo que você esteja desenvolvendo código-fonte fechado, não gostaria de incluir seus testes em sua distribuição binária padrão, apenas por precaução. (Você iria criar um pacote separado contendo os testes que você deseja que o cliente para ser executado.)
Stephen C

1
1) Você perdeu a primeira parte da minha resposta em que eu dei três razões reais? Havia algum "pensamento crítico" envolvido lá .... 2) Você perdeu a segunda parte em que eu disse que os programadores Java costumavam fazer isso, mas não o fazem agora? E a implicação óbvia de que os programadores pararam de fazer isso ... por um bom motivo?
Stephen C

14

Na verdade, você pode pensar em Design por contrato como fazendo isso. O problema é que a maioria das linguagens de programação não permite escrever códigos como este :( É muito fácil testar pré-condições manualmente, mas as condições pós são um verdadeiro desafio sem alterar a maneira como você escreve um código (um IMO negativo enorme).

Michael Feathers tem uma apresentação sobre isso e esta é uma das muitas maneiras pelas quais ele menciona que você pode melhorar a qualidade do código.


13

Por muitas das mesmas razões pelas quais você tenta evitar um acoplamento rígido entre classes no seu código, também é uma boa idéia evitar um acoplamento desnecessário entre testes e código.

Criação: Testes e código podem ser escritos em momentos diferentes, por pessoas diferentes.

Controle: se testes são usados ​​para especificar requisitos, você certamente deseja que eles estejam sujeitos a regras diferentes sobre quem pode alterá-los e quando é o código real.

Reutilização: se você colocar os testes em linha, não poderá usá-los com outro trecho de código.

Imagine que você tenha um pedaço de código que faz o trabalho corretamente, mas deixa muito a desejar em termos de desempenho, manutenção, qualquer que seja. Você decide substituir esse código por um código novo e aprimorado. Usar o mesmo conjunto de testes pode ajudá-lo a verificar se o novo código produz os mesmos resultados que o código antigo.

Selecionabilidade: manter os testes separados do código facilita a escolha de quais testes você deseja executar.

Por exemplo, você pode ter um pequeno conjunto de testes relacionados apenas ao código em que está trabalhando no momento e um conjunto maior que testa todo o projeto.


Estou intrigado com os seus motivos: o TDD já diz que a criação do teste acontece antes (ou ao mesmo tempo) do código de produção e deve ser feita pelo mesmo codificador! Eles também sugerem que os testes são muito parecidos com os requisitos. Obviamente, essas objeções não se aplicam se você não assinar o dogma do TDD (o que seria aceitável, mas você deve deixar claro!). Além disso, o que exatamente é um teste "reutilizável"? Os testes, por definição, não são específicos ao código que testam?
Andrés F.

1
@AndresF. Não, os testes não são específicos para o código que testam; eles são específicos para o comportamento que testam. Então, digamos que você tenha um módulo Widget completo com um conjunto de testes que verificam se o Widget está se comportando corretamente. Seu colega cria o BetterWidget, que pretende fazer o mesmo que o Widget, mas três vezes mais rápido. Se os testes para o Widget estiverem incorporados no código-fonte do Widget da mesma maneira que a Programação Alfabetizada incorpora a documentação no código-fonte, você não pode aplicar muito bem esses testes ao BetterWidget para verificar se ele se comporta da mesma forma que o Widget.
Caleb

@AndresF. não é necessário especificar que você não segue o TDD. não é um padrão cósmico. Quanto ao ponto de reutilização. Ao testar um sistema, você se preocupa com as entradas e saídas, não com os internos. Quando você precisa criar um novo sistema que se comporte da mesma maneira, mas seja implementado de maneira diferente, é ótimo ter testes que você possa executar no sistema antigo e no novo. isso aconteceu comigo mais de uma vez, às vezes você precisa trabalhar no novo sistema enquanto o antigo ainda está em produção ou até executá-lo lado a lado. veja como o Facebook estava testando 'reagir fibra' com os testes de reação para chegar à paridade.
user1852503

10

Aqui estão algumas razões adicionais em que posso pensar:

  • a realização de testes em uma biblioteca separada facilita a vinculação apenas dessa biblioteca à sua estrutura de teste, e não ao seu código de produção (isso pode ser evitado por algum pré-processador, mas por que criar isso quando a solução mais fácil é gravar os testes no um local separado)

  • testes de uma função, classe e biblioteca são tipicamente escritos do ponto de vista de "usuários" (um usuário dessa função / classe / biblioteca). Esse "uso de código" é tipicamente escrito em um arquivo ou biblioteca separado e um teste pode ser mais claro ou "mais realista" se imitar essa situação.


5

Se os testes estivessem em linha, seria necessário remover o código necessário para o envio ao enviar o produto ao seu cliente. Portanto, um local extra onde você armazena seus testes simplesmente separa o código que você precisa e o código que seu cliente precisa.


9
Não é impossível. Isso exigiria uma fase adicional de pré-processamento, assim como o LP. Isso poderia ser feito facilmente em C, ou em uma linguagem de compilação para js, por exemplo.
22413 Chris Devereux

+1 por apontar isso para mim. Eu editei minha resposta para representar isso.
25413

Há também uma suposição de que o tamanho do código é importante em todos os casos. Só porque importa em alguns casos, não significa que importa em todos os casos. Existem muitos ambientes em que os programadores não são orientados a otimizar o tamanho do código fonte. Se fosse esse o caso, eles não estariam criando tantas classes.
Zumalifeguard

5

Essa idéia simplesmente equivale a um método "Self_Test" dentro do contexto de um design baseado em objeto ou orientado a objeto. Se estiver usando uma linguagem baseada em objeto compilada como Ada, todo o código de autoteste será marcado pelo compilador como não utilizado (nunca invocado) durante a compilação de produção e, portanto, tudo será otimizado - nada disso aparecerá no executável resultante.

Usar um método "Self_Test" é uma idéia extremamente boa, e se os programadores estivessem realmente preocupados com a qualidade, todos estariam fazendo isso. Uma questão importante, porém, é que o método "Self_Test" precisa ter disciplina intensa, pois não pode acessar nenhum detalhe da implementação e deve confiar apenas em todos os outros métodos publicados na especificação do objeto. Obviamente, se o autoteste falhar, a implementação precisará mudar. O autoteste deve estar testando rigorosamente todas as propriedades publicadas dos métodos do objeto, mas nunca confiando em nenhum detalhe de qualquer implementação específica.

Linguagens baseadas em objetos e orientadas a objetos freqüentemente fornecem exatamente esse tipo de disciplina com relação a métodos externos ao objeto testado (eles reforçam a especificação do objeto, impedindo qualquer acesso aos detalhes de sua implementação e gerando um erro de compilação se qualquer tentativa desse tipo for detectada ) Mas todos os métodos internos do objeto têm acesso completo a todos os detalhes da implementação. Portanto, o método de autoteste está em uma situação única: ele precisa ser um método interno devido à sua natureza (o autoteste é obviamente um método do objeto que está sendo testado), mas precisa receber toda a disciplina do compilador de um método externo ( deve ser independente dos detalhes de implementação do objeto). Poucas ou nenhuma linguagem de programação fornece a capacidade de disciplinar um objeto ' s método interno como se fosse um método externo. Portanto, esse é um problema importante de design da linguagem de programação.

Na ausência de suporte adequado à linguagem de programação, a melhor maneira de fazer isso é criar um objeto complementar. Em outras palavras, para cada objeto que você codifica (vamos chamá-lo de "Big_Object"), você também cria um segundo objeto complementar cujo nome consiste em um sufixo padrão concatenado com o nome do objeto "real" (nesse caso, "Big_Object_Self_Test ") e cuja especificação consiste em um único método (" Big_Object_Self_Test.Self_Test (This_Big_Object: Big_Object) retorna Booleano; "). O objeto complementar dependerá da especificação do objeto principal e o compilador aplicará totalmente toda a disciplina dessa especificação contra a implementação do objeto complementar.


4

Isso ocorre em resposta a um grande número de comentários, sugerindo que os testes em linha não são feitos porque é difícil impossível remover o código de teste das compilações de versão. Isso é falso. Quase todos os compiladores e montadores já suportam isso, com linguagens compiladas, como C, C ++, C #, isso é feito com as chamadas diretivas do compilador.

No caso de c # (acredito que também em c ++, a sintaxe pode ter um pouco diferente dependendo do compilador que você está usando), é assim que você pode fazê-lo.

#define DEBUG //  = true if c++ code
#define TEST /* can also be defined in the make file for c++ or project file for c# and applies to all associated .cs/.cpp files */

//somewhere in your code
#if DEBUG
// debug only code
#elif TEST
// test only code
#endif

Como isso usa diretivas do compilador, o código não existirá nos arquivos executáveis ​​criados se os sinalizadores não estiverem definidos. É assim também que você cria programas "escreva uma vez, compile duas vezes" para várias plataformas / hardware.


2

Usamos testes em linha com nosso código Perl. Existe um módulo, Test :: Inline , que gera arquivos de teste a partir do código embutido.

Não sou particularmente bom em organizar meus testes e os achei mais fáceis e com maior probabilidade de serem mantidos quando incluídos.

Respondendo a algumas das preocupações levantadas:

  • Os testes incorporados são escritos nas seções POD, portanto, não fazem parte do código real. Eles são ignorados pelo intérprete, portanto não há inchaço no código.
  • Usamos o Vim folding para ocultar as seções de teste. A única coisa que você vê é uma única linha acima de cada método que está sendo testado +-- 33 lines: #test----. Quando você deseja trabalhar com o teste, basta expandi-lo.
  • O módulo Test :: Inline "compila" os testes em arquivos compatíveis com TAP normais, para que possam coexistir com os testes tradicionais.

Para referência:


1

O Erlang 2 realmente suporta testes em linha. Qualquer expressão booleana no código que não é usada (por exemplo, atribuída a uma variável ou aprovada) é automaticamente tratada como um teste e avaliada pelo compilador; se a expressão for falsa, o código não será compilado.


1

Outro motivo para separar testes é que você costuma usar bibliotecas adicionais ou até diferentes para testar e não para a implementação real. Se você combinar testes e implementação, o uso acidental de bibliotecas de teste na implementação não poderá ser capturado pelo compilador.

Além disso, os testes tendem a ter muito mais linhas de código do que as partes de implementação testadas, portanto, você terá problemas para encontrar a implementação entre todos os testes. :-)


0

Isso não é verdade. É muito melhor colocar seus testes de unidade ao lado do código de produção quando o código de produção, especialmente quando a rotina de produção é pura.

Se você estiver desenvolvendo no .NET, por exemplo, poderá colocar seu código de teste no assembly de produção e usar o Scalpel para removê-los antes de enviar.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.