No momento, há um debate em nossa equipe sobre se modificar o design do código para permitir o teste de unidade é um cheiro de código ou até que ponto isso pode ser feito sem ser um cheiro de código. Isso aconteceu porque estamos apenas começando a implementar práticas que estão presentes em praticamente todas as outras empresas de desenvolvimento de software.
Especificamente, teremos um serviço de API da Web que será muito fino. Sua principal responsabilidade será organizar solicitações / respostas da Web e chamar uma API subjacente que contenha a lógica de negócios.
Um exemplo é que planejamos criar uma fábrica que retornará um tipo de método de autenticação. Não precisamos herdar uma interface, pois não prevemos que ela seja outra coisa senão o tipo concreto que será. No entanto, para testar o serviço de API da Web, precisamos zombar desta fábrica.
Isso significa essencialmente que projetamos a classe do controlador da API da Web para aceitar DI (por meio de seu construtor ou setter), o que significa que estamos projetando parte do controlador apenas para permitir o DI e implementando uma interface que de outra forma não precisamos, ou usamos uma estrutura de terceiros como o Ninject para evitar a necessidade de projetar o controlador dessa maneira, mas ainda precisamos criar uma interface.
Alguns membros da equipe parecem relutantes em criar código apenas para testar. Parece-me que deve haver algum compromisso se você espera fazer um teste de unidade, mas não tenho certeza de como acalmar suas preocupações.
Só para esclarecer, este é um projeto totalmente novo, portanto não se trata de modificar o código para permitir o teste de unidade; trata-se de projetar o código que vamos escrever para ser testado por unidade.
Respostas:
A relutância em modificar o código para fins de teste mostra que um desenvolvedor não entendeu o papel dos testes e, implicitamente, seu próprio papel na organização.
O negócio de software gira em torno da entrega de uma base de código que cria valor comercial. Descobrimos, através de uma longa e amarga experiência, que não podemos criar essas bases de código de tamanho não trivial sem teste. Portanto, os conjuntos de testes são parte integrante dos negócios.
Muitos codificadores prestam pouca atenção a esse princípio, mas inconscientemente nunca o aceitam. É fácil entender por que isso é; a consciência de que nossa capacidade mental não é infinita e, de fato, surpreendentemente limitada quando confrontada com a enorme complexidade de uma base de código moderna, é indesejável e facilmente suprimida ou racionalizada. O fato de o código de teste não ser entregue ao cliente facilita a crença de que ele é um cidadão de segunda classe e não essencial em comparação com o código comercial "essencial". E a ideia de adicionar código de teste ao código comercial parece duplamente ofensiva para muitos.
O problema de justificar essa prática tem a ver com o fato de que toda a imagem de como o valor é criado em um negócio de software é geralmente entendida apenas por altos na hierarquia da empresa, mas essas pessoas não têm o entendimento técnico detalhado de o fluxo de trabalho de codificação necessário para entender por que os testes não podem ser eliminados. Portanto, eles são pacificados com muita frequência por profissionais que garantem que o teste pode ser uma boa ideia em geral, mas "somos programadores de elite que não precisam de muletas assim" ou que "não temos tempo para isso no momento" etc. etc. O fato de o sucesso nos negócios ser um jogo de números e evitar dívidas técnicas, assegurando qualidade etc. mostra seu valor somente a longo prazo, significa que eles geralmente são bastante sinceros nessa crença.
Para encurtar a história: tornar o código testável é uma parte essencial do processo de desenvolvimento, não diferente de outros campos (muitos microchips são projetados com uma proporção substancial de elementos apenas para fins de teste), mas é muito fácil ignorar as boas razões para este. Não caia nessa armadilha.
fonte
Não é tão simples quanto você imagina. Vamos dividir.
MAS!
Qualquer alteração no seu código pode introduzir um erro. Portanto, alterar o código sem uma boa razão comercial não é uma boa ideia.
Seu webapi 'muito fino' não parece ser o melhor argumento para testes de unidade.
Alterar código e testes ao mesmo tempo é uma coisa ruim.
Eu sugeriria a seguinte abordagem:
Escreva testes de integração . Isso não deve exigir nenhuma alteração no código. Ele fornecerá seus casos de teste básicos e permitirá que você verifique se outras alterações no código que você faz não apresentam bugs.
Verifique se o novo código é testável e possui testes de unidade e integração.
Verifique se sua cadeia de IC executa testes após compilações e implantações.
Quando você tiver essas coisas configuradas, só então comece a pensar em refatorar projetos herdados para testabilidade.
Espero que todos tenham aprendido lições do processo e tenham uma boa idéia de onde o teste é mais necessário, como você deseja estruturá-lo e o valor que ele traz para os negócios.
EDIT : Desde que escrevi esta resposta, o OP esclareceu a pergunta para mostrar que eles estão falando sobre novo código, não modificações no código existente. Talvez eu tenha pensado ingenuamente que "o teste de unidade é bom?" argumento foi resolvido há alguns anos.
É difícil imaginar que alterações de código seriam necessárias para testes de unidade, mas não seria uma boa prática geral que você desejaria em qualquer caso. Provavelmente seria sensato examinar as objeções reais, possivelmente o estilo de teste de unidade que está sendo contestado.
fonte
Projetar código para ser inerentemente testável não é um cheiro de código; pelo contrário, é o sinal de um bom design. Existem vários padrões de design conhecidos e amplamente utilizados baseados neste (por exemplo, Model-View-Presenter) que oferecem testes fáceis (mais fáceis) como uma grande vantagem.
Portanto, se você precisar escrever uma interface para sua classe concreta para testá-la com mais facilidade, isso é uma coisa boa. Se você já possui a classe concreta, a maioria dos IDEs pode extrair uma interface, tornando o esforço necessário mínimo. É um pouco mais trabalhoso manter os dois em sincronia, mas uma interface não deve mudar muito de qualquer maneira, e os benefícios dos testes podem compensar esse esforço extra.
Por outro lado, como @MatthieuM. mencionado em um comentário, se você estiver adicionando pontos de entrada específicos ao seu código que nunca deveriam ser usados na produção, apenas para fins de teste, isso pode ser um problema.
fonte
_ForTest
) e verifique a base de código para obter chamadas de código que não seja de teste.É muito simples entender que para criar testes de unidade, o código a ser testado deve ter pelo menos determinadas propriedades. Por exemplo, se o código não consistir em unidades individuais que possam ser testadas isoladamente, a palavra "teste de unidade" nem começará a fazer sentido. Se o código não tiver essas propriedades, ele deverá ser alterado primeiro, isso é bastante óbvio.
Disse que, em teoria, pode-se tentar escrever alguma unidade de código testável primeiro, aplicando todos os princípios do SOLID e, em seguida, tentar escrever um teste para ela posteriormente, sem modificar mais o código original. Infelizmente, escrever código que seja realmente testável por unidade nem sempre é simples, portanto é bem provável que haja algumas alterações necessárias que apenas serão detectadas ao tentar criar os testes. Isso é verdade para o código mesmo quando foi escrito com a idéia de teste de unidade em mente, e é definitivamente mais verdadeiro para o código que foi escrito onde "testabilidade de unidade" não estava na agenda no início.
Existe uma abordagem bem conhecida que tenta resolver o problema escrevendo primeiro os testes de unidade - é chamado de Test Driven Development (TDD), e certamente pode ajudar a tornar o código mais testável por unidade desde o início.
Obviamente, a relutância em alterar o código posteriormente para torná-lo testável geralmente ocorre em uma situação em que o código foi testado manualmente primeiro e / ou funciona bem na produção, portanto, a alteração pode realmente introduzir novos bugs, isso é verdade. A melhor abordagem para atenuar isso é criar primeiro um conjunto de testes de regressão (que geralmente pode ser implementado com alterações muito mínimas na base de código), além de outras medidas associadas, como revisões de código ou novas sessões de teste manual. Isso deve dar confiança suficiente para garantir que o redesenho de alguns internos não quebre nada importante.
fonte
Discordo da afirmação (sem fundamento) que você faz:
Isso não é necessariamente verdade. Há muitas maneiras de escrever testes, e há são maneiras de escrever testes de unidade que não envolvem simulações. Mais importante, existem outros tipos de testes, como testes funcionais ou de integração. Muitas vezes é possível encontrar uma "costura de teste" em uma "interface" que não seja uma linguagem de programação OOP
interface
.Algumas perguntas para ajudá-lo a encontrar uma costura de teste alternativa, que pode ser mais natural:
Outra afirmação sem fundamento que você faz é sobre DI:
A injeção de dependência não significa necessariamente criar um novo
interface
. Por exemplo, na causa de um token de autenticação: você pode simplesmente criar um token de autenticação real programaticamente? Em seguida, o teste pode criar esses tokens e injetá-los. O processo de validação de um token depende de algum segredo criptográfico? Espero que você não tenha codificado um segredo - espero que você possa lê-lo do armazenamento de alguma forma e, nesse caso, você pode simplesmente usar um segredo diferente (conhecido) em seus casos de teste.Isso não quer dizer que você nunca deve criar um novo
interface
. Mas não se apegue a existir apenas uma maneira de escrever um teste ou uma maneira de fingir um comportamento. Se você pensa fora da caixa, normalmente pode encontrar uma solução que exigirá um mínimo de contorções do seu código e ainda assim lhe dê o efeito desejado.fonte
Você está com sorte, pois este é um novo projeto. Descobri que o Test Driven Design funciona muito bem para escrever um bom código (e é por isso que o fazemos em primeiro lugar).
Ao descobrir de antemão como invocar um determinado pedaço de código com dados de entrada realistas e obter dados de saída realistas que você pode verificar como pretendido, você faz o design da API muito cedo no processo e tem uma boa chance de obter um design útil porque você não é prejudicado pelo código existente que precisa ser reescrito para acomodar. Além disso, é mais fácil entender por seus colegas, para que você possa ter boas discussões novamente no início do processo.
Observe que "útil" na sentença acima significa não apenas que os métodos resultantes são fáceis de chamar, mas também que você tende a obter interfaces limpas que são fáceis de montar nos testes de integração e a escrever maquetes para.
Considere isso. Especialmente com revisão por pares. Na minha experiência, o investimento de tempo e esforço será devolvido muito rapidamente.
fonte
UserCanChangeTheirPassword
, no teste você chama a função (ainda não existente) para alterar a senha e, em seguida, afirma que a senha foi realmente alterada. Em seguida, você escreve a função, até poder executar o teste e ele não gera exceções nem afirmação errada. Se nesse ponto você tiver um motivo para adicionar qualquer código, esse motivo será usado em outro teste, por exemploUserCantChangePasswordToEmptyString
.CalculateFactorial
que retorna apenas 120 e o teste passa. Esse é o mínimo. Obviamente, também não é o que foi planejado, mas isso significa que você precisa de outro teste para expressar o que foi planejado.Se você precisar modificar o código, esse é o cheiro do código.
Por experiência pessoal, se é difícil escrever testes para meu código, é um código incorreto. Não é um código ruim porque não é executado ou funciona como projetado, é ruim porque não consigo entender rapidamente por que está funcionando. Se eu encontrar um bug, sei que será um trabalho muito doloroso corrigi-lo. O código também é difícil / impossível de reutilizar.
Um bom código (limpo) divide as tarefas em seções menores que são facilmente entendidas rapidamente (ou pelo menos uma boa aparência). Testar essas seções menores é fácil. Também posso escrever testes que testam apenas um pedaço da base de código com a mesma facilidade, se estiver bastante confiante sobre as subseções (a reutilização também ajuda aqui, pois já foi testada).
Mantenha o código fácil de testar, fácil de refatorar e fácil de reutilizar desde o início e você não estará se matando sempre que precisar fazer alterações.
Estou digitando isso enquanto reconstruindo completamente um projeto que deveria ter sido um protótipo descartável em um código mais limpo. É muito melhor acertar desde o início e refatorar o código incorreto o mais rápido possível, em vez de encarar a tela por horas a fio, com medo de tocar em qualquer coisa por medo de quebrar algo que parcialmente funciona.
fonte
Eu diria que escrever código que não pode ser testado em unidade é um cheiro de código. Em geral, se seu código não puder ser testado em unidade, ele não será modular, o que dificulta a compreensão, a manutenção ou o aprimoramento. Talvez se o código é um código colado que realmente só faz sentido em termos de teste de integração, você pode substituir o teste de integração pelo teste de unidade, mas mesmo assim, quando a integração falhar, você terá que isolar o problema e o teste de unidade é uma ótima maneira de faça.
Você diz
Eu realmente não sigo isso. O motivo de ter uma fábrica que cria algo é permitir que você mude de fábrica ou mude o que a fábrica cria facilmente, para que outras partes do código não precisem ser alteradas. Se o seu método de autenticação nunca for alterado, a fábrica ficará sem código. No entanto, se você quiser ter um método de autenticação diferente em teste e em produção, ter uma fábrica que retorne um método de autenticação diferente em teste e em produção é uma ótima solução.
Você não precisa de DI ou Mocks para isso. Você só precisa que sua fábrica ofereça suporte aos diferentes tipos de autenticação e que seja configurável de alguma forma, como em um arquivo de configuração ou variável de ambiente.
fonte
Em todas as disciplinas de engenharia em que posso pensar, há apenas uma maneira de atingir níveis decentes ou mais altos de qualidade:
Contabilizar a inspeção / teste no projeto.
Isso vale para construção, design de chips, desenvolvimento de software e fabricação. Agora, isso não significa que o teste é o pilar em que todos os projetos precisam ser construídos, de maneira alguma. Mas a cada decisão de projeto, os projetistas devem ter clareza sobre os impactos nos custos de teste e tomar decisões conscientes sobre o trade-off.
Em alguns casos, o teste manual ou automatizado (por exemplo, Selenium) será mais conveniente do que os testes de unidade, além de fornecer cobertura de teste aceitável por conta própria. Em casos raros, jogar algo lá fora, quase totalmente não testado, também pode ser aceitável. Mas essas decisões devem ser conscientes caso a caso. Chamar um design que é responsável por testar um "cheiro de código" indica uma séria falta de experiência.
fonte
Descobri que o teste de unidade (e outros tipos de teste automatizado) tendem a reduzir os odores de código e não consigo pensar em um único exemplo em que eles introduzem odores de código. Os testes de unidade geralmente forçam você a escrever um código melhor. Se você não pode usar um método facilmente em teste, por que deveria ser mais fácil no seu código?
Testes de unidade bem escritos mostram como o código se destina a ser usado. Eles são uma forma de documentação executável. Já vi testes de unidade excessivamente escritos e hediondos que simplesmente não podiam ser entendidos. Não escreva isso! Se você precisar escrever testes longos para configurar suas aulas, elas precisarão ser refatoradas.
Os testes de unidade destacarão onde estão alguns dos cheiros do seu código. Eu recomendaria a leitura de Michael C. Feathers ' Working Effective with Legacy Code' . Mesmo que seu projeto seja novo, se ele ainda não possui nenhum (ou muitos) testes de unidade, você pode precisar de algumas técnicas não óbvias para obter um bom teste do seu código.
fonte
Em poucas palavras:
Código testável é (geralmente) código de manutenção - ou melhor, código difícil de testar é geralmente difícil de manter. Projetar código que não pode ser testado é semelhante a projetar uma máquina que não pode ser reparada - tenha pena do pobre coitado que será designado para repará-lo eventualmente (pode ser você).
Você sabe que precisará de cinco tipos diferentes de métodos de autenticação em três anos, agora que você disse isso, certo? Os requisitos mudam e, embora você deva evitar a engenharia excessiva de seu design, ter um design testável significa que seu design possui (apenas) costuras suficientes para serem alteradas sem (muita) dor - e que os testes do módulo fornecerão meios automatizados para verificar se suas alterações não quebram nada.
fonte
Projetar em torno da injeção de dependência não é um cheiro de código - é uma prática recomendada. Usar o DI não é apenas para testabilidade. Construir seus componentes em torno do DI auxilia na modularidade e na reutilização, permitindo mais facilmente a troca de componentes principais (como uma camada de interface de banco de dados). Embora ele acrescente um certo grau de complexidade, feito corretamente, permite uma melhor separação de camadas e isolamento da funcionalidade, o que facilita o gerenciamento e a navegação da complexidade. Isso facilita a validação correta do comportamento de cada componente, reduzindo bugs e também a localização de bugs.
fonte
Vejamos a diferença entre um testável:
e controlador não testável:
A primeira opção possui literalmente 5 linhas de código extras, duas das quais podem ser geradas automaticamente pelo Visual Studio. Depois de configurar sua estrutura de injeção de dependência para substituir um tipo concreto
IMyDependency
no tempo de execução - que para qualquer estrutura de DI decente é outra linha de código - tudo funciona, exceto agora, mas agora você pode zombar e testar seu controlador de acordo com o conteúdo do seu coração .6 linhas extras de código para permitir a testabilidade ... e seus colegas estão argumentando que isso é "muito trabalho"? Esse argumento não voa comigo, e não deve voar com você.
E você não precisa criar e implementar uma interface para teste: o Moq , por exemplo, permite simular o comportamento de um tipo concreto para fins de teste de unidade. Obviamente, isso não será muito útil se você não puder injetar esses tipos nas classes que está testando.
A injeção de dependência é uma daquelas coisas que, quando você a entende, se pergunta "como eu trabalhei sem isso?". É simples, é eficaz e faz sentido. Por favor, não permita que a falta de compreensão de seus colegas atrapalhe o teste de seu projeto.
fonte
Quando escrevo testes de unidade, começo a pensar no que poderia dar errado dentro do meu código. Isso me ajuda a melhorar o design do código e a aplicar o princípio de responsabilidade única (SRP). Além disso, quando volto para modificar o mesmo código, alguns meses depois, ajuda-me a confirmar que a funcionalidade existente não está corrompida.
Há uma tendência de usar funções puras o máximo possível (aplicativos sem servidor). O teste de unidade me ajuda a isolar o estado e escrever funções puras.
Escreva testes de unidade para a API subjacente primeiro e, se você tiver tempo de desenvolvimento suficiente, também precisará escrever testes para o serviço de API da Web thin.
O teste de unidade TL; DR ajuda a melhorar a qualidade do código e ajuda a fazer alterações futuras no código sem risco. Também melhora a legibilidade do código. Use testes em vez de comentários para fazer o seu ponto.
fonte
A linha inferior, e qual deve ser o seu argumento com o grupo relutante, é que não há conflito. O grande erro parece ter sido que alguém cunhou a idéia de "projetar para teste" para pessoas que odeiam o teste. Eles deveriam ter calado a boca ou dizer palavras de maneira diferente, como "vamos dedicar um tempo para fazer isso direito".
A idéia de que "você precisa implementar uma interface" para tornar algo testável está errada. A interface já está implementada, mas ainda não está declarada na declaração de classe. É uma questão de reconhecer métodos públicos existentes, copiar suas assinaturas para uma interface e declarar essa interface na declaração da classe. Sem programação, sem alterações na lógica existente.
Aparentemente, algumas pessoas têm uma idéia diferente sobre isso. Eu sugiro que você tente consertar isso primeiro.
fonte