Tutorial de teste de unidade DotNet - Tecnologia
Freqüentemente, há muita confusão e dúvida em relação ao teste de unidade ao discuti-lo com os stakeholders e clientes. O teste de unidade às vezes soa como o uso do fio dental para uma criança, "Eu já escovo os dentes, por que preciso fazer isso?"
A sugestão de teste de unidade muitas vezes soa como uma despesa desnecessária para pessoas que consideram seus métodos de teste e testes de aceitação do usuário fortes o suficiente.
Mas os testes de unidade são uma ferramenta muito poderosa e são mais simples do que você pode imaginar. Neste artigo, daremos uma olhada nos testes de unidade e quais ferramentas estão disponíveis no DotNet, como Microsoft.VisualStudio.TestTools e Moq .
Tentaremos construir uma biblioteca de classes simples que calculará o enésimo termo na sequência de Fibonacci. Para fazer isso, queremos criar uma classe para calcular sequências de Fibonacci que depende de uma classe de matemática personalizada que adiciona os números juntos. Então, podemos usar o .NET Testing Framework para garantir que nosso programa seja executado conforme o esperado.
O teste de unidade divide o programa no menor trecho de código, geralmente em nível de função, e garante que a função retorne o valor esperado. Usando uma estrutura de teste de unidade, os testes de unidade se tornam uma entidade separada que pode então executar testes automatizados no programa conforme ele está sendo construído.
Um teste de unidade simples usando a metodologia Arrange, Act, Assert testando que nossa biblioteca matemática pode adicionar 2 + 2 corretamente.
Uma vez que os testes de unidade são configurados, se uma mudança for feita no código, para levar em conta uma condição adicional que não era conhecida quando o programa foi desenvolvido pela primeira vez, por exemplo, os testes de unidade mostrarão se todos os casos correspondem aos valores esperados saída pela função.
O teste de unidade é não teste de integração. Isto é não teste de ponta a ponta. Embora ambas sejam metodologias poderosas, elas devem funcionar em conjunto com o teste de unidade - não como uma substituição.
Os benefícios e a finalidade do teste de unidade
O benefício mais difícil de entender do teste de unidade, mas o mais importante, é a capacidade de testar novamente o código alterado em tempo real. A razão pela qual pode ser tão difícil de entender é porque muitos desenvolvedores pensam consigo mesmos, "Nunca mais tocarei nessa função", ou 'Vou apenas testá-lo novamente quando terminar.' E as partes interessadas pensam em termos de "Se essa peça já está escrita, por que preciso testá-la novamente?"
Como alguém que esteve em ambos os lados do espectro do desenvolvimento, eu disse essas duas coisas. O desenvolvedor dentro de mim sabe por que temos que testá-lo novamente.
As mudanças que fazemos no dia a dia podem ter impactos enormes. Por exemplo:
O seu switch conta corretamente para um novo valor que você colocou?
Você sabe quantas vezes você usou essa chave?
Você considerou adequadamente as comparações de strings que não diferenciam maiúsculas de minúsculas?
Você está verificando os nulos de forma adequada?
Uma exceção de lançamento é tratada conforme o esperado?
O teste de unidade pega essas perguntas e as memoriza em código e um processo para garantir que essas perguntas sejam sempre respondidas. Os testes de unidade podem ser executados antes de uma construção para garantir que você não introduziu novos bugs. Como os testes de unidade são projetados para serem atômicos, eles são executados muito rapidamente, geralmente menos de 10 milissegundos por teste. Mesmo em um aplicativo muito grande, um conjunto de testes completo pode ser executado em menos de uma hora. O seu processo UAT pode corresponder a isso?
Diferente de Fibonacci_GetNthTerm_Input2_AssertResult1 que é a primeira execução e inclui o tempo de configuração, todos os testes de unidade são executados em 5 ms. Minha convenção de nomenclatura aqui é configurada para pesquisar facilmente uma classe ou método dentro de uma classe que eu desejo testar
Porém, como desenvolvedor, talvez isso soe como mais trabalho para você. Sim, você fica tranquilo sabendo que o código que você está lançando é bom. Mas o teste de unidade também oferece a oportunidade de ver onde seu projeto é fraco. Você está escrevendo os mesmos testes de unidade para duas partes do código? Eles deveriam estar em um pedaço de código em vez disso?
Pegando o seu código para ser testável em unidade em si é uma forma de melhorar seu design. E para a maioria dos desenvolvedores que nunca testou a unidade ou não leva muito tempo para considerar o design antes de codificar, você pode perceber o quanto seu design melhora, tornando-o pronto para o teste de unidade.
A sua unidade de código pode ser testada?
Além do DRY, também temos outras considerações.
Seus métodos ou funções estão tentando fazer muito?
Se você precisar escrever testes de unidade excessivamente complexos que estão sendo executados por mais tempo do que o esperado, seu método pode ser muito complicado e mais adequado como métodos múltiplos.
Você está aproveitando adequadamente a injeção de dependência?
Se o seu método em teste requer outra classe ou função, chamamos isso de dependência. Em testes de unidade, não nos importamos com o que a dependência está fazendo nos bastidores; para o propósito do método em teste, é uma caixa preta. A dependência tem seu próprio conjunto de testes de unidade que determinarão se seu comportamento está funcionando corretamente.
Como testador, você deseja simular essa dependência e dizer a ela quais valores retornar em instâncias específicas. Isso lhe dará maior controle sobre seus casos de teste. Para fazer isso, você precisará injetar uma versão fictícia (ou como veremos mais tarde, simulada) dessa dependência.
Seus componentes interagem uns com os outros como você espera?
Depois de definir suas dependências e injeção de dependências, você pode descobrir que introduziu dependências cíclicas em seu código. Se a Classe A depende da Classe B, que por sua vez depende da Classe A, você deve reconsiderar seu projeto.
A beleza da injeção de dependência
Vamos considerar nosso Exemplo de Fibonacci . Seu chefe diz que eles têm uma nova classe que é mais eficiente e precisa do que o operador de adição atual disponível em C #.
Embora este exemplo específico não seja muito provável no mundo real, vemos exemplos análogos em outros componentes, como autenticação, mapeamento de objeto e praticamente qualquer processo algorítmico. Para os fins deste artigo, vamos apenas fingir que a nova função de adição de seu cliente é a maior e mais recente desde que os computadores foram inventados.
Como tal, seu chefe entrega a você uma biblioteca de caixa preta com uma única classe Math e, nessa classe, uma única função Add. Seu trabalho de implementação de uma calculadora de Fibonacci provavelmente será mais ou menos assim:
Isso não é horrível. Você instancia um novo Math classe e use isso para adicionar os dois termos anteriores para obter o próximo. Você executa esse método por meio de sua bateria normal de testes, calculando até 100 termos, calculando o milésimo termo, o 10.000º termo e assim por diante, até se sentir satisfeito de que sua metodologia funciona bem. Então, em algum momento no futuro, um usuário reclamará que o 501º termo não está funcionando como o esperado. Você passa a noite olhando seu código e tentando descobrir por que este caso não está funcionando. Você começa a suspeitar que o mais recente e o melhor Math a aula não é tão boa quanto seu chefe pensa. Mas é uma caixa preta e você não pode realmente provar isso - você chega a um impasse internamente.
O problema aqui é que a dependência Math não é injetado em sua calculadora de Fibonacci. Portanto, em seus testes, você sempre confia nos resultados existentes, não testados e desconhecidos de Math para testar Fibonacci. Se houver um problema com Math, então Fibonacci estará sempre errado (sem codificar um caso especial para o 501º termo).
A ideia para corrigir esse problema é injetar o Math classe em sua calculadora Fibonacci. Melhor ainda, é criar uma interface para Math classe que define os métodos públicos (em nosso caso, Add) e implementa a interface em nosso Math classe.
Em vez de injetar o Math classe em Fibonacci, podemos injetar o IMath interface em Fibonacci. A vantagem aqui é que podemos definir o nosso próprio OurMath classe que sabemos ser precisa e testar nossa calculadora com ela. Melhor ainda, usando o Moq podemos simplesmente definir o que Math.Add retorna. Podemos definir várias somas ou podemos apenas dizer Math.Add para retornar x + y.
Injetar a interface IMath na classe Fibonacci
Usando Moq para definir o que Math.Add retorna.
Agora temos um método testado e comprovado (bem, se esse operador + estiver errado em C #, temos problemas maiores) para adicionar dois números. Usando nosso novo Mocked IMath, podemos codificar um teste de unidade para nosso 501º termo e ver se erramos em nossa implementação ou se o custom Math a aula precisa de um pouco mais de trabalho.
Não deixe um método tentar fazer muito
Este exemplo também aponta para a ideia de um método que faz muito. Claro, a adição é uma operação bastante simples, sem muita necessidade de abstrair sua funcionalidade de nosso GetNthTerm método. Mas e se a operação fosse um pouco mais complicada? Em vez de adição, talvez fosse a validação do modelo, chamando uma fábrica para obter um objeto para operar ou coletando dados adicionais necessários de um repositório.
A maioria dos desenvolvedores tentará manter a ideia de que um método tem um propósito. Nos testes de unidade, tentamos seguir o princípio de que os testes de unidade devem ser aplicados a métodos atômicos e, ao introduzir muitas operações em um método, o tornamos não testável. Freqüentemente, podemos criar um problema em que precisamos escrever tantos testes para testar adequadamente nossa função.
Cada parâmetro que adicionamos a um método aumenta o número de testes que temos que escrever exponencialmente de acordo com a complexidade do parâmetro. Se você adicionar um booleano à sua lógica, precisará dobrar o número de testes a serem gravados, pois agora você precisa verificar os casos verdadeiro e falso junto com seus testes atuais. No caso de validação de modelo, a complexidade de nossos testes de unidade pode aumentar muito rapidamente.
Todos nós somos culpados de adicionar um pouco mais a um método. Mas esses métodos maiores e mais complexos criam a necessidade de muitos testes de unidade. E rapidamente se torna aparente quando você escreve os testes de unidade que o método está tentando fazer muito. Se você sentir que está tentando testar muitos resultados possíveis de seus parâmetros de entrada, considere o fato de que seu método precisa ser dividido em uma série de outros menores.
Um de nossos locatários favoritos de programação. Este deve ser bastante simples. Se você estiver escrevendo os mesmos testes mais de uma vez, é porque introduziu o código mais de uma vez. Pode ser benéfico para você refatorar esse trabalho em uma classe comum que seja acessível para ambas as instâncias que você está tentando usar.
Quais ferramentas de teste de unidade estão disponíveis?
DotNet nos oferece uma plataforma de teste de unidade muito poderosa pronta para uso. Usando isso, você pode implementar o que é conhecido como o Metodologia Arrange, Act, Assert . Você organiza suas considerações iniciais, age sobre essas condições com seu método em teste e, em seguida, afirma que algo aconteceu. Você pode afirmar qualquer coisa, tornando esta ferramenta ainda mais poderosa. Você pode afirmar que um método foi chamado um número específico de vezes, que o método retornou um valor específico, que um tipo específico de exceção foi lançado ou qualquer outra coisa que você possa imaginar. Para aqueles que procuram uma estrutura mais avançada, NUnit e sua contraparte Java JUnit são opções viáveis.
Testar se nosso Método Fibonacci lida com números negativos lançando uma exceção. Os testes de unidade podem verificar se a exceção foi lançada.
Para lidar com a injeção de dependência, ambos Ninject e Unidade existem na plataforma DotNet. Há muito pouca diferença entre os dois, e é uma questão de se você deseja gerenciar as configurações com a sintaxe fluente ou a configuração XML.
Para simular as dependências, recomendo o Moq. O Moq pode ser desafiador, mas a essência é que você cria uma versão simulada de suas dependências. Em seguida, você diz à dependência o que retornar sob condições específicas. Por exemplo, se você tivesse um método denominado Square(int x) que ao quadrado do inteiro, você poderia dizer quando x = 2, retornar 4. Você também poderia dizer a ele para retornar x ^ 2 para qualquer número inteiro. Ou você poderia dizer a ele para retornar 5 quando x = 2. Por que você executaria o último caso? No caso de o método sob a função do teste ser validar a resposta da dependência, você pode querer forçar o retorno de respostas inválidas para garantir que está detectando o bug de maneira adequada.
Usando o Moq para contar ao IMath interface como lidar com Add sob teste. Você pode definir casos explícitos com It.Is ou um intervalo com It.IsInRange.
Frameworks de teste de unidade para DotNet
o Microsoft Unit Testing Framework é a solução de teste de unidade pronta para uso da Microsoft e incluída no Visual Studio. Como vem com o VS, ele se integra perfeitamente a ele. Ao iniciar um projeto, o Visual Studio perguntará se você deseja criar uma Biblioteca de Teste de Unidade junto com seu aplicativo.
O Microsoft Unit Testing Framework também vem com várias ferramentas para ajudá-lo a analisar melhor seus procedimentos de teste. Além disso, como é propriedade e é escrito pela Microsoft, há alguma sensação de estabilidade em sua existência no futuro.
Mas ao trabalhar com ferramentas da Microsoft, você obtém o que elas oferecem. O Microsoft Unit Testing Framework pode ser complicado de integrar.
A maior vantagem para mim em usar NUnit são testes parametrizados. Em nosso exemplo de Fibonacci acima, podemos inserir uma série de casos de teste e garantir que os resultados sejam verdadeiros. E, no caso do nosso 501º problema, sempre podemos adicionar um novo conjunto de parâmetros para garantir que o teste seja sempre executado sem a necessidade de um novo método de teste.
A principal desvantagem do NUnit é integrá-lo ao Visual Studio. Ele não possui os recursos que vêm com a versão da Microsoft e significa que você precisará baixar seu próprio conjunto de ferramentas.
xUnit é muito popular em C # porque se integra muito bem ao ecossistema .NET existente. Nuget tem muitas extensões de xUnit disponíveis. Ele também se integra perfeitamente ao Team Foundation Server, embora eu não tenha certeza de quantos Desenvolvedores .NET ainda usam o TFS em várias implementações Git.
Por outro lado, muitos usuários reclamam que a documentação do xUnit é um pouco deficiente. Para novos usuários em testes de unidade, isso pode causar uma enorme dor de cabeça. Além disso, a extensibilidade e adaptabilidade do xUnit também tornam a curva de aprendizado um pouco mais íngreme do que o NUnit ou o Unit Testing Framework da Microsoft.
Design / desenvolvimento orientado a testes
Design / desenvolvimento orientado a testes (TDD) é um tópico um pouco mais avançado que merece uma postagem própria. No entanto, gostaria de fazer uma introdução.
A ideia é começar com seus testes de unidade e dizer a eles o que é correto. Então, você pode escrever seu código em torno desses testes. Em teoria, o conceito parece simples, mas na prática, é muito difícil treinar seu cérebro para pensar retroativamente sobre a aplicação. Mas a abordagem tem o benefício interno de não ser necessário escrever seus testes de unidade após o fato. Isso leva a menos refatoração, reescrita e confusão de classes.
TDD tem sido uma palavra da moda nos últimos anos, mas sua adoção tem sido lenta. Sua natureza conceitual é confusa para os stakeholders, o que dificulta sua aprovação. Mas, como desenvolvedor, encorajo você a escrever até mesmo um pequeno aplicativo usando a abordagem TDD para se acostumar com o processo.
Por que você não pode ter muitos testes de unidade
O teste de unidade é uma das ferramentas de teste mais poderosas que os desenvolvedores têm à sua disposição. Não é de forma alguma suficiente para um teste completo de seu aplicativo, mas seus benefícios em teste de regressão, design de código e documentação de propósito são incomparáveis.
Não existe tal coisa como escrever muitos testes de unidade. Cada caso extremo pode propor grandes problemas no futuro em seu software. Memorizar bugs encontrados como testes de unidade pode garantir que esses bugs não encontrem maneiras de voltar para o seu software durante as alterações de código posteriores. Embora você possa adicionar 10-20% ao orçamento inicial do seu projeto, pode economizar muito mais do que isso em treinamento, correções de bugs e documentação.
Você pode encontrar o repositório Bitbucket usado neste artigo Aqui .
O que você quer dizer com teste de unidade?
Teste de unidade o processo de teste de métodos individuais. O teste de unidade garante que cada componente atue como pretendido.
O que é um exemplo de teste de unidade?
O teste de unidade do processo de teste de uma função atômica passando valores conhecidos para essa função e afirmando que um resultado esperado é produzido pela função.
Como o teste de unidade é executado?
O teste de unidade é realizado usando uma estrutura e um conjunto de ferramentas, muitas vezes adaptados à estrutura de sua base de código.
O que é um bom teste de unidade?
Um bom teste de unidade é aquele que engloba todas as entradas possíveis em seu método. Os resultados desses métodos devem ser conhecidos pelo designer e o teste de unidade deve refletir o resultado esperado.
Quais são os benefícios do teste de unidade?
Os testes de unidade ajudam a garantir a qualidade do seu código, exigindo que você escreva um código testável, garantem a funcionalidade do seu código e fornecem um excelente ponto de partida para o teste de regressão.
Os testes de unidade valem a pena?
Sim. Os testes de unidade podem aumentar um pouco os custos de desenvolvimento, mas, a longo prazo, eles economizarão tempo e esforço por meio de testes de regressão e memorização de bugs.
Quantos testes de unidade devo escrever?
Idealmente, seus testes de unidade devem cobrir todos os aspectos de seu código em todas as possibilidades. Claro, na realidade, isso é impossível e a resposta simples é que você nunca pode escrever muitos testes de unidade.