Como melhorar o teste do seu próprio código

45

Sou um desenvolvedor de software relativamente novo e uma das coisas que acho que devo melhorar é a minha capacidade de testar meu próprio código. Sempre que desenvolvo uma nova funcionalidade, acho realmente difícil seguir todos os caminhos possíveis para encontrar bugs. Eu costumo seguir o caminho onde tudo funciona. Sei que esse é um problema bem conhecido dos programadores, mas não temos testadores no meu atual empregador e meus colegas parecem ser muito bons nisso.

Na minha organização, não realizamos desenvolvimento orientado a testes nem testes de unidade. Isso me ajudaria muito, mas não é provável que isso mude.

O que vocês acham que eu poderia fazer para superar isso? Qual abordagem você usa ao testar seu próprio código?

Fran Sevillano
fonte
28
Só porque sua organização não usa TDD ou testes de unidade não significa que você não pode, desde que continue cumprindo seus prazos e produzindo código de qualidade.
Thomas Owens
1
Eu acho que Thomas me venceu, mas estou em uma situação semelhante. Escrevo expectativas de alto nível sobre o que uma alteração de código deve fazer e escrevo testes de unidade, se ou quando puder (mesmo que nossa empresa não faça oficialmente o teste de unidade). Você não precisa comprometê-los, e eles são ótimas maneiras de aprender como as funções devem agir (ou devem agir depois de corrigi-las).
Brian
6
@ Brian, acho que você deve cometer eles, independentemente de outras pessoas os usarem atualmente. Talvez mostrar boas práticas faça com que outras pessoas sigam.
CaffGeek

Respostas:

20

O trabalho de um codificador é construir coisas.

O trabalho de um testador é quebrar as coisas.

O mais difícil é quebrar as coisas que você acabou de construir. Você terá sucesso apenas ultrapassando essa barreira psicológica.

mouviciel
fonte
27
-1 O trabalho de um codificador é construir coisas que funcionem . Isso sempre envolve uma certa quantidade de testes. Concordo que é necessário um papel de testador separado, mas não é a única linha de defesa.
Matthew Rodatus
8
@ Matthew Rodatus - A quantidade de testes envolvidos no lado do codificador visa apenas verificar se o que deve funcionar realmente funciona. No lado do testador, o objetivo é encontrar erros, não observar que o código funciona.
Mouviciel 29/08/11
2
Isso é diferente da sua resposta, e eu concordo com isso mais. Mas ainda não concordo plenamente. Escrever código de qualidade vem da prática à medida que você aprende a pensar - antecipadamente - nas possibilidades de falha. Você não aprende a pensar nas possibilidades do fracasso sem aprender a criar essas possibilidades. Não acho que os codificadores devam ser a única linha de defesa, mas devem ser a primeira linha de defesa. A questão em jogo é a de minuciosidade e domínio do ofício.
Matthew Rodatus
2
@mouviciel - falsa dicotomia. O trabalho de um codificador é construir coisas que funcionem, e ele faz isso pensando a priori sob quais condições seu código deve funcionar. E isso é verificado, pelo menos, criando teste destrutivo + algumas análises de limites ad-hoc (novamente, pelo menos .) Além disso, um bom codificador trabalha com especificações e especificações (quando válidas) são sempre testáveis. Assim, um bom programador desenvolve teste que verificar estes requisitos são cumpridos em código (e você normalmente fazer isso por escrito req testes que falham inicialmente até que você tem o código que passa os testes..)
luis.espinal
2
@ Dave Lasley - Este é precisamente o meu ponto: o arquiteto não é a melhor pessoa para derrubar sua casa: ele tem muito orgulho de quão forte é ser capaz de ver seus defeitos. Somente outro arquiteto (não o cara da rua) pode trazer um olhar objetivo para a casa e descobrir que a casa pode se quebrar em algumas condições específicas que o ex-arquiteto era cego demais para imaginar.
Mouviciel 29/08/11
14

Franciso, vou fazer algumas suposições aqui, com base no que você disse:

"Não fazemos TDD nem testes de unidade. Isso me ajudaria muito, mas não é provável que isso mude."

A partir disso, desconfio que sua equipe não valorize muito os testes ou a gerência não terá um orçamento para tentar organizar o código existente e reduzir ao mínimo a dívida técnica .

Em primeiro lugar, você precisa convencer sua equipe / gerência do valor dos testes. Seja diplomático. Se a gerência estiver mantendo sua equipe em frente, você precisará mostrar alguns fatos, como a taxa de defeitos em cada versão. O tempo gasto na correção de defeitos poderia ser melhor gasto em outras coisas, como melhorar o aplicativo e torná-lo mais adaptável aos requisitos futuros.

Se a equipe e a gerência em geral são apáticas em relação à correção do código e você se sente insatisfeito com isso, pode ser necessário procurar outro local para trabalhar, a menos que possa convencê-los, como eu disse. Encontrei esse problema em diferentes graus em todos os lugares em que trabalhei. Pode ser qualquer coisa, desde a falta de um modelo de domínio adequado até a falta de comunicação na equipe.

Importar-se com o seu código e a qualidade do produto que você desenvolve é um bom atributo e que você sempre deseja incentivar em outras pessoas.

Planeta Desolado
fonte
11

Se você codificar em C, Objective-C ou C ++, poderá usar o CLang Static Analyzer para criticar sua fonte sem realmente executá-la.

Existem algumas ferramentas de depuração de memória disponíveis: ValGrind, Guard Malloc no Mac OS X, Cerca Elétrica no * NIX.

Alguns ambientes de desenvolvimento oferecem a opção de usar um alocador de memória de depuração, que faz coisas como preencher páginas recém-alocadas e páginas recém-liberadas com lixo, detectar a liberação de ponteiros não alocados e gravar alguns dados antes e depois de cada bloco de heap, com o depurador sendo chamado se o padrão conhecido desses dados mudar.

Um cara do Slashdot disse que ganhou muito valor com a nova linha de fonte de etapa única em um depurador. "É isso aí", ele disse. Eu nem sempre sigo o seu conselho, mas quando o recebi, foi muito útil para mim. Mesmo se você não tiver um caso de teste que estimule um caminho de código incomum, poderá modificar uma variável no depurador para seguir esses caminhos, digamos, alocando um pouco de memória e, em seguida, usando o depurador para definir seu novo ponteiro como NULL em vez de endereço de memória e, em seguida, percorrer o manipulador de falhas de alocação.

Use assertions - a macro assert () em C, C ++ e Objective-C. Se o seu idioma não fornecer uma função de afirmação, escreva um você mesmo.

Use afirmações liberalmente e deixe-as em seu código. Eu chamo assert () "O teste que continua testando". Eu os uso com mais frequência para verificar as pré-condições no ponto de entrada da maioria das minhas funções. Essa é uma parte da "Programação por contrato", incorporada à linguagem de programação Eiffel. A outra parte são pós-condições, ou seja, usando assert () nos pontos de retorno da função, mas acho que não obtenho tanta milhagem quanto as pré-condições.

Você também pode usar assert para verificar invariantes de classe. Embora nenhuma classe seja estritamente obrigada a ter nenhuma invariável, as classes mais sensatas projetadas as possuem. Uma classe invariável é uma condição que sempre é verdadeira, exceto dentro das funções de membro que podem temporariamente colocar seu objeto em um estado inconsistente. Tais funções sempre devem restaurar a consistência antes de retornar.

Assim, cada função membro poderia verificar a invariante ao entrar e sair, e a classe poderia definir uma função chamada CheckInvariant que qualquer outro código poderia chamar a qualquer momento.

Use uma ferramenta de cobertura de código para verificar quais linhas de sua fonte estão realmente sendo testadas e, em seguida, crie testes que estimulem as linhas não testadas. Por exemplo, você pode verificar os manipuladores com pouca memória executando seu aplicativo em uma VM configurada com pouca memória física e nenhum arquivo de troca ou um arquivo muito pequeno.

(Por alguma razão que eu nunca tive conhecimento, embora o BeOS pudesse ser executado sem um arquivo de troca, era altamente instável dessa forma. Dominic Giampaolo, que escreveu o sistema de arquivos BFS, me pediu para nunca executar o BeOS sem troca. Eu não veja por que isso importa, mas deve ter sido algum tipo de artefato de implementação.)

Você também deve testar a resposta do seu código a erros de E / S. Tente armazenar todos os seus arquivos em um compartilhamento de rede e desconecte o cabo de rede enquanto o aplicativo tem uma carga de trabalho alta. Da mesma forma, desconecte o cabo - ou desligue a sua conexão sem fio - se estiver se comunicando pela rede.

Uma coisa que acho particularmente irritante são sites que não possuem código Javascript robusto. As páginas do Facebook carregam dezenas de pequenos arquivos Javascript, mas se algum deles falhar no download, a página inteira é quebrada. Só precisa haver alguma maneira de fornecer alguma tolerância a falhas, digamos, repetindo um download ou fornecer algum tipo de fallback razoável quando alguns de seus scripts não foram baixados.

Tente matar seu aplicativo com o depurador ou com "kill -9" no * NIX enquanto ele estiver no meio da gravação de um arquivo grande e importante. Se o seu aplicativo estiver bem arquitetado, o arquivo inteiro será gravado ou não será gravado, ou talvez seja apenas parcialmente gravado, o que for gravado não será corrompido, com os dados salvos sendo completamente utilizáveis ​​por o aplicativo após a leitura do arquivo.

os bancos de dados sempre têm E / S de disco tolerantes a falhas, mas quase nenhum outro tipo de aplicativo possui. Embora os sistemas de arquivos registrados no diário evitem a corrupção do sistema de arquivos em caso de falha de energia ou falha, eles não fazem nada para impedir a corrupção ou a perda de dados do usuário final. Essa é a responsabilidade dos aplicativos do usuário, mas quase nenhum outro banco de dados implementa tolerância a falhas.

Mike Crawford
fonte
1
Marque com +1 conselhos práticos que não precisam de apoio de mais ninguém. A única coisa que eu acrescentaria é que assert é para documentar e verificar condições que não podem falhar, a menos que haja um erro no código . Nunca assert coisas que poderiam falhar devido a 'má sorte', como um arquivo essencial não ser encontrado, ou uma entrada inválida, etc.
Ian Goldby
10

Quando olho para testar meu código, geralmente passo por uma série de processos de pensamento:

  1. Como faço para dividir esse "coisinha" em pedaços de tamanho testável? Como posso isolar exatamente o que quero testar? Quais stubs / zombarias devo criar?
  2. Para cada pedaço: como faço para testar esse pedaço para garantir que ele responda corretamente a um conjunto razoável de entradas corretas?
  3. Para cada pedaço: como testar se o pedaço responde corretamente a entradas incorretas (ponteiros NULL, valores inválidos)?
  4. Como faço para testar limites (por exemplo, para onde os valores passam de assinado para não assinado, 8 bits para 16 bits etc.)?
  5. Quão bem meus testes cobrem o código? Há alguma condição que eu perdi? [Este é um ótimo local para ferramentas de cobertura de código.] Se houver um código que foi perdido e nunca pode ser executado, ele realmente precisa estar lá? [Essa é outra questão!]

A maneira mais fácil de encontrar isso é desenvolver meus testes junto com meu código. Assim que escrevi mesmo um fragmento de código, gosto de escrever um teste para ele. Tentar fazer todos os testes depois de codificar vários milhares de linhas de código com complexidade de código ciclomático não trivial é um pesadelo. Adicionar um ou mais dois testes após adicionar algumas linhas de código é realmente fácil.

BTW, apenas porque a empresa em que você trabalha e / ou seus colegas não realiza testes de unidade ou TDD, não significa que você não possa experimentá-los, a menos que sejam especificamente proibidos. Talvez usá-los para criar código robusto seja um bom exemplo para outros.

jwernerny
fonte
5

Além do conselho dado nas outras respostas, eu sugeriria o uso de ferramentas de análise estática (a Wikipedia possui uma lista de várias ferramentas de análise estática para vários idiomas ) para encontrar possíveis defeitos antes do início do teste, além de monitorar algumas métricas relacionadas a a testabilidade do código, como complexidade ciclomática , medidas de complexidade de Halstead e coesão e acoplamento (você pode medi-las com fan-in e fan-out).

Encontrar um código difícil de testar e facilitar o teste facilitará a criação de casos de teste. Além disso, a detecção precoce de defeitos agregará valor a todas as suas práticas de garantia de qualidade (que incluem testes). A partir daqui, familiarizar-se com as ferramentas de teste de unidade e as ferramentas de simulação facilitará a implementação de seus testes.

Thomas Owens
fonte
1
+1 para complexidade ciclomática. "Acho realmente difícil seguir todos os caminhos possíveis para encontrar bugs" implica que o código do OP talvez precise ser dividido em pedaços menores e menos complexos.
Toby
@ Toby Sim, foi por isso que decidi jogar na análise estática. Se você não pode testar seu código, está com problemas. E se você tiver um problema com seu código, pode haver outros. Use uma ferramenta para encontrar possíveis sinalizadores de aviso, avaliá-los e corrija conforme necessário. Você não terá apenas mais código testável, mas também código mais legível.
Thomas Owens
3

Você pode examinar o possível uso das Tabelas da Verdade para ajudá-lo a definir todos os caminhos possíveis no seu código. É impossível considerar todas as possibilidades em funções complexas, mas depois de estabelecer seu tratamento para todos os caminhos conhecidos, você pode estabelecer um tratamento para o caso else.

A maior parte dessa habilidade em particular é aprendida pela experiência. Depois de usar uma certa estrutura por um período significativo de tempo, você começa a ver os padrões e as marcas de comportamento que permitem examinar um pedaço de código e ver onde uma pequena alteração pode causar um erro grave. A única maneira de pensar em aumentar sua aptidão nisso é praticar.

Joel Etherton
fonte
3

Se, como você disse, não precisa de teste de unidade, não vejo uma abordagem melhor do que tentar quebrar seu próprio código manualmente.

Tente levar seu código ao limite . Por exemplo, tente passar variáveis ​​para uma função que excede os limites do limite. Você tem uma função que supostamente filtra a entrada do usuário? Tente inserir diferentes combinações de caracteres.

Considere o ponto de vista do usuário . Tente ser um dos usuários que usará seu aplicativo ou biblioteca de funções.

Jose Faeti
fonte
1
+1 por mencionar a visualização do ponto de vista do usuário.
Matthew Rodatus
3

mas não temos testadores no meu atual empregador e meus colegas parecem ser muito bons nisso

Seus colegas devem ser verdadeiramente excepcionais para não seguir o TDD ou o teste de unidade e nunca gerar bugs; portanto, em algum nível, duvido que eles não estejam realizando nenhum teste de unidade.

Suponho que seus colegas estão realizando mais testes do que os permitidos, mas, como esse fato não é conhecido pela gerência, a organização sofre como resultado porque a gerência tem a impressão de que testes reais não estão sendo realizados e, portanto, o número de erros é baixo. o teste não é importante e o horário não será agendado para ele.

Converse com seus colegas e tente descobrir que tipo de teste de unidade eles estão fazendo e imite isso. Posteriormente, você pode criar protótipos de maneiras melhores de atributos de teste de unidade e TDD e apresentar lentamente esses conceitos à equipe para facilitar a adoção.

maple_shaft
fonte
2
  • Escreva seus testes antes de escrever seu código.
  • Sempre que você corrigir um bug que não foi detectado por um teste, escreva um teste para detectá-lo.

Você deve conseguir cobertura sobre o que escreve, mesmo que sua organização não tenha cobertura total. Como muitas coisas na programação, a experiência de fazer isso de novo e de novo é uma das melhores maneiras de ser eficiente nisso.

Jeff Ferland
fonte
2

Além de todos os outros comentários, como você diz que seus colegas são bons em escrever testes que não são felizes, por que não pedir que eles se unam a você para escrever alguns testes.

A melhor maneira de aprender é ver como isso é feito e extrair o que você aprende disso.

Jim Munro
fonte
2

Teste de caixa preta! Você deve criar suas classes / métodos com o teste em mente. Seus testes devem basear-se na especificação do software e devem ser claramente definidos no diagrama de sequência (por meio de casos de uso).

Agora, já que você pode não querer fazer desenvolvimento orientado a testes ...

Coloque validação de entrada em todos os dados recebidos; não confie em ninguém. A estrutura .net lança muitas exceções com base em argumentos inválidos, referências nulas e estados inválidos. Você já deve estar pensando em usar a validação de entrada na camada da interface do usuário, por isso é o mesmo truque no middleware.

Mas você realmente deveria fazer algum tipo de teste automatizado; esse material salva vidas.


fonte
2

Em minha experiência

A unidade de teste, se não for totalmente automática, é inútil. É mais como um chefe de cabelos pontudos poderia comprar. Por quê ?, porque a Unidade de Teste prometeu economizar tempo (e dinheiro), automatizando alguns processos de teste. Mas, algumas ferramentas da unidade de teste fazem o oposto, forçam os programadores a trabalhar de uma maneira estranha e forçam outros a criar testes de extensão excessiva. Na maioria das vezes, não economiza horas de trabalho, mas aumenta o tempo de mudança do controle de qualidade para o desenvolvedor.

UML é outro desperdício de tempo. um único quadro branco + caneta pode fazer o mesmo, mais barato e rapidamente.

BTW, como ser bom em codificação (e evitar bugs)?

  • a) atomicidade. Uma função que executa uma simples (ou algumas tarefas únicas). Como é fácil de entender, é fácil acompanhar e é fácil resolvê-lo.

  • b) Homologia. Se, por exemplo, você chamar um banco de dados usando um procedimento de armazenamento, faça o restante do código.

  • c) Identifique, reduza e isole o "código do criativo". A maior parte do código é basicamente copiar e colar. O código do criativo é o oposto, um código que é novo e atua como um protótipo, pode falhar. Esse código é propenso a erros de lógica, por isso é importante reduzi-lo, isolá-lo e identificá-lo.

  • d) Código "Thin ice", é o código que você sabe que é "incorreto" (ou potencialmente perigoso), mas ainda precisa, por exemplo, de código não seguro para um processo de várias tarefas. Evite se puder.

  • e) Evite o código da caixa preta, isso inclui o código que não é feito por você (por exemplo, framework) e a expressão regular. É fácil perder um bug com esse tipo de código. Por exemplo, trabalhei em um projeto usando o Jboss e encontrei não um erro, mas 10 no Jboss (usando a versão estável mais recente); era uma PITA encontrá-los. Evite especialmente o Hibernate, pois oculta a implementação, daí os erros.

  • f) adicione comentários no seu código.

  • g) entrada do usuário como fonte de bugs. identifique-o. Por exemplo, a injeção SQL é causada por uma entrada do usuário.

  • h) Identifique o elemento defeituoso da equipe e separe a tarefa atribuída. Alguns programadores são propensos a estragar o código.

  • i) Evite código desnecessário. Se, por exemplo, a classe precisar de Interface , use-a; caso contrário, evite adicionar código irrelevante.

a) eb) são fundamentais. Por exemplo, eu tive um problema com um sistema, quando cliquei em um botão (salvar) que não salvou o formulário. Então eu fiz uma lista de verificação:

  • o botão funciona? ... sim.
  • o banco de dados armazena alguma coisa? não, então o erro ocorreu na etapa intermediária.
  • então, a classe que armazena no banco de dados funciona? não <- ok, encontrei o erro. (estava relacionado à permissão do banco de dados). Então eu verifiquei não apenas este procedimento, mas todos os procedimentos que fazem o mesmo (devido à homologia do código). Levei 5 minutos para rastrear o bug e 1 minuto para resolvê-lo (e muitos outros bugs).

E uma nota lateral

Na maioria das vezes, o controle de qualidade é péssimo (como uma seção separada do Dev), é inútil se eles não forem trabalhados no projeto. Eles fazem alguns testes genéricos e nada mais. Eles não conseguem identificar a maioria dos erros lógicos. No meu caso, eu estava trabalhando em um banco de prestígio, um programador terminou um código e o enviou ao controle de qualidade. O controle de qualidade aprovou o código e foi colocado em produção ... então o código falhou (uma falha épica), você sabe quem foi o culpado ?. sim, o programador.

Magallanes
fonte
2

Um testador e um programador enfrentam o problema de diferentes ângulos, mas ambas as funções devem testar completamente a funcionalidade e encontrar bugs. Onde os papéis diferem está em foco. Um testador clássico vê o aplicativo apenas do lado de fora (por exemplo, caixa preta). Eles são especialistas nos requisitos funcionais do aplicativo. Espera-se que um programador seja especialista tanto nos requisitos funcionais quanto no código (mas tende a se concentrar mais no código).

(Depende da organização se se espera explicitamente que os programadores sejam especialistas em requisitos. Independentemente disso, a expectativa implícita existe - se você cria algo errado, você - e não a pessoa responsável - recebe a culpa.)

Essa dupla função de especialista está sobrecarregando a mente do programador e, exceto os mais experientes, pode diminuir a proficiência nos requisitos. Acho que devo mudar de marcha mentalmente para considerar os usuários do aplicativo. Aqui está o que me ajuda:

  1. Depuração ; defina pontos de interrupção no código e execute o aplicativo. Depois de atingir um ponto de interrupção, percorra as linhas à medida que interage com o aplicativo.
  2. Teste automatizado ; escreva um código que teste seu código. Isso ajuda apenas em níveis abaixo da interface do usuário.
  3. Conheça seus testadores ; eles podem conhecer o aplicativo melhor do que você, então aprenda com eles. Pergunte a eles quais são os pontos fracos do seu aplicativo e quais táticas eles usam para encontrar bugs.
  4. Conheça seus usuários ; Aprenda a andar no lugar dos seus usuários. Os requisitos funcionais são a impressão digital dos seus usuários. Muitas vezes, os usuários sabem muitas coisas sobre o aplicativo que podem não aparecer claramente nos requisitos funcionais. À medida que você entender melhor seus usuários - a natureza do trabalho deles no mundo real e como seu aplicativo deve ajudá-los -, você entenderá melhor o que o aplicativo deve ser.
Matthew Rodatus
fonte
2

Eu acho que você quer trabalhar em duas frentes. Um é político, fazendo com que sua organização adote testes em algum nível (com a esperança de que, com o tempo, eles adotem mais). Converse com engenheiros de controle de qualidade fora do seu local de trabalho. Encontre listas de livros de controle de qualidade . Dê uma olhada nos artigos relevantes da Wikipedia . Familiarize-se com os princípios e práticas de controle de qualidade. Aprender essas coisas o preparará para fazer o caso mais convincente possível em sua organização. Existem bons departamentos de controle de qualidade e agregam um valor considerável às suas organizações.

Como desenvolvedor individual, adote estratégias para usar em seu próprio trabalho. Use o TDD você mesmo desenvolvendo códigos e testes. Mantenha os testes limpos e bem conservados. Se perguntado por que você está fazendo isso, pode dizer que está impedindo regressões e isso mantém seu processo de pensamento melhor organizado (os dois serão verdadeiros). Existe uma arte em escrever código testável , aprendê-lo. Seja um bom exemplo para seus colegas desenvolvedores.

Em parte, estou pregando para mim mesmo aqui, porque faço muito menos dessas coisas do que sei que deveria.

Will Ware
fonte