Teste de unidade - Unit testing

Em programação de computador , o teste de unidade é um método de teste de software pelo qual unidades individuais de código-fonte - conjuntos de um ou mais módulos de programa de computador, juntamente com dados de controle, procedimentos de uso e procedimentos operacionais associados - são testados para determinar se eles são adequados para uso .

Descrição

Os testes de unidade são normalmente testes automatizados escritos e executados por desenvolvedores de software para garantir que uma seção de um aplicativo (conhecida como "unidade") atenda ao seu design e se comporte conforme o esperado. Na programação procedural , uma unidade pode ser um módulo inteiro, mas é mais comumente uma função ou procedimento individual. Na programação orientada a objetos , uma unidade geralmente é uma interface inteira, como uma classe ou um método individual. Ao escrever testes primeiro para as menores unidades testáveis, depois os comportamentos compostos entre elas, pode-se construir testes abrangentes para aplicativos complexos.

Para isolar os problemas que podem surgir, cada caso de teste deve ser testado de forma independente. Substitutos como stubs de método , objetos simulados , fakes e chicotes de teste podem ser usados ​​para auxiliar no teste de um módulo isoladamente.

Durante o desenvolvimento, um desenvolvedor de software pode codificar critérios ou resultados que são considerados bons no teste para verificar se a unidade está correta. Durante a execução do caso de teste, os frameworks registram os testes que falham em qualquer critério e os relatam em um resumo. Para isso, a abordagem mais comumente usada é teste - função - valor esperado.

Escrever e manter testes de unidade pode ser feito mais rápido usando testes parametrizados . Isso permite a execução de um teste várias vezes com diferentes conjuntos de entrada, reduzindo assim a duplicação do código de teste. Ao contrário dos testes de unidade tradicionais, que geralmente são métodos fechados e condições invariantes de teste, os testes parametrizados usam qualquer conjunto de parâmetros. Os testes parametrizados são suportados por TestNG , JUnit e sua contraparte .Net, XUnit . Parâmetros adequados para os testes de unidade podem ser fornecidos manualmente ou, em alguns casos, são gerados automaticamente pela estrutura de teste. Nos últimos anos, foi adicionado suporte para escrever testes (de unidade) mais poderosos, aproveitando o conceito de teorias, casos de teste que executam as mesmas etapas, mas usando dados de teste gerados em tempo de execução, ao contrário de testes parametrizados regulares que usam as mesmas etapas de execução com conjuntos de entrada que são predefinidos.

Vantagens

O objetivo do teste de unidade é isolar cada parte do programa e mostrar que as partes individuais estão corretas. Um teste de unidade fornece um contrato estrito por escrito que a parte do código deve satisfazer. Como resultado, oferece vários benefícios.

O teste de unidade encontra problemas no início do ciclo de desenvolvimento . Isso inclui bugs na implementação do programador e falhas ou partes ausentes da especificação da unidade. O processo de escrever um conjunto completo de testes força o autor a pensar nas entradas, saídas e condições de erro e, assim, definir com mais precisão o comportamento desejado da unidade. O custo de encontrar um bug antes do início da codificação ou quando o código é escrito pela primeira vez é consideravelmente menor do que o custo de detectar, identificar e corrigir o bug posteriormente. Bugs no código lançado também podem causar problemas caros para os usuários finais do software. O código pode ser impossível ou difícil de testar por unidade se for mal escrito; portanto, o teste de unidade pode forçar os desenvolvedores a estruturar funções e objetos de maneiras melhores.

No desenvolvimento orientado a testes (TDD), que é freqüentemente usado tanto em programação extrema quanto em scrum , os testes de unidade são criados antes que o próprio código seja escrito. Quando os testes são aprovados, o código é considerado completo. Os mesmos testes de unidade são executados com relação a essa função frequentemente, pois a base de código maior é desenvolvida quando o código é alterado ou por meio de um processo automatizado com a construção. Se os testes de unidade falharem, isso é considerado um bug no código alterado ou nos próprios testes. Os testes de unidade permitem então que a localização da falha ou falha seja facilmente rastreada. Como os testes de unidade alertam a equipe de desenvolvimento sobre o problema antes de entregar o código aos testadores ou clientes, os problemas potenciais são detectados no início do processo de desenvolvimento.

O teste de unidade permite que o programador refatore o código ou atualize as bibliotecas do sistema posteriormente e certifique-se de que o módulo ainda funcione corretamente (por exemplo, no teste de regressão ). O procedimento é escrever casos de teste para todas as funções e métodos de modo que, sempre que uma alteração causar uma falha, ela possa ser rapidamente identificada. Os testes de unidade detectam mudanças que podem quebrar um contrato de projeto .

O teste de unidade pode reduzir a incerteza nas próprias unidades e pode ser usado em uma abordagem de estilo de teste de baixo para cima . Testando as partes de um programa primeiro e depois testando a soma de suas partes, o teste de integração se torna muito mais fácil.

O teste de unidade fornece uma espécie de documentação viva do sistema. Os desenvolvedores que procuram aprender qual funcionalidade é fornecida por uma unidade e como usá-la, podem consultar os testes de unidade para obter uma compreensão básica da interface da unidade ( API ).

Os casos de teste de unidade incorporam características que são críticas para o sucesso da unidade. Essas características podem indicar o uso apropriado / inadequado de uma unidade, bem como comportamentos negativos que devem ser controlados pela unidade. Um caso de teste de unidade, por si só, documenta essas características críticas, embora muitos ambientes de desenvolvimento de software não dependam apenas do código para documentar o produto em desenvolvimento.

Quando o software é desenvolvido usando uma abordagem orientada a teste, a combinação de escrever o teste de unidade para especificar a interface mais as atividades de refatoração realizadas após a aprovação do teste pode tomar o lugar do design formal. Cada teste de unidade pode ser visto como um elemento de design que especifica classes, métodos e comportamento observável.

Limitações e desvantagens

O teste não detectará todos os erros do programa, porque não pode avaliar todos os caminhos de execução em nenhum dos programas, exceto os mais triviais. Esse problema é um superconjunto do problema da parada , que é indecidível . O mesmo é verdadeiro para o teste de unidade. Além disso, o teste de unidade, por definição, testa apenas a funcionalidade das próprias unidades. Portanto, ele não detectará erros de integração ou erros mais amplos no nível do sistema (como funções executadas em várias unidades ou áreas de teste não funcionais, como desempenho ). O teste de unidade deve ser feito em conjunto com outras atividades de teste de software , pois eles podem apenas mostrar a presença ou ausência de erros específicos; eles não podem provar uma ausência completa de erros. Para garantir o correcto comportamento de todos os caminhos de execução e de todas as entradas possíveis, e garantir a ausência de erros, são necessárias outras técnicas, nomeadamente a aplicação de métodos formais para provar que um componente de software não apresenta um comportamento inesperado.

Uma hierarquia elaborada de testes de unidade não é igual a testes de integração. A integração com unidades periféricas deve ser incluída nos testes de integração, mas não nos testes de unidade. O teste de integração normalmente ainda depende muito do teste manual de humanos ; o teste de alto nível ou de escopo global pode ser difícil de automatizar, de modo que o teste manual muitas vezes parece mais rápido e barato.

O teste de software é um problema combinatório. Por exemplo, cada declaração de decisão booleana requer pelo menos dois testes: um com resultado "verdadeiro" e outro com resultado "falso". Como resultado, para cada linha de código escrita, os programadores geralmente precisam de 3 a 5 linhas de código de teste. Obviamente, isso leva tempo e seu investimento pode não valer o esforço. Existem problemas que não podem ser testados facilmente - por exemplo, aqueles que não são determinísticos ou envolvem vários threads . Além disso, é provável que o código de um teste de unidade seja pelo menos tão problemático quanto o código que ele está testando. Fred Brooks em The Mythical Man-Month cita: "Nunca vá para o mar com dois cronômetros; pegue um ou três." Ou seja, se dois cronômetros se contradizem, como saber qual é o correto?

Outro desafio relacionado a escrever os testes de unidade é a dificuldade de configurar testes realistas e úteis. É necessário criar condições iniciais relevantes para que a parte da aplicação que está sendo testada se comporte como parte do sistema completo. Se essas condições iniciais não forem definidas corretamente, o teste não estará exercitando o código em um contexto realista, o que diminui o valor e a precisão dos resultados do teste de unidade.

Para obter os benefícios pretendidos com os testes de unidade, é necessária uma disciplina rigorosa em todo o processo de desenvolvimento de software. É essencial manter registros cuidadosos não apenas dos testes que foram realizados, mas também de todas as alterações que foram feitas no código-fonte desta ou de qualquer outra unidade do software. O uso de um sistema de controle de versão é essencial. Se uma versão posterior da unidade falhar em um teste específico no qual ela foi aprovada anteriormente, o software de controle de versão pode fornecer uma lista das alterações do código-fonte (se houver) que foram aplicadas à unidade desde então.

Também é essencial implementar um processo sustentável para garantir que as falhas dos casos de teste sejam revisadas regularmente e corrigidas imediatamente. Se tal processo não for implementado e enraizado no fluxo de trabalho da equipe, o aplicativo evoluirá fora de sincronia com o conjunto de testes de unidade, aumentando os falsos positivos e reduzindo a eficácia do conjunto de testes.

O software de sistema embarcado de teste de unidade apresenta um desafio único: como o software está sendo desenvolvido em uma plataforma diferente daquela em que será executado, você não pode executar prontamente um programa de teste no ambiente de implantação real, como é possível com os programas de desktop.

Os testes de unidade tendem a ser mais fáceis quando um método tem parâmetros de entrada e alguma saída. Não é tão fácil criar testes de unidade quando uma função principal do método é interagir com algo externo ao aplicativo. Por exemplo, um método que funcionará com um banco de dados pode exigir a criação de um mock up das interações do banco de dados, o que provavelmente não será tão abrangente quanto as interações reais do banco de dados.

Exemplo

Aqui está um conjunto de casos de teste em Java que especificam vários elementos da implementação. Primeiro, deve haver uma interface chamada Adder e uma classe de implementação com um construtor de argumento zero chamado AdderImpl. Em seguida, afirma que a interface Adder deve ter um método chamado add, com dois parâmetros inteiros, que retorna outro inteiro. Ele também especifica o comportamento desse método para um pequeno intervalo de valores em vários métodos de teste.

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class TestAdder {

    @Test
    public void testSumPositiveNumbersOneAndOne() {
        Adder adder = new AdderImpl();
        assertEquals(2, adder.add(1, 1));
    }

    // can it add the positive numbers 1 and 2?
    @Test
    public void testSumPositiveNumbersOneAndTwo() {
        Adder adder = new AdderImpl();
        assertEquals(3, adder.add(1, 2));
    }

    // can it add the positive numbers 2 and 2?
    @Test
    public void testSumPositiveNumbersTwoAndTwo() {
        Adder adder = new AdderImpl();
        assertEquals(4, adder.add(2, 2));
    }

    // is zero neutral?
    @Test
    public void testSumZeroNeutral() {
        Adder adder = new AdderImpl();
        assertEquals(0, adder.add(0, 0));
    }

    // can it add the negative numbers -1 and -2?
    @Test
    public void testSumNegativeNumbers() {
        Adder adder = new AdderImpl();
        assertEquals(-3, adder.add(-1, -2));
    }

    // can it add a positive and a negative?
    @Test
    public void testSumPositiveAndNegative() {
        Adder adder = new AdderImpl();
        assertEquals(0, adder.add(-1, 1));
    }

    // how about larger numbers?
    @Test
    public void testSumLargeNumbers() {
        Adder adder = new AdderImpl();
        assertEquals(2222, adder.add(1234, 988));
    }
}

Nesse caso, os testes de unidade, tendo sido escritos primeiro, agem como um documento de design especificando a forma e o comportamento de uma solução desejada, mas não os detalhes de implementação, que são deixados para o programador. Seguindo a prática "faça a coisa mais simples que possa funcionar", a solução mais fácil que fará o teste passar é mostrada abaixo.

interface Adder {
    int add(int a, int b);
}
class AdderImpl implements Adder {
    public int add(int a, int b) {
        return a + b;
    }
}

Como especificações executáveis

O uso de testes de unidade como especificação de design tem uma vantagem significativa sobre outros métodos de design: O documento de design (os próprios testes de unidade) pode ser usado para verificar a implementação. Os testes nunca serão aprovados, a menos que o desenvolvedor implemente uma solução de acordo com o design.

O teste de unidade carece de alguma acessibilidade de uma especificação diagramática, como um diagrama UML , mas eles podem ser gerados a partir do teste de unidade usando ferramentas automatizadas. A maioria das linguagens modernas possui ferramentas gratuitas (geralmente disponíveis como extensões para IDEs ). Ferramentas gratuitas, como aquelas baseadas no framework xUnit , terceirizam para outro sistema a renderização gráfica de uma visualização para consumo humano.

Formulários

Programação extrema

O teste de unidade é a base da programação extrema , que depende de uma estrutura de teste de unidade automatizada . Esta estrutura de teste de unidade automatizada pode ser de terceiros, por exemplo, xUnit , ou criada dentro do grupo de desenvolvimento.

A programação extrema usa a criação de testes de unidade para desenvolvimento orientado a testes . O desenvolvedor escreve um teste de unidade que expõe um requisito de software ou um defeito. Esse teste falhará porque o requisito ainda não foi implementado ou porque expõe intencionalmente um defeito no código existente. Em seguida, o desenvolvedor escreve o código mais simples para fazer o teste, junto com outros testes, passar.

A maior parte do código em um sistema é testada por unidade, mas não necessariamente todos os caminhos através do código. A programação extrema exige uma estratégia de "testar tudo o que pode quebrar", em vez do método tradicional de "testar todos os caminhos de execução". Isso leva os desenvolvedores a desenvolver menos testes do que os métodos clássicos, mas isso não é realmente um problema, é mais uma reafirmação do fato, já que os métodos clássicos raramente são seguidos metodicamente o suficiente para que todos os caminhos de execução tenham sido testados exaustivamente. A programação extrema simplesmente reconhece que o teste raramente é exaustivo (porque geralmente é muito caro e demorado para ser economicamente viável) e fornece orientação sobre como focar recursos limitados de maneira eficaz.

Crucialmente, o código de teste é considerado um artefato de projeto de primeira classe, pois é mantido com a mesma qualidade do código de implementação, com todas as duplicações removidas. Os desenvolvedores liberam o código de teste de unidade para o repositório de código em conjunto com o código que ele testa. O teste de unidade completo da Extreme programming permite os benefícios mencionados acima, como desenvolvimento e refatoração de código mais simples e confiável , integração de código simplificada, documentação precisa e designs mais modulares. Esses testes de unidade também são executados constantemente como uma forma de teste de regressão .

O teste de unidade também é crítico para o conceito de Design Emergente . Como o design emergente depende muito da refatoração, os testes de unidade são um componente integral.

Estruturas de teste de unidade

As estruturas de teste de unidade geralmente são produtos de terceiros que não são distribuídos como parte do pacote do compilador. Eles ajudam a simplificar o processo de teste de unidade, tendo sido desenvolvidos para uma ampla variedade de linguagens .

Geralmente, é possível realizar testes de unidade sem o suporte de uma estrutura específica, escrevendo o código do cliente que exercita as unidades em teste e usa asserções , tratamento de exceções ou outros mecanismos de fluxo de controle para sinalizar a falha. O teste de unidade sem uma estrutura é valioso porque há uma barreira de entrada para a adoção de testes de unidade; ter poucos testes de unidade dificilmente é melhor do que não ter nenhum, ao passo que, uma vez que uma estrutura esteja pronta, adicionar testes de unidade se torna relativamente fácil. Em algumas estruturas, muitos recursos avançados de teste de unidade estão faltando ou devem ser codificados manualmente.

Suporte para teste de unidade em nível de idioma

Algumas linguagens de programação oferecem suporte direto ao teste de unidade. Sua gramática permite a declaração direta de testes de unidade sem importar uma biblioteca (seja de terceiros ou padrão). Além disso, as condições booleanas dos testes de unidade podem ser expressas na mesma sintaxe das expressões booleanas usadas no código de teste não unitário, como para que é usado ife whileinstruções.

Os idiomas com suporte para teste de unidade integrado incluem:

Algumas linguagens sem suporte de teste de unidade integrado têm bibliotecas / estruturas de teste de unidade muito boas. Esses idiomas incluem:

Veja também

Referências

links externos