Edit page

Code Coverage, Testes de Objeto e Componente

Code Coverage

Em white-box testing, podemos definir uma métrica que nos indica a percentagem do nosso código que é testado, chamada de code coverage.

Code Coverage

Consiste numa aplicação de white-box testing para determinar a percentagem de código que é executado quando um dado conjunto de testes é corrido. Quanto maior for a code coverage, há mais código a ser executado, o que aumenta a probabilidade de encontrar bugs.

No entanto, existem diversos critérios que podemos aplicar, cada um com as suas vantagens e desvantagens:

  • Statement Coverage
  • Branch Coverage
  • Condition Coverage
  • Path Coverage

Para exemplificar cada um dos critérios acima, teremos em conta o pseudocódigo seguinte, assim como o gráfico correspondente às suas possíveis execuções:

Pseudo-código e respetivo CFG

Mais exemplos podem ser encontrados aqui.

Statement Coverage

A Statement Coverage de um programa relaciona-se com a percentagem das linhas de código que são executadas com um dado conjunto de testes. Temos Statement Coverage completa quando os testes garantem que todas as linhas de código são executadas pelo menos uma vez.

Tendo em conta o exemplo acima, consideremos dois casos de teste:

  • Test Case 1: (b0 && b1) = true, b2 = true
  • Test Case 2: (b0 && b1) = false, b2 = false

Verifica-se que, para executar 100% das linhas do programa, basta ter o Test Case 1, o que nos dá uma Statement Coverage completa, enquanto o Test Case 2 apenas excuta 75% das linhas.

Exemplo de Statement Coverage

Branch Coverage

A Branch Coverage de um programa relaciona-se com a percentagem de condições que são avaliadas com um dado conjunto de teste. Temos Branch Coverage completa quando os testes garantem que todas as condições são avaliadas como verdadeiras e falsas.

Tendo em conta o exemplo acima, consideremos dois casos de teste:

  • Test Case 1: (b0 && b1) = true, b2 = true
  • Test Case 2: (b0 && b1) = false, b2 = false

Verifica-se que ambos os testes apenas testam 50% das condições possíveis, pelo que, para termos Branch Coverage completa, temos de considerar, pelo menos, ambos os test cases.

Exemplo de Branch Coverage

Condition Coverage

A Condition Coverage de um programa relaciona-se com a percentagem de subexpressões booleanas de condições que são avaliadas como verdadeiro e falso. Temos Condition Coverage completa quando os testes garantem que todas as componentes de uma condição são avaliadas como verdadeiras e como falsas.

Tendo em conta o exemplo acima, consideremos três casos de teste:

  • Test Case 1: b0 = true, b1 = false, b2 = false
  • Test Case 2: b0 = false, b1 = true, b2 = true
  • Test Case 3: b0 = true, b1 = true, b2 = true

Verifica-se que, tendo em conta apenas os dois primeiros test cases, as condições b0 e b2 são ambas avaliadas como verdadeiro e falso. No entanto, devido à presença da conjunção (&&), a condição b1 apenas é avaliada como falsa, dado que b0 = false e (b0 && _) vai ser sempre falso independentemente da segunda condição, que acaba por não ser avaliada. Assim, para ter Condition Coverage completa, temos de incluir todos os três test cases.

Exemplo de Condition Coverage

Disjunções e Conjunções

Para verificar Condition Coverage, é preciso ter em atenção as disjunções (||) e as conjunções (&&) presentes no código, assim como a ordem pela qual as condições aparecem.

Path Coverage

A Path Coverage de um programa relaciona-se com todos os caminhos independentes que podem ser percorridos. Temos Path Coverage completa quando todos os caminhos independentes são executados.

Caminho Independente

Um caminho independente num programa é um que atravessa pelo menos uma nova aresta do grafo de execução do programa.

O número de caminhos independentes pode ser obtido a partir da seguinte maneira:

  • Um corresponde ao caminho default;
  • Há mais um caminho por cada instrução if, while, repeat, for, and e or;
  • Há mais um caminho por cada case numa instrução switch, havendo ainda mais um caso não haja um caso default.

Nuˊmero de Caminhos Independentes=Nuˊmero de Deciso˜es+1\text{Número de Caminhos Independentes} = \text{Número de Decisões} + 1

Tendo em conta o exemplo acima, consideremos dois casos de teste:

  • Test Case 1: (b0 && b1) = true, b2 = true
  • Test Case 2: (b0 && b1) = false, b2 = true
  • Test Case 3: (b0 && b1) = true, b2 = false

Verifica-se que, a cada test case, estamos a percorrer uma aresta no grafo que ainda não tinha sido percorrida, pelo que necessitamos dos três test cases para ter Path Coverage completa.

Exemplo de Path Coverage

Testes de Objeto

Os testes de objeto são usados especialmente no contexto de programação orientada a objetos. Ao contrário dos testes unitários, que testam funções e métodos de forma isolada, os testes de objeto testam sequências de métodos.

No contexto dos testes de objeto, é importante saber o conceito de Classe Modal.

Classe Modal

Uma classe modal é uma classe onde o seu estado interno afeta o resultado de certas sequências de invocação de métodos. Costumam ser o alvo principal dos testes de objeto.

As sequências de métodos a testar dependem de fatores como a longevidade do objeto em questão e devem ser feitas de acordo com o diagrama de estados desse objeto.

Testes de Componente

Os testes de componente costumam ser os últimos testes a ser executados (após os testes de unidade e de objeto) e tratam de verificar a interação entre interfaces de diferentes componentes e classes. O principal objetivo destes testes é encontrar fragilidades que resultem da interação entre unidades.

Alguns dos erros relacionados com o uso de interfaces são:

  • Interface Misuse: quando uma interface é usada incorretamente (como por exemplo, chamar uma função ou método com os parâmetros por ordem errada);
  • Interface Misunderstanding: quando uma componente invoca outra assumindo comportamentos errados acerca dela;
  • Timing Errors: as componentes invocadora e invocada operam a velocidades diferentes, resultando em operações fora de ordem ou informação desatualizada.

Podemos testar as componentes todas ao mesmo tempo (Big Bang Integration) ou incrementalmente (Bottom-up ou Top-down integration), sendo que a escolha depende de fatores como a dependência entre unidades e a dificuldade de encontrar falhas no sistema.

  • Big Bang Integration: testa todas as componentes e todas as interações ao mesmo tempo.

Diagrama sobre Big Bang Integration

  • Bottom-up Integration: testa primeiro as componentes de mais baixo nível e vai acrescentando incrementalmente módulos de mais alto nível.

Diagrama sobre Bottom-up Integration

  • Top-down Integration: testa primeiro as componentes de mais alto nível (recorrendo a test doubles para simular o comportamento de componentes mais baixas) e vai acrescentando incrementalmente módulos de mais baixo nível.

Diagrama sobre Bottom-up Integration

Pirâmide de Teste

A pirâmide de teste (Test Pyramid) é um indicador de quantos testes de cada tipo devem ser criados para avaliar corretamente um sistema. Há vários modelos de pirâmides de teste, sendo que as versões faladas em aula são a de Mike Cohn (descrita no livro Succeeding with Agile: Software Development Using Scrum ou acedendo aqui) e a da Google (descrita no livro Software Engineering at Google).

Pirâmides de teste de Mike Cohn e da Google