O TDD torna a programação defensiva redundante?

103

Hoje tive uma discussão interessante com um colega.

Eu sou um programador defensivo. Acredito que a regra " uma classe deve garantir que seus objetos tenham um estado válido quando interagidos de fora da classe " deve ser sempre respeitada. A razão para essa regra é que a classe não sabe quem são seus usuários e que ela deve falhar previsivelmente quando for interagida de maneira ilegal. Na minha opinião, essa regra se aplica a todas as classes.

Na situação específica em que tive uma discussão hoje, escrevi código que valida que os argumentos para meu construtor estão corretos (por exemplo, um parâmetro inteiro deve ser > 0) e se a pré-condição não for atendida, uma exceção jogado. Meu colega, por outro lado, acredita que tal verificação é redundante, porque os testes de unidade devem detectar qualquer uso incorreto da classe. Além disso, ele acredita que as validações de programação defensiva também devem ser testadas em unidade, então a programação defensiva adiciona muito trabalho e, portanto, não é ideal para TDD.

É verdade que o TDD é capaz de substituir a programação defensiva? A validação de parâmetros (e não quero dizer a entrada do usuário) é desnecessária como conseqüência? Ou as duas técnicas se complementam?

    
por user2180613 23.09.2016 / 22:21
fonte

14 respostas

195

Isso é ridículo. O TDD força o código a passar nos testes e força todo o código a fazer alguns testes em torno dele. Isso não impede que seus consumidores indiquem o código de forma incorreta, nem impede magicamente que os programadores não tenham casos de teste.

Nenhuma metodologia pode forçar os usuários a usar o código corretamente.

Existe um pequeno argumento a ser feito de que se você fizesse o TDD perfeitamente você teria pego o seu > 0 verifique em um caso de teste, antes de implementá-lo, e resolva isso - provavelmente adicionando o cheque. Mas se você fez TDD, sua exigência (> 0 no construtor) apareceria primeiro como um testcase que falha. Assim, dando-lhe o teste depois de adicionar o seu cheque.

Também é razoável testar algumas das condições defensivas (você adicionou lógica, por que você não quer testar algo tão facilmente testável?). Não sei por que você parece discordar disso.

Or do the two techniques complement each other?

O TDD desenvolverá os testes. A implementação da validação de parâmetros fará com que eles passem.

    
por 23.09.2016 / 22:37
fonte
32

Programação defensiva e testes de unidade são duas maneiras diferentes de capturar erros e cada um tem pontos strongs diferentes. Usar apenas uma maneira de detectar erros torna seus mecanismos de detecção de erros frágeis. O uso de ambos detectará erros que podem ter sido perdidos por um ou outro, mesmo no código que não é uma API voltada para o público; Por exemplo, alguém pode ter esquecido de adicionar um teste de unidade para dados inválidos passados para a API pública. Verificar tudo em locais apropriados significa mais chances de detectar o erro.

Na segurança da informação, isso é chamado de Defesa em Profundidade. Ter várias camadas de defesa garante que, se uma falhar, ainda haverá outras para capturá-la.

Seu colega está certo sobre uma coisa: você deve testar suas validações, mas isso não é "trabalho desnecessário". É o mesmo que testar qualquer outro código, você quer ter certeza de que todos os usos, mesmo os inválidos, têm um resultado esperado.

    
por 23.09.2016 / 22:39
fonte
30

O TDD não substitui a programação defensiva. Em vez disso, você pode usar o TDD para garantir que todas as defesas estejam no lugar e funcionem conforme o esperado.

No TDD, você não deve escrever código sem antes escrever um teste - siga o ciclo refator-vermelho-verde religiosamente. Isso significa que, se você quiser adicionar validação, primeiro escreva primeiro um teste que requeira essa validação. Chame o método em questão com números negativos e com zero e espere lançar uma exceção.

Além disso, não esqueça a etapa "refatorar". Embora o TDD seja testado, isso não significa testar somente. Você ainda deve aplicar um design adequado e escrever um código sensato. Escrever códigos defensivos é um código sensato, porque torna as expectativas mais explícitas e seu código mais robusto - detectar possíveis erros no início torna mais fácil depurá-los.

Mas não devemos usar testes para localizar erros? Afirmações e testes são complementares. Uma boa estratégia de teste irá misturar várias abordagens para garantir que o software seja robusto. Apenas testes unitários ou apenas testes de integração ou apenas afirmações no código são todos insatisfatórios, você precisa de uma boa combinação para alcançar um grau de confiança suficiente em seu software com um esforço aceitável.

Depois, há um grande equívoco conceitual de seu colega de trabalho: Testes de unidade nunca podem testar usos de sua classe, somente que a própria classe funciona como esperado em isolamento. Você usaria testes de integração para verificar se a interação entre vários componentes trabalha, mas a explosão combinatória de possíveis casos de teste torna impossível testar tudo. Os testes de integração devem, portanto, restringir-se a alguns casos importantes. Testes mais detalhados que também cobrem casos de borda e casos de erro são mais adequados para testes de unidade.

    
por 23.09.2016 / 22:40
fonte
16

Testes estão lá para apoiar e garantir a programação defensiva

A programação defensiva protege a integridade do sistema em tempo de execução.

Os testes são ferramentas de diagnóstico (principalmente estáticas). Em tempo de execução, seus testes não estão à vista. Eles são como andaimes usados para colocar uma parede de tijolos altos ou uma cúpula de rocha. Você não deixa partes importantes fora da estrutura porque você tem um andaime segurando-o durante a construção. Você tem um andaime segurando-o durante a construção para facilitar colocar todas as peças importantes.

EDIT: uma analogia

Que tal uma analogia aos comentários no código?

Os comentários têm o seu propósito, mas podem ser redundantes ou mesmo prejudiciais. Por exemplo, se você colocar conhecimento intrínseco sobre o código nos comentários , em seguida, alterar o código, os comentários se tornarão irrelevantes na melhor das hipóteses e prejudiciais na pior das hipóteses.

Digamos que você coloque muito conhecimento intrínseco de sua base de código nos testes, como o MethodA não pode aceitar um nulo e o argumento do MethodB deve ser > 0 . Então o código muda. Null está bem para A agora, e B pode pegar valores tão pequenos quanto -10. Os testes existentes estão agora funcionalmente errados, mas continuarão a passar.

Sim, você deve atualizar os testes ao mesmo tempo em que atualiza o código. Você também deve atualizar (ou remover) comentários ao mesmo tempo em que atualiza o código. Mas todos sabemos que essas coisas nem sempre acontecem e que erros são cometidos.

Os testes verificam o comportamento do sistema. Esse comportamento real é intrínseco ao próprio sistema, não intrínseco aos testes.

O que poderia dar errado?

O objetivo com relação aos testes é pensar em tudo que poderia dar errado, escrever um teste para ele que verifique o comportamento correto e criar o código de tempo de execução para que ele passe em todos os testes.

O que significa que programação defensiva é o ponto .

TDD impulsiona a programação defensiva, se os testes forem abrangentes.

Mais testes, conduzindo uma programação mais defensiva

Quando erros são encontrados inevitavelmente, mais testes são escritos para modelar as condições que manifestam o erro. Em seguida, o código é corrigido, com código para fazer com que esses testes passem, e os novos testes permanecem no conjunto de testes.

Um bom conjunto de testes vai passar bons e maus argumentos para uma função / método e esperar resultados consistentes. Isso, por sua vez, significa que o componente testado usará verificações de pré-condição (programação defensiva) para confirmar os argumentos passados para ele.

Genericamente falando ...

Por exemplo, se um argumento nulo para um determinado procedimento é inválido, então pelo menos um teste vai passar um nulo, e ele vai esperar uma exceção "argumento nulo inválido" / algum tipo de erro.

Pelo menos um outro teste vai passar um argumento válido , claro - ou passar por um grande array e passar muitos argumentos válidos - e confirmar que o estado resultante é apropriado. / p>

Se um teste não passar esse argumento nulo e for esbofeteado com a exceção esperada (e essa exceção foi lançada porque o código defensivamente verificou o estado passado a ele), então o nulo pode terminar atribuído a uma propriedade de uma classe ou enterrado em uma coleção de algum tipo, onde não deveria ser.

Isso pode causar um comportamento inesperado em alguma parte completamente diferente do sistema para o qual a instância da classe é passada, em algum local geográfico distante após o software ter sido enviado . E esse é o tipo de coisa que estamos tentando evitar, certo?

Pode até ser pior. A instância de classe com o estado inválido pode ser serializada e armazenada, apenas para causar uma falha quando for reconstituída para uso posterior. Nossa, eu não sei, talvez seja um sistema de controle mecânico de algum tipo que não pode reiniciar após um desligamento porque não pode desserializar seu próprio estado de configuração persistente. Ou a instância da classe poderia ser serializada e passada para algum sistema completamente diferente criado por alguma outra entidade, e o sistema pode falhar.

Especialmente se os programadores do outro sistema não codificassem defensivamente.

    
por 24.09.2016 / 08:54
fonte
9

Em vez de TDD, vamos falar sobre "teste de software" em geral, e em vez de "programação defensiva" em geral, vamos falar sobre minha maneira favorita de fazer programação defensiva, que é usando asserções.

Então, como fazemos o teste de software, devemos deixar de colocar instruções de afirmação no código de produção, certo? Deixe-me contar as maneiras pelas quais isso está errado:

  1. As afirmações são opcionais, portanto, se você não gostar delas, basta executar o sistema com as afirmações desativadas.

  2. Afirmações verificam coisas que o teste não pode (e não deveria). Porque o teste deve ter uma visualização de caixa preta do seu sistema, enquanto as asserções têm uma visualização de caixa branca. (Claro, já que eles vivem nela).

  3. As asserções são uma excelente ferramenta de documentação. Nenhum comentário jamais foi, ou será, tão inequívoco quanto um pedaço de código afirmando a mesma coisa. Além disso, a documentação tende a ficar desatualizada à medida que o código evolui, e não é de forma alguma aplicável pelo compilador.

  4. As asserções podem detectar erros no código de teste. Você já se deparou com uma situação em que um teste falha e não sabe quem está errado - o código de produção ou o teste?

  5. As asserções podem ser mais pertinentes do que testes. Os testes irão verificar o que é prescrito pelos requisitos funcionais, mas o código muitas vezes tem que fazer certas suposições que são muito mais técnicas do que isso. As pessoas que escrevem documentos de requisitos funcionais raramente pensam em divisão por zero.

  6. Afirmações apontam erros que o teste apenas sugere amplamente. Assim, seu teste configura algumas precondições extensas, invoca algum código extenso, reúne os resultados e descobre que eles não são os esperados. Dada uma solução de problemas suficiente, você acabará por descobrir exatamente onde as coisas deram errado, mas as asserções geralmente a encontrarão primeiro.

  7. As asserções reduzem a complexidade do programa. Cada linha de código que você escreve aumenta a complexidade do programa. As asserções e a palavra-chave final ( readonly ) são as únicas construções que conheço que reduzem a complexidade do programa. Isso é inestimável.

  8. As asserções ajudam o compilador a entender melhor seu código. Por favor, tente isso em casa: void foo( Object x ) { assert x != null; if( x == null ) { } } seu compilador deve emitir um aviso informando que a condição x == null é sempre falsa. Isso pode ser muito útil.

O texto acima foi um resumo de um post do meu blog, 2014-09-21 "Assertions e teste "

    
por 23.09.2016 / 23:00
fonte
5

Acredito que a maioria das respostas não tem uma distinção importante: depende de como seu código será usado.

O módulo em questão será usado por outros clientes independentemente do aplicativo que você está testando? Se você estiver fornecendo uma biblioteca ou API para uso por terceiros, não há como garantir que eles só chamem seu código com uma entrada válida. Você tem que validar todas as entradas.

Mas se o módulo em questão é usado apenas pelo código que você controla, seu amigo pode ter um ponto. Você pode usar testes de unidade para verificar se o módulo em questão é chamado apenas com uma entrada válida. As verificações de condições prévias ainda podem ser consideradas uma boa prática, mas é uma compensação: eu não coloco o código que verifica a condição que você sabe que nunca pode surgir, apenas obscurece a intenção do código.

Eu discordo que verificações pré-requisito exigem mais testes unitários. Se você decidir que não precisa testar algumas formas de entrada inválidas, então não importa se a função contém verificações de pré-condição ou não. Lembre-se de que os testes devem verificar o comportamento e não os detalhes da implementação.

    
por 24.09.2016 / 11:46
fonte
3

Esse argumento meio que me deixa perplexo, porque quando comecei a praticar o TDD, meus testes de unidade do formulário "objeto responde de maneira certa > quando < entrada inválida >" aumentou 2 ou 3 vezes. Eu estou querendo saber como o seu colega está conseguindo passar com sucesso esses tipos de testes de unidade sem que suas funções façam a validação.

O caso inverso, que os testes de unidade mostram que você nunca está produzindo saídas ruins que serão passadas para argumentos de outras funções, é muito mais difícil de provar. Como no primeiro caso, depende muito da cobertura completa dos casos de borda, mas você tem o requisito adicional de que todas as suas entradas de função devem vir das saídas de outras funções cujas saídas você testou na unidade e não de, digamos, entrada do usuário ou módulos de terceiros.

Em outras palavras, o que o TDD faz não impede que você precise do código de validação, mas ajuda a evitar esquecê-lo .

    
por 23.09.2016 / 23:54
fonte
2

Acho que interpreto as observações do seu colega de forma diferente da maioria das respostas.

Parece-me que o argumento é:

  • Todo o nosso código é testado na unidade.
  • Todo o código que usa seu componente é nosso código, ou se não for testado por outra pessoa (não explicitamente declarado, mas é o que eu entendo de "testes de unidade devem pegar qualquer uso incorreto da classe").
  • Portanto, para cada chamador de sua função, há um teste de unidade em algum lugar que zomba de seu componente, e o teste falha se o chamador passar um valor inválido para essa simulação.
  • Portanto, não importa o que sua função faz quando passou um valor inválido, porque nossos testes dizem que isso não pode acontecer.

Para mim, este argumento tem alguma lógica, mas coloca muita confiança em testes de unidade para cobrir todas as situações possíveis. O simples fato é que 100% de cobertura de linha / ramal / caminho não necessariamente exercita cada valor que o chamador pode passar, enquanto 100% de cobertura de todos os estados possíveis do chamador (isto é , todos os valores possíveis de suas entradas e variáveis) é computacionalmente inviável.

Portanto, eu tenderia a preferir testar os chamadores para garantir que (até onde os testes estão) eles nunca passem em valores ruins, e adicionalmente para exigir que seu componente falhe em alguns maneira reconhecível quando um valor ruim é passado (pelo menos até onde é possível reconhecer valores ruins em sua linguagem de escolha). Isso ajudará na depuração quando ocorrerem problemas nos testes de integração e, da mesma forma, ajudará os usuários de sua classe que são menos rigorosos a isolar sua unidade de código dessa dependência.

Esteja ciente, porém, de que se você documentar e testar o comportamento de sua função quando um valor < = 0 for passado, os valores negativos não serão mais inválidos (pelo menos, não mais inválido do que qualquer argumento para throw , desde que isso também está documentado para lançar uma exceção!). Os chamadores têm o direito de confiar nesse comportamento defensivo. Se a linguagem permitir, pode ser que esse seja o melhor cenário - a função não tem "entradas inválidas", mas os chamadores que esperam não provocar a função em lançar uma exceção devem ser unitários. testado o suficiente para garantir que eles não passem quaisquer valores que causem isso.

Apesar de achar que seu colega está menos mal do que a maioria das respostas, chego à mesma conclusão, que é a de que as duas técnicas se complementam. Programe-se defensivamente, documente suas verificações defensivas e teste-as. O trabalho só é "desnecessário" se os usuários do seu código não puderem se beneficiar de mensagens de erro úteis quando cometem erros. Em teoria, se eles testarem completamente o código antes de integrá-lo ao seu, e nunca houver erros em seus testes, eles nunca verão as mensagens de erro. Na prática, mesmo que estejam fazendo TDD e injeção total de dependência, eles ainda podem explorar durante o desenvolvimento ou pode haver um lapso em seus testes. O resultado é que eles chamam seu código antes que o código seja perfeito!

    
por 24.09.2016 / 00:23
fonte
1

As interfaces públicas podem e serão usadas de maneira inadequada

A alegação do seu colega de trabalho "testes de unidade devem detectar qualquer uso incorreto da classe" é estritamente falsa para qualquer interface que não seja privada. Se uma função pública pode ser chamada com argumentos inteiros, então ela pode e será chamada com argumentos inteiros any , e o código deve se comportar apropriadamente. Se uma assinatura de função pública aceita, e. Tipo Java duplo, então, nulo, NaN, MAX_VALUE, -Inf são todos valores possíveis. Seus testes unitários não podem capturar usos incorretos da classe porque esses testes não podem testar o código que usará essa classe, porque esse código ainda não está escrito, pode não ter sido escrito por você e definitivamente estará fora do escopo seus testes de unidade .

Por outro lado, esta abordagem pode ser válida para as propriedades privadas (provavelmente muito mais numerosas) - se uma classe puder garantir que algum fato é sempre verdadeiro (por exemplo, a propriedade X não pode ser nula , a posição inteira não excede o comprimento máximo, quando a função A é chamada, todas as estruturas de dados de pré-requisito são bem formadas), então pode ser apropriado evitar verificar isso repetidas vezes por motivos de desempenho e confiar nos testes de unidade. / p>     

por 24.09.2016 / 10:19
fonte
1

A defesa contra o uso indevido é um recurso , desenvolvido devido a um requisito para isso. (Nem todas as interfaces exigem verificações rigorosas contra o uso indevido; por exemplo, aquelas internas de uso muito estreito.)

O recurso requer testes: a defesa contra o uso incorreto realmente funciona? O objetivo de testar esse recurso é tentar mostrar que isso não acontece: para inventar um mau uso do módulo que não é detectado por suas verificações.

Se verificações específicas são um recurso necessário, é realmente absurdo afirmar que a existência de alguns testes os torna desnecessários. Se é uma característica de alguma função que (digamos) lança uma exceção quando o parâmetro três é negativo, então isso não é negociável; fará isso.

No entanto, suspeito que o seu colega está realmente a fazer sentido do ponto de vista de uma situação em que não há necessidade de verificações específicas de entradas, com respostas específicas a entradas ruins: uma situação em que há apenas um requisito geral para robustez.

Verificações na entrada em alguma função de nível superior existem, em parte, para proteger algum código interno fraco ou mal testado de combinações inesperadas de parâmetros (de forma que se o código for bem testado, as verificações não são necessárias: o código pode apenas "resistir" aos parâmetros ruins).

Há verdade na ideia do colega, e o que ele provavelmente quer dizer é: se construirmos uma função a partir de peças de baixo nível muito robustas que são codificadas defensivamente e testadas individualmente contra todo uso indevido, então é possível para o nível superior função para ser robusto sem ter suas próprias auto-verificações extensas.

Se o contrato for violado, isso se traduzirá em algum uso indevido das funções de nível inferior, talvez lançando exceções ou o que quer que seja.

O único problema com isso é que as exceções de nível inferior não são específicas da interface de nível superior. Se isso é um problema, depende de quais são os requisitos. Se a exigência for simplesmente "a função deve ser robusta contra o uso indevido e lançar algum tipo de exceção ao invés de falha, ou continuar calculando com dados de lixo" então, de fato, ela pode ser coberta por toda a robustez das partes de nível inferior nas quais é construído.

Se a função requer um relatório de erros detalhado e muito específico relacionado aos seus parâmetros, as verificações de nível inferior não satisfazem totalmente esses requisitos. Eles garantem apenas que a função explode de alguma forma (não continua com uma combinação ruim de parâmetros, produzindo um resultado de lixo). Se o código do cliente for gravado para detectar especificamente certos erros e lidar com eles, talvez ele não funcione corretamente. O código do cliente pode estar obtendo, como entrada, os dados nos quais os parâmetros são baseados, e pode estar esperando que a função os verifique e traduza valores inválidos para os erros específicos, conforme documentado (para que possa lidar com esses erros). erros corretamente) em vez de alguns outros erros que não são tratados e talvez parem a imagem do software.

TL; DR: seu colega provavelmente não é um idiota; vocês estão apenas conversando uns com os outros com diferentes perspectivas em torno da mesma coisa, porque os requisitos não estão totalmente estabelecidos e cada um de vocês tem uma idéia diferente de quais são os "requisitos não-escritos". Você acha que quando não há requisitos específicos na verificação de parâmetros, você deve codificar a verificação detalhada de qualquer maneira; O colega pensa, apenas deixe o robusto código de nível inferior explodir quando os parâmetros estiverem errados. É um pouco improdutivo argumentar sobre requisitos não escritos através do código: reconheça que você discorda sobre requisitos em vez de código. Sua maneira de codificar reflete o que você acha que são os requisitos; o caminho do colega representa sua visão dos requisitos. Se você vê dessa maneira, fica claro que o que está certo ou errado não está no próprio código; o código é apenas um proxy para sua opinião sobre qual deve ser a especificação.

    
por 27.09.2016 / 01:23
fonte
1

Os testes definem o contrato da sua classe.

Como corolário, a ausência de um teste define um contrato que inclui comportamento indefinido . Portanto, quando você passar null para Foo::Frobnicate(Widget widget) e incalculável confusão em tempo de execução, você ainda estará no contrato da sua turma.

Mais tarde, você decide "não queremos a possibilidade de um comportamento indefinido", o que é uma escolha sensata. Isso significa que você precisa ter um comportamento esperado para passar null para Foo::Frobnicate(Widget widget) .

E você documenta essa decisão incluindo um

[Test]
void Foo_FrobnicatesANullWidget_ThrowsInvalidArgument() 
{
    Given(Foo foo);
    When(foo.Frobnicate(null));
    Then(Expect_Exception(InvalidArgument));
}
    
por 24.05.2018 / 17:44
fonte
1

Um bom conjunto de testes irá exercitar a interface externa da sua classe e garantir que tais erros gerem a resposta correta (uma exceção, ou o que você definir como "correto"). Na verdade, o primeiro caso de teste que eu escrevo para uma classe é chamar seu construtor com argumentos fora do intervalo.

O tipo de programação defensiva que tende a ser eliminada por uma abordagem totalmente testada em unidade é a validação desnecessária de invariantes internos que não podem ser violados por código externo.

Uma idéia útil que às vezes uso é fornecer um método que testa as invariantes do objeto; seu método de eliminação pode chamá-lo para validar que suas ações externas no objeto nunca quebram os invariantes.

    
por 26.09.2016 / 13:12
fonte
0

Os testes do TDD irão detectar erros durante o desenvolvimento do código .

Os limites que você descreve como parte da programação defensiva detectarão erros durante o uso do código .

Se os dois domínios são os mesmos, ou seja, o código que você está escrevendo só é usado internamente por este projeto específico, então pode ser verdade que o TDD impedirá a necessidade de verificação de limites de programação defensivos descritos por você, mas apenas if esses tipos de verificação de limites são executados especificamente em testes TDD .

Como um exemplo específico, suponha que uma biblioteca de código financeiro foi desenvolvida usando o TDD. Um dos testes pode afirmar que um determinado valor nunca pode ser negativo. Isso garante que os desenvolvedores da biblioteca não usem acidentalmente as classes ao implementarem os recursos.

Mas depois que a biblioteca é lançada e eu a estou usando no meu próprio programa, esses testes de TDD não me impedem de atribuir um valor negativo (supondo que esteja exposto). Verificação de limites seria.

Meu ponto é que, enquanto uma declaração TDD poderia resolver o problema de valor negativo se o código é usado apenas internamente como parte do desenvolvimento de um aplicativo maior (em TDD), se vai ser uma biblioteca usada por outros programadores sem o framework TDD e testes , questões de verificação de limites.

    
por 24.09.2016 / 01:21
fonte
0

TDD e programação defensiva andam de mãos dadas. Usar ambos não é redundante, mas de fato complementar. Quando você tem uma função, você quer ter certeza de que a função funciona como descrito e escrever testes para ela; se você não cobrir o que acontece quando, no caso de uma entrada inválida, retorno incorreto, estado incorreto, etc., você não está gravando seus testes de maneira robusta o suficiente, e seu código ficará frágil mesmo se todos os seus testes estiverem passando.

Como engenheiro incorporado, gosto de usar o exemplo de escrever uma função para simplesmente adicionar dois bytes e retornar o resultado assim:

uint8_t AddTwoBytes(uint8_t a, uint8_t b, uint8_t *sum); 

Agora, se você simplesmente fez *(sum) = a + b , isso funcionaria, mas apenas com algumas entradas. a = 1 e b = 2 produziriam sum = 3 ; no entanto, como o tamanho da soma é um byte, a = 100 e b = 200 produzirão sum = 44 devido ao estouro. Em C, você retornaria um erro neste caso para indicar que a função falhou; lançar uma exceção é a mesma coisa no seu código. Não considerar as falhas ou testar como lidar com elas não funcionará a longo prazo, porque, se essas condições ocorrerem, elas não serão tratadas e poderão causar vários problemas.

    
por 24.09.2016 / 04:07
fonte