Devemos projetar nosso código desde o início para permitir o teste de unidade?


91

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.


33
Permita-me repetir: seus colegas desejam testes de unidade para um novo código, mas eles se recusam a escrever o código de uma maneira que seja testável por unidade, embora não exista risco de quebrar qualquer coisa existente? Se isso for verdade, você deve aceitar a resposta de @ KilianFoth e pedir que ele destaque a primeira frase da resposta em negrito! Seus colegas aparentemente têm um grande mal-entendido sobre qual é o trabalho deles.
Doc Brown

20
@ Lee: Quem disse que a dissociação é sempre uma boa ideia? Você já viu uma base de código na qual tudo é passado como uma interface criada a partir de uma fábrica de interfaces usando alguma interface de configuração? Eu tenho; foi escrito em Java e era uma bagunça completa, inatingível e com erros. Desacoplamento extremo é ofuscação do código.
Christian Hackl

8
O trabalho eficaz de Michael Feathers com o código herdado lida muito bem com esse problema e deve fornecer uma boa idéia sobre as vantagens dos testes, mesmo em uma nova base de código.
l0b0 30/01

8
@ l0b0 Essa é praticamente a Bíblia para isso. No stackexchange, não seria uma resposta para a pergunta, mas no RL eu diria ao OP para ler este livro (pelo menos em parte). OP, comece a trabalhar efetivamente com o código legado e leia-o, pelo menos em parte (ou peça ao seu chefe para obtê-lo). Ele aborda questões como estas. Especialmente se você não fez os testes e agora está participando - você pode ter 20 anos de experiência, mas agora fará coisas com as quais não tem experiência . É muito mais fácil ler sobre eles do que aprender meticulosamente tudo isso por tentativa e erro.
R. Schmitz

4
Obrigado pela recomendação do livro de Michael Feathers, eu definitivamente pegarei uma cópia.
Lee

Respostas:


204

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.


39
Eu argumentaria que depende do tipo de mudança. Há uma diferença entre tornar o código mais fácil de testar e introduzir ganchos específicos de teste que NUNCA devem ser usados ​​na produção. Pessoalmente, sou cauteloso com o último, porque Murphy ...
Matthieu M.

61
Os testes de unidade geralmente quebram o encapsulamento e tornam o código em teste mais complexo do que seria necessário (por exemplo, introduzindo tipos de interface adicionais ou adicionando sinalizadores). Como sempre na engenharia de software, todas as boas práticas e todas as boas regras têm sua parcela de responsabilidades. Produzir cegamente muitos testes de unidade pode ter um efeito prejudicial no valor comercial, sem mencionar que escrever e manter os testes já custa tempo e esforço. Na minha experiência, os testes de integração têm um ROI muito maior e tendem a melhorar as arquiteturas de software com menos compromissos.
Christian Hackl

20
@Lee Claro, mas você precisa considerar se ter um tipo específico de teste justifica o aumento da complexidade do código. Minha experiência pessoal é que os testes de unidade são uma ótima ferramenta até o ponto em que exigem mudanças fundamentais no projeto para acomodar a zombaria. É aí que eu mudo para um tipo diferente de teste. Escrever testes de unidade às custas de tornar a arquitetura substancialmente mais complexa, com o único objetivo de realizar testes de unidade, é um olhar de umbigo.
Konrad Rudolph

21
@ChristianHackl Por que um teste de unidade quebraria o encapsulamento? Descobri que, para o código em que trabalhei, se houver uma necessidade percebida de adicionar funcionalidade extra para permitir o teste, o problema real é que a função que você deve testar precisa ser refatorada, para que toda a funcionalidade seja a mesma nível de abstração (são as diferenças no nível de abstração que geralmente criam essa "necessidade" de código extra), com o código de nível inferior movido para suas próprias funções (testáveis).
Baldrickk

29
Os testes de unidade @ChristianHackl nunca devem interromper o encapsulamento. Se você estiver tentando acessar variáveis ​​privadas, protegidas ou locais a partir de um teste de unidade, está fazendo algo errado. Se você estiver testando a funcionalidade foo, estará testando apenas se realmente funcionou, e não se a variável local x for a raiz quadrada da entrada y na terceira iteração do segundo loop. Se alguma funcionalidade é privada, então, você estará testando-a transitivamente de qualquer maneira. se é realmente grande e privado? Essa é uma falha de design, mas provavelmente nem mesmo é possível fora de C e C ++ com separação de implementação de cabeçalho.
opa

75

Não é tão simples quanto você imagina. Vamos dividir.

  • Escrever testes de unidade é definitivamente uma coisa boa.

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:

  1. 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.

  2. Verifique se o novo código é testável e possui testes de unidade e integração.

  3. 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.


12
Esta é uma resposta muito melhor do que a aceita. O desequilíbrio nos votos é desanimador.
Konrad Rudolph

4
@ Lee Um teste de unidade deve testar uma unidade de funcionalidade , que pode ou não corresponder a uma classe. Uma unidade de funcionalidade deve ser testada em sua interface (que pode ser a API nesse caso). Os testes podem destacar os cheiros do design e a necessidade de aplicar alguma / mais nivelação. Crie seus sistemas a partir de pequenas peças compostáveis, será mais fácil raciocinar e testar.
Wes Toleman 30/01

2
@ KonradRudolph: Acho que não entendi o ponto em que o OP acrescentou que essa pergunta era sobre o design de um novo código, não a alteração de um existente. Portanto, não há nada a quebrar, o que torna a maior parte dessa resposta não aplicável.
Doc Brown

1
Discordo totalmente da afirmação de que escrever testes de unidade é sempre uma coisa boa. Os testes de unidade são bons apenas em alguns casos. É bobagem usar testes de unidade para testar o código de interface (UI), eles são feitos para testar a lógica de negócios. Além disso, é bom escrever testes de unidade para substituir as verificações de compilação ausentes (por exemplo, em Javascript). A maioria dos códigos somente de front-end deve escrever testes de ponta a ponta exclusivamente, não testes de unidade.
Sulthan 31/01

1
Os designs podem sofrer definitivamente com "danos induzidos pelo teste". Geralmente, a testabilidade aprimora o design: você nota ao escrever testes que algo não pode ser buscado, mas deve ser passado, criando interfaces mais claras e assim por diante. Mas, ocasionalmente, você encontrará algo que requer um design desconfortável apenas para teste. Um exemplo pode ser um construtor somente de teste necessário no seu novo código devido ao código de terceiros existente que usa um singleton, por exemplo. Quando isso acontecer: dê um passo atrás e faça apenas um teste de integração, em vez de danificar seu próprio design em nome da capacidade de teste.
Anders Forsgren

18

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.


Esse problema pode ser resolvido através da análise de código estático - sinalize os métodos (por exemplo, deve ser nomeado _ForTest) e verifique a base de código para obter chamadas de código que não seja de teste.
Riking

13

É 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.


Interessante que você mencionou TDD. Estamos tentando trazer o BDD / TDD, que também encontrou alguma resistência - ou seja, o que "o código mínimo a ser passado" realmente significa.
Lee

2
@ Lee: trazer mudanças para uma organização sempre causa alguma resistência, e sempre precisa de algum tempo para adaptar coisas novas, isso não é novidade. Este é um problema das pessoas.
Doc Brown

Absolutamente. Eu só gostaria que tivéssemos recebido mais tempo!
Lee

Freqüentemente, é uma questão de mostrar às pessoas que fazê-lo dessa maneira economizará tempo (e esperançosamente também). Por que algo que não irá beneficiar você?
Thorbjørn Ravn Andersen 02/02

@ ThorbjørnRavnAndersen: A equipe também pode mostrar ao OP que sua abordagem economizará tempo. Quem sabe? Mas me pergunto se realmente não estamos enfrentando problemas de natureza menos técnica aqui; o OP continua vindo aqui para nos dizer o que sua equipe faz de errado (em sua opinião), como se ele estivesse tentando encontrar aliados para sua causa. Talvez seja mais benéfico discutir o projeto em conjunto com a equipe, não com estranhos no Stack Exchange.
Christian Hackl

11

Discordo da afirmação (sem fundamento) que você faz:

para testar o serviço de API da Web, precisaremos zombar dessa fábrica

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:

  • Será que eu vou querer escrever uma API Web fina em uma API diferente ?
  • Posso reduzir a duplicação de código entre a API da Web e a API subjacente? Um pode ser gerado em termos de outro?
  • Posso tratar toda a API da Web e a API subjacente como uma única unidade de "caixa preta" e fazer afirmações significativas sobre como a coisa toda se comporta?
  • Se a API da Web tivesse que ser substituída por uma nova implementação no futuro, como faríamos isso?
  • Se a API da Web fosse substituída por uma nova implementação no futuro, os clientes da API da Web poderiam perceber? Se sim, como?

Outra afirmação sem fundamento que você faz é sobre DI:

projetamos a classe de 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 terceiros estrutura como o Ninject para evitar a necessidade de projetar o controlador dessa maneira, mas ainda precisamos criar uma interface.

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.


Considerações sobre as afirmações sobre interfaces, mas, mesmo que não as usássemos, ainda teríamos que injetar objetos de alguma forma, essa é a preocupação do restante da equipe. ou seja, alguns membros da equipe ficariam satisfeitos com um controle sem parâmetros instanciando a implementação concreta e deixando assim. De fato, um membro apresentou a ideia de usar reflexão para injetar zombarias, para que não tenhamos que projetar código para aceitá-las. Que é um código reeky cheiro imo
Lee

9

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.


Também temos um problema com o TDD, a saber, o que constitui "código mínimo a ser passado". Eu demonstrei para a equipe esse processo e eles fizeram uma exceção para não apenas escrever o que já projetamos - o que eu entendo. O "mínimo" não parece estar definido. Se escrevemos um teste e temos planos e projetos claros, por que não escrever isso para passar no teste?
Lee

@ Lee "código mínimo para passar" ... bem, isso pode parecer um pouco idiota, mas é literalmente o que diz. Por exemplo, se você tiver um teste 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 exemplo UserCantChangePasswordToEmptyString.
R. Schmitz

@Lee Por fim, seus testes acabarão sendo a documentação do que seu código faz, exceto a documentação que verifica se ele foi cumprido, em vez de apenas ser tinta no papel. Compare também com esta pergunta - Um método CalculateFactorialque 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.
R. Schmitz

1
@ Lee Pequenos passos. O mínimo necessário pode ser mais do que você pensa quando o código ultrapassa o trivial. Além disso, o design que você faz ao implementar tudo ao mesmo tempo pode ser menos otimizado, porque você faz suposições sobre como deve ser feito sem ter escrito os testes que o demonstram. Lembre-se novamente, o código deve falhar no início.
Thorbjørn Ravn Andersen

1
Além disso, testes de regressão são muito importantes. Eles estão no escopo da equipe?
Thorbjørn Ravn Andersen

8

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.


3
"Protótipo descartável" - todo projeto começa a vida como um desses ... melhor pensar nas coisas como nunca sendo isso. digitando isso como eu sou .. adivinhem? ... refatorar um protótipo descartável que acabou não sendo;)
Algy Taylor

4
Se você quiser ter certeza de que um protótipo descartável será descartado, escreva-o em uma linguagem de protótipo que nunca será permitida na produção. Clojure e Python são boas escolhas.
Thorbjørn Ravn Andersen 30/01

2
@ ThorbjørnRavnAndersen Isso me fez rir. Isso era para ser uma escavação nessas línguas? :)
Lee

@Lee. Não, apenas exemplos de idiomas que podem não ser aceitáveis ​​para produção - normalmente porque ninguém na organização pode mantê-los porque eles não estão familiarizados com eles e suas curvas de aprendizado são íngremes. Se forem aceitáveis, escolha outra que não seja.
Thorbjørn Ravn Andersen

4

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

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.

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.


2

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.


1

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.


3
Você pode ser tentado a introduzir várias camadas de indireção para poder testar e nunca usá-las conforme o esperado.
Thorbjørn Ravn Andersen

1

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ê).

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á.

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.


1

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.


1
"bem feito" é um problema. Eu tenho que manter dois projetos nos quais o DI foi feito de maneira errada (embora com o objetivo de fazê-lo "corretamente"). Isso torna o código horrível e muito pior do que os projetos herdados sem DI e testes de unidade. Acertar o DI não é fácil.
Jan

@ Jan isso é interessante. Como eles fizeram isso errado?
Lee

1
O projeto @Lee One é um serviço que precisa de um tempo de início rápido, mas é terrivelmente lento no início, porque toda a inicialização da classe é feita antecipadamente pela estrutura do DI (Castle Windsor em C #). Outro problema que vejo nesses projetos é misturar o DI com a criação de objetos com "novo", evitando o DI. Isso torna os testes difíceis novamente e levou a algumas condições desagradáveis ​​de corrida.
Janeiro

1

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.

Vejamos a diferença entre um testável:

public class MyController : Controller
{
    private readonly IMyDependency _thing;

    public MyController(IMyDependency thing)
    {
        _thing = thing;
    }
}

e controlador não testável:

public class MyController : Controller
{
}

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 IMyDependencyno 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.


1
O que você é tão rápido em rejeitar como "falta de entendimento de coisas novas" pode se tornar um bom entendimento das coisas antigas. Injeção de dependência certamente não é nova. A idéia, e provavelmente as primeiras implementações, têm décadas. E sim, acredito que sua resposta é um exemplo de código se tornando mais complicado devido a testes de unidade e, possivelmente, um exemplo de testes de unidade quebrando o encapsulamento (porque quem diz que a classe tem um construtor público em primeiro lugar?). Eu sempre removia a injeção de dependência das bases de código que eu herdara de outra pessoa, por causa das compensações.
Christian Hackl

Os controladores sempre têm um construtor público, implícito ou não, porque o MVC exige. "Complicado" - talvez, se você não entender como os construtores funcionam. Encapsulamento - sim, em alguns casos, mas o debate DI vs encapsulamento é um debate altamente subjetivo e contínuo que não ajudará aqui, e em particular para a maioria das aplicações, o DI servirá melhor que o IMO de encapsulamento.
Ian Kemp

Em relação aos construtores públicos: de fato, essa é uma particularidade da estrutura que está sendo usada. Eu estava pensando no caso mais geral de uma classe comum que não é instanciada por uma estrutura. Por que você acredita que ver parâmetros adicionais do método como complexidade adicionada equivale a uma falta de entendimento sobre como os construtores funcionam? No entanto, aprecio que você reconheça a existência de uma troca entre DI e encapsulamento.
Christian Hackl

0

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.

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.

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.


0

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.