Eu tenho um debate com um colega programador sobre se é uma prática boa ou ruim modificar um trecho de código apenas para torná-lo testável (por meio de testes de unidade, por exemplo).
Minha opinião é de que está tudo bem, dentro dos limites de manter boas práticas orientadas a objetos e de engenharia 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 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).
}
}
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...
}
Esse método utiliza um objeto Assembly para trabalhar, possibilitando passar seu próprio Assembly para fazer os testes. Meu colega não achou que isso fosse uma boa prática.
O que é considerado uma prática boa e comum?
Type
como parâmetro, não comoAssembly
.Respostas:
Modificar o código para torná-lo mais testável tem benefícios além da testabilidade. Em geral, código mais testável
fonte
Existem (aparentemente) forças opostas em jogo.
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 incompreendida do encapsulamento. Se manter tudo indisponível fosse o objetivo final, o único código encapsulado verdadeiro seria este:
Seu colega está propondo tornar esse o único ponto de acesso no seu código? Todos os outros códigos devem estar inacessíveis por chamadores externos?
Dificilmente. Então, o que torna aceitável tornar alguns métodos públicos? Não é, no final, uma decisão subjetiva de design?
Não é bem assim. O que tende a orientar os programadores, mesmo em um nível inconsciente, é, novamente, o conceito de encapsulamento. Você se sente seguro em expor um método público quando ele protege adequadamente seus invariantes .
Eu não gostaria de expor um método particular que não protege seus invariantes, mas muitas vezes você pode modificá-lo para que ele não proteger seus invariantes, e , em seguida, expô-la ao público (claro, com TDD, você fazê-lo da ao contrário).
Abrir uma API para testabilidade é uma coisa boa , porque o que você realmente está fazendo é aplicar o Princípio Aberto / Fechado .
Se você tem apenas um chamador da sua API, não sabe o quão flexível ela é. As chances são de que seja bastante inflexível. Os testes agem como um segundo cliente, fornecendo um feedback valioso sobre a flexibilidade da sua API .
Portanto, se os testes sugerem que você deve abrir sua API, faça-o; mas mantenha o encapsulamento, não ocultando a complexidade, mas expondo a complexidade de maneira segura.
fonte
Parece que você está falando sobre injeção de dependência . É realmente comum e IMO, bastante necessário para a testabilidade.
Para abordar a questão mais ampla de saber se é uma boa idéia modificar o código apenas para torná-lo testável, pense da seguinte maneira: o código tem várias responsabilidades, incluindo a) a ser executado, b) lido por humanos ec) para ser testado. Todos os três são importantes e, se o seu código não cumprir todas as três responsabilidades, eu diria que não é um código muito bom. Então modifique-o!
fonte
É 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 um código que (presumivelmente) funciona, e se você o refatorar - por qualquer motivo - há um risco de que você o violará.
Mas se esse é um código que deve ter manutenção e modificação contínuas, você estará correndo esse risco toda vez que fizer algum trabalho nele. E refatorar agora e obter 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 haja um trabalho significativo no futuro, o que você deseja fazer é uma boa prática técnica.
É claro que, se é uma boa prática comercial , é uma lata de minhocas completa.
fonte
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 refatoração desse tipo até que você possa prever que esse código precisará de manutenção para promover alguma finalidade comercial.
No ponto em que você determinar que este código vai exigir alguma manutenção, que seria um bom momento para refatorar para testabilidade. Dependendo do seu caso de negócios, pode ser uma suposição válida de que todo o código exigirá alguma manutenção, nesse caso a distinção que eu faço com as outras respostas aqui ( por exemplo, a resposta de Jason Swett ) desaparece.
Resumindo: a testabilidade por si só 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 comercial modificar a função do seu código que deve impulsionar sua refatoração. Se não houver esse requisito comercial, provavelmente seria melhor trabalhar em algo que seus clientes se importam.
(É claro que o novo código está sendo mantido ativamente, portanto deve ser escrito para ser testado.)
fonte
Eu acho que seu colega está errado.
Outros já mencionaram as razões pelas quais isso já é uma coisa boa, mas, desde que você tenha permissão para fazer isso, deve ficar bem.
A razão para esta ressalva é que fazer qualquer alteração no código tem que custar um novo teste. Dependendo do que você faz, esse trabalho de teste pode, na verdade, ser um grande esforço por si só.
Não é necessariamente o seu lugar tomar a decisão de refatorar em vez de trabalhar em novos recursos que beneficiarão sua empresa / cliente.
fonte
Eu usei ferramentas de cobertura de código como parte do teste de unidade para verificar se todos os caminhos através do código são exercidos. Como um bom codificador / testador por conta própria, eu costumo cobrir 80-90% dos caminhos de código.
Quando estudo os caminhos descobertos e faço um esforço para alguns deles, é quando descubro bugs, como casos de erro que "nunca acontecerão". Então, sim, modificar o código e verificar a cobertura do teste cria um código melhor.
fonte
Seu problema aqui é que suas ferramentas de teste são uma porcaria. Você deve conseguir zombar desse objeto e chamar seu método de teste sem alterá-lo - porque, embora este 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 de IoC, DI e interface, simplesmente para permitir o teste de unidade usando as ferramentas de simulação e teste de unidade que exigem essas alterações de código. Eu não acho que eles são uma coisa saudável, não quando você vê um código bastante direto e simples se transformar em um pesadelo de interações complexas impulsionadas inteiramente pela necessidade de tornar cada método de classe totalmente dissociado de todo o resto . E, para acrescentar insulto à lesão, temos muitos argumentos sobre se métodos privados devem ser testados em unidade ou não! (é claro que deveriam, o que '
O problema, é claro, é da natureza das ferramentas de teste.
Agora existem ferramentas melhores disponíveis que podem colocar essas alterações de design para sempre. A Microsoft possui Fakes (nee Moles) que permitem stub objetos concretos, incluindo estáticos, para que você não precise mais alterar seu código para ajustar-se à ferramenta. No seu caso, se você usasse o Fakes, substituiria a chamada GetTypes pela sua, que retornou dados de teste válidos e inválidos - o que é bastante importante, sua alteração sugerida não fornece isso.
Para responder: seu colega está certo, mas possivelmente pelos motivos errados. Não altere o código para testar, 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 detalhados).
Martin Fowler tem uma discussão sobre essa área em seu artigo Mocks are not Stubs
fonte
Uma prática boa e comum é usar logs de teste e depuração de unidade . Os testes de unidade garantem que, se você fizer mais alterações no programa, sua funcionalidade antiga não será interrompida. Os logs de depuração podem ajudar a rastrear o programa em tempo de execução.
Às vezes acontece que, além disso, precisamos ter algo apenas para fins de teste. Não é incomum alterar o código para isso. Mas deve-se tomar cuidado para que o código de produção não seja afetado por causa disso. Em C ++ e C, isso é alcançado usando o MACRO , que é uma 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 essa disposição existe em C #.
Além disso, quando você adiciona código de teste ao seu programa, ele deve estar claramente visívelque esta parte do código é adicionada para algum propósito de teste. Ou então, o desenvolvedor que está tentando entender o código simplesmente suará essa parte do código.
fonte
Existem algumas diferenças sérias entre seus exemplos. No caso de
DoSomethingOnAllTypes()
, há uma implicaçãodo something
aplicável aos tipos na montagem atual. MasDoSomething(Assembly asm)
indica explicitamente que você pode passar qualquer montagem para ele.A razão pela qual indico isso é que muitas etapas de injeção de dependência apenas para teste estão fora dos 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: abrindo os métodos do objeto até usos para os quais eles não se destinam.
fonte
Sua pergunta não deu muito contexto em que seu colega argumentou para que haja espaço para especulações
"má prática" ou não depende de como e quando as alterações são feitas.
Na minha opção, seu exemplo para extrair um método
DoSomething(types)
está ok.Mas eu vi o código que não está bem assim:
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, para "implementar recursos de teste", fez as alterações, você deve testar novamente seu aplicativo porque pode ter quebrado seu
DoSomething()
método.A
if (!testmode)
é mais mais difficulilt de compreender e de teste do que o método extraída.fonte