É uma prática ruim modificar o código estritamente para fins de teste

74

Eu tenho um debate com um colega de programador sobre se é uma prática boa ou ruim modificar um trecho de código de trabalho apenas para torná-lo testável (por meio de testes de unidade, por exemplo).

Minha opinião é que está tudo bem, dentro dos limites de manter boas práticas de engenharia orientada a objeto e de software (não "tornar tudo público", etc.).

A opinião do meu colega é que modificar o código (que funciona) apenas para fins de teste está errado.

Apenas um exemplo simples, pense neste pedaço de código que é usado por algum componente (escrito em C #):

public void DoSomethingOnAllTypes()
{
    var types = Assembly.GetExecutingAssembly().GetTypes();

    foreach (var currentType in types)
    {
        // do something with this type (e.g: read it's attributes, process, etc).
    }
}

Eu sugeri que esse código possa ser modificado para chamar outro método que fará o trabalho real:

public void DoSomething(Assembly asm)
{
    // not relying on Assembly.GetExecutingAssembly() anymore...
}

Este método leva em um objeto Assembly para trabalhar, tornando possível passar sua própria montagem para fazer o teste. Meu colega não achou isso uma boa prática.

O que é considerado uma prática boa e comum?

    
por liortal 22.05.2013 / 22:33
fonte

11 respostas

132

A modificação de código para torná-lo mais testável tem benefícios além da testabilidade. Em geral, código mais testável

  • É mais fácil de manter,
  • É mais fácil raciocinar,
  • é mais fracamente acoplado e
  • Tem um design geral melhor, arquitetonicamente.
por 22.05.2013 / 22:43
fonte
58

Existem (aparentemente) forças opostas em jogo.

  • Por um lado, você quer reforçar o encapsulamento
  • Por outro lado, você quer poder testar o software
Os proponentes de manter todos os detalhes da implementação privados são geralmente motivados pelo desejo de manter o encapsulamento. No entanto, manter tudo bloqueado e indisponível é uma abordagem mal entendida ao encapsulamento. Se manter tudo indisponível era o objetivo final, o único verdadeiro código encapsulado seria:

static void Main(string[] args)

O seu colega está propondo que este seja o único ponto de acesso em seu código? Todos os outros códigos devem ficar inacessíveis por chamadores externos?

Dificilmente. Então, o que torna certo para tornar alguns métodos públicos? Não é, no final, uma decisão subjetiva de design?

Não é bem assim. O que tende a guiar os programadores, mesmo em um nível inconsciente, é, novamente, o conceito de encapsulamento. Você se sente seguro ao expor um método público quando protege adequadamente suas invariantes .

Eu não gostaria de expor um método privado que não protege seus invariantes, mas muitas vezes você pode modificá-lo de modo que faça proteger suas invariantes, e então exponha ao público (claro, com o TDD, você faz o contrário)

Abrir uma API para testabilidade é uma coisa boa , porque o que você é Realmente está aplicando o Princípio Aberto / Fechado .

Se você tem apenas um único chamador da sua API, não sabe o quanto sua API é flexível. As chances são, é bastante inflexível. Os testes funcionam como um segundo cliente, dando a você um feedback valioso sobre a flexibilidade de sua API.

>

Assim, se os testes sugerirem que você deve abrir sua API, faça-o; mas mantenha o encapsulamento, não escondendo a complexidade, mas expondo a complexidade de uma maneira à prova de falhas.

    
por 23.05.2013 / 08:16
fonte
21

Parece que você está falando sobre injeção de dependência . É muito comum, e IMO, bastante necessário para testabilidade.

Para abordar a questão mais ampla de saber se é uma boa ideia modificar o código apenas para torná-lo testável, pense assim: o código tem várias responsabilidades, incluindo a) ser executado, b) ser lido por humanos e c) para ser testado. Todos os três são importantes, e se o seu código não cumpre todas as três responsabilidades, então eu diria que não é um código muito bom. Então modifique a distância!

    
por 22.05.2013 / 22:43
fonte
13

É um problema de galinha e ovo.

Uma das maiores razões pelas quais é bom ter uma boa cobertura de teste do seu código é que ele permite refatorar sem medo. Mas você está em uma situação em que precisa refatorar o código para obter uma boa cobertura de teste! E seu colega está com medo.

Eu vejo o ponto de vista do seu colega. Você tem código que (presumivelmente) funciona, e se você for refatorá-lo - por qualquer motivo - há o risco de você quebrá-lo.

Mas se esse for o código que deverá ter manutenção e modificação contínuas, você estará correndo esse risco toda vez que fizer qualquer trabalho nele. E refatorar agora e obter alguma cobertura de teste agora permitirá que você corra esse risco, sob condições controladas, e coloque o código em melhor forma para futuras modificações.

Então, eu diria que, a menos que essa base de código específica seja razoavelmente estática e não se espere que um trabalho significativo seja feito no futuro, o que você deseja fazer é uma boa prática técnica.

É claro que, seja uma boa prática de negócio , existe uma grande quantidade de worms.

    
por 23.05.2013 / 05:29
fonte
7

Isso pode ser apenas uma diferença na ênfase das outras respostas, mas eu diria que o código não deve ser refatorado estritamente para melhorar a testabilidade. A testabilidade é altamente importante para a manutenção, mas a testabilidade não é um fim em si mesma. Como tal, eu adiaria qualquer dessas refatorações até que você possa prever que esse código precisará de manutenção para promover alguns negócios finais.

No ponto em que você determinar que esse código exigirá alguma manutenção, esse seria um bom momento para refatorar a capacidade de teste. Dependendo do seu caso de negócio, pode ser uma suposição válida de que o código todo acabará exigindo alguma manutenção, nesse caso a distinção que eu desenho com as outras respostas aqui ( por exemplo < uma resposta de Jason Swett desaparece.

Resumindo: a testabilidade sozinha não é (IMO) uma razão suficiente para refatorar uma base de código. A testabilidade tem um papel valioso ao permitir a manutenção em uma base de código, mas é um requisito de negócios modificar a função do código que deve orientar sua refatoração. Se não houver esse requisito comercial, provavelmente seria melhor trabalhar em algo com o qual seus clientes se preocupam.

(O novo código, é claro, está sendo mantido ativamente, por isso deve ser escrito para ser testável.)

    
por 23.05.2013 / 13:51
fonte
2

Eu acho que seu colega está errado.

Outros mencionaram as razões pelas quais isso já é bom, mas desde que você tenha a permissão para fazer isso, você deve estar bem.

A razão para esta ressalva é que fazer qualquer alteração no código vem com o custo do código ter que ser testado novamente. Dependendo do que você faz, esse trabalho de teste pode realmente ser um grande esforço por si só.

Não é necessariamente o seu lugar para tomar a decisão de refatorar versus trabalhar em novos recursos que beneficiarão sua empresa / cliente.

    
por 24.05.2013 / 17:13
fonte
2

Eu usei ferramentas de cobertura de código como parte do teste de unidade para verificar se todos os caminhos do código são exercitados. Como um bom codificador / testador por conta própria, geralmente cobre 80-90% dos caminhos de código.

Quando estudo os caminhos descobertos e faço um esforço para alguns deles, descobri bugs como casos de erro que "nunca acontecem". Então, sim, modificar o código e verificar a cobertura de teste torna o código melhor.

    
por 24.05.2013 / 16:59
fonte
2

Seu problema aqui é que suas ferramentas de teste são uma porcaria. Você deve ser capaz de ridicularizar esse objeto e chamar seu método de teste sem alterá-lo - porque, embora esse exemplo simples seja realmente simples e fácil de modificar, o que acontece quando você tem algo muito mais complicado.

Muitas pessoas modificaram seu código para introduzir classes baseadas em IoC, DI e interface simplesmente para habilitar o teste de unidade usando as ferramentas de simulação e testes unitários que exigem essas alterações de código. Eu não finjo que eles são uma coisa saudável, não quando você vê um código que é direto e simples, transforma-se em um pesadelo de interações complexas conduzidas inteiramente pela necessidade de tornar todo e qualquer método de classe totalmente dissociado de tudo o mais. . E para acrescentar insulto à injúria, temos muitos argumentos sobre se os métodos privados devem ser testados ou não! (claro que devem, qual é o objetivo do teste unitário se você testar apenas parte do seu sistema), mas esses argumentos são mais direcionados do ponto de vista de que é difícil testar esses métodos usando as ferramentas existentes - imagine se sua ferramenta de teste pudesse executar testes contra um método privado tão facilmente quanto um método público - todos estariam testando-os sem reclamar.

O problema, naturalmente, é a natureza das ferramentas de teste.

Existem ferramentas melhores disponíveis agora que poderiam colocar essas mudanças de design na cama para sempre. A Microsoft tem Fakes (nee Moles) que permite a você esboçar objetos concretos, incluindo objetos estáticos, você não precisa mais alterar seu código para se ajustar à ferramenta. No seu caso, se você usasse o Fakes, substituiria a chamada GetTypes pela sua própria e retornaria dados de teste válidos e inválidos - o que é muito importante, sua alteração sugerida não fornece nada disso.

Para responder: seu colega está certo, mas possivelmente pelas razões erradas. Não altere o código para teste, altere sua ferramenta de teste (ou toda a sua estratégia de teste para ter mais testes de unidade no estilo de integração em vez de testes tão refinados).

Martin Fowler tem uma discussão sobre essa área em seu artigo Mocks não são stubs

    
por 15.09.2013 / 14:09
fonte
1

Uma prática boa e comum é usar testes de unidade e registros de depuração . Os testes unitários garantem que, se você fizer mais alterações no programa, sua funcionalidade antiga não será quebrada. Logs de depuração podem ajudá-lo a rastrear o programa em tempo de execução.
Às vezes acontece que, mesmo além disso, precisamos ter algo apenas para fins de teste. Não é incomum mudar o código para isso. Mas o cuidado deve ser tomado de tal forma que o código de produção não seja afetado por causa disso. Em C ++ e C isso é conseguido usando a MACRO , que é a entidade de tempo de compilação. Em seguida, o código de teste não entra em cena no ambiente de produção. Não sei se tal provisão existe em C #.
Além disso, quando você adiciona código de teste em seu programa, deve ser claramente visível que essa parte do código seja adicionada para fins de teste. Ou então o desenvolvedor tentando entender o código simplesmente vai suar essa parte do código.

    
por 23.05.2013 / 08:21
fonte
1

Existem algumas diferenças sérias entre seus exemplos. No caso de DoSomethingOnAllTypes() , há uma implicação de que do something é aplicável a tipos no conjunto atual. Mas DoSomething(Assembly asm) indica explicitamente que você pode passar qualquer assembly para ele.

A razão pela qual eu apontei isto é que muitos passos de dependência-injeção-por-teste-somente ultrapassam os limites do objeto original. Eu sei que você disse " não 'tornando tudo público' ", mas esse é um dos maiores erros desse modelo, seguido de perto por este: abrir os métodos do objeto até os usos aos quais eles não se destinam .

    
por 24.05.2013 / 00:03
fonte
0

Sua pergunta não deu muito contexto em que seu colega argumentou para que haja espaço para especulação

"má prática" ou não depende de como e quando as alterações são feitas.

Na minha opinião, seu exemplo para extrair um método DoSomething(types) é ok.

Mas eu vi o código que é não ok assim:

public void DoSomethingOnAllTypes()
{
  var types = (!testmode) 
      ? Assembly.GetExecutingAssembly().GetTypes() 
      : getTypesFromTestgenerator();

  foreach (var currentType in types)
  {
     if (!testmode)
     {
        // do something with this type that made the unittest fail and should be skipped.
     }
     // do something with this type (e.g: read it's attributes, process, etc).
  }
}

Essas alterações tornaram o código mais difícil de entender porque você aumentou o número de possíveis caminhos de código.

O que quero dizer com como e quando :

Se você tem uma implementação em funcionamento e por causa da "implementação de testes de capacidades" você fez as mudanças, então você tem que testar novamente o seu aplicativo porque você pode ter quebrado o seu método DoSomething() .

O if (!testmode) é mais difícil de entender e testar do que o método extraído.

    
por 14.09.2013 / 13:09
fonte