Fundamentos de teste de software

Gabriel Barberini
Inside SumUp
Published in
7 min readOct 24, 2022
Euklid von Alexandria — Wikipédia

John Guttag na terceira edição de seu livro de introdução à computação faz um breve diálogo sobre software testing.

John começa o capítulo de testes introduzindo a diferença entre white-box e black-box testing:

Heurísticas baseadas em explorar diferentes caminhos no código de um software pertencem a uma classe de testes conhecida por glass-box ou white-box (caixa branca). Já heurísticas baseadas em explorar caminhos diferentes nas especificações de um software se encaixam em uma classe de testes conhecida por black-box (caixa preta)

Eu reconheço a importância em destacar essa diferença pois existem contextos em que uma abordagem pode superar a outra a fim de melhor garantir a qualidade de um software.

Por exemplo, quando uma determinada implementação carece de especificações, ou quando um produto é sensível a falhas, provavelmente é melhor seguir uma abordagem de testes white-box; Quando a aplicação não é crítica ou a implementação possui especificações bem detalhadas: black-box; Agora quando o contexto em que o software se encontra é totalmente novo ou não for óbvio que abordagem utilizar, recomendo combinar black-box e white-box durante as validações.

Além da distinção entre black-box e white-box também vale notar a diferença entre testes funcionais e não-funcionais.

Testes não-funcionais são aqueles cuja intenção é validar as condições em que um determinado software opera e não sua operação em si, por exemplo: quanto de memória ou tempo são consumidos durante a execução de um determinado segmento de código.

Enquanto que testes funcionais estressam exclusivamente as funcionalidades de um software.

Geralmente ambos são verificáveis por sequências distintas de entrada de informação → alterações de estado → saída de informação.

Vamos realizar um pequeno exercício para conectar as ideias anteriores. Suponha que você precise testar o funcionamento de um elevador bem simples.

Para esse elevador espera-se que:

  1. O elevador se locomova até qualquer andar solicitado em seu painel.
  2. O elevador apenas abra a porta quando estiver completamente parado no andar selecionado.
  3. O elevador se locomove a todo momento em velocidade constante.

Limitando-se às informações fornecidas acima, seria possível apenas desenhar testes black-box, validando as ramificações presentes nas especificações do produto. Caso tivéssemos acesso aos detalhes de implementação em baixo nível, poderíamos também desenhar testes white-box para validar ramificações no código.

Continuando com a abordagem black-box, podemos variar os andares disponíveis no painel do elevador verificando sequências distintas de seleção → locomoção → abertura de porta nos aproximando do modelo entrada → mudanças de estado → saída mencionado anteriormente.

Pensando em testes funcionais, estaremos preocupados em responder as seguintes perguntas:

  • Ao selecionar um andar no painel, o elevador vai até o andar selecionado?
  • Ao se locomover, o elevador permanece em velocidade constante?
  • Ao abrir a porta, o elevador está parado?

Por outro lado, testes que exercitam fatores como a temperatura do ambiente interno ou tempo para se locomover entre um andar e outro, se enquadrariam melhor na categoria de validações não-funcionais pois nesse caso a variabilidade dos resultados não comprometeria o funcionamento do elevador. Um exemplo seria responder as seguintes perguntas:

  • Ao selecionar um andar no painel, o botão muda de cor?
  • Ao se locomover, o elevador liga o ventilador de teto para refrescar o ambiente?
  • Ao abrir a porta, o elevador emite um som anunciando em que andar se encontra?

É possível testar tudo ?

Uma suíte de testes white-box é considerada path-complete (completamente percorrida) quando exercita todos os caminhos em potencial no decorrer da execução de um software. Isso é tipicamente impossível de se alcançar porque geralmente depende da quantidade de vezes em que um loop é executado ou do nível de profundidade de cada recursão.

Por exemplo, uma implementação de fatorial com recursão segue um caminho diferente para cada input (parâmetros de entrada) porque os níveis de profundidade de cada recursão será diferente.

E mesmo quando uma suíte white-box aparente ser path-complete ela pode ainda assim não garantir que todos os bugs sejam expostos, por exemplo, considere o código em python que tenta implementar a função absolute (módulo):

Nesse caso a especificação sugere apenas dois possíveis cenários: ou x é negativo, ou não é. Isso sugere que o conjunto de entrada {2, -2} seja suficiente para explorar todos os possíveis cenários da especificação => black-box path-complete. Esse conjunto de teste também tem a propriedade bacana de fazer com que todas as partes do código sejam acessadas => white-box path-complete.

O problema é que essa suíte não expõem o problema de que abs(-1) retorne -1 em vez de 1.

Nesse exemplo John também destaca a importância de uma especificação bem definida, perceba que o exemplo que ele quis problematizar teria sido facilmente solucionado se o trecho “(…) caso contrário (…) “ na especificação tivesse sido traduzido pro seu tácito significado: x < 0, assim notaríamos rapidamente que a condição x < -1 está errada e solucionaríamos o caso abs(-1).

Apesar das limitações apresentadas, segue algumas regrinhas de ouro para tirar melhor proveito de validações white-box:

- Teste as ramificações de todas as condicionais.

- Teste cada tratamento de erro (try-catch, except, etc).

- Para cada for-Loop, certifique-se de ter um cenário de teste onde:
O loop não é entrado (ex: se o loop itera uma lista de itens, certifique-se de testar o loop com uma lista vazia);
O corpo do loop é executado exatamente uma vez;
→ O corpo do
loop é executado pelo menos duas vezes.

- Para cada while-loop, certifique-se de ter um cenário de teste onde:
Todos os casos de entrada no loop sejam contemplados;
Todas as maneiras de sair do loop sejam contempladas.
Ex, no caso de: while (len(L) > 0 and not L[i] == e) garanta que haja um cenário de teste onde len(L) > 0 e L[i] != e, também onde L[i] == e ou len(L) < 0.

- Para funções recursivas inclua testes que façam com que a função retorne:
Nenhuma chamada recursiva;
Exatamente uma chamada recursiva;
→ Mais de uma chamada recursiva.

Vale notar que a maior parte das implementações podem ser resumidas a uma combinação de condicionais e laços.

Analogamente, uma suíte de testes black-box é considerada path-complete quando os testes exploram todos os caminhos permitidos pela especificação do software.

Considere a seguinte especificação:

Aparentemente a especificação aponta para apenas dois caminhos distintos: onde x = 0 ou x > 0. Apesar de sabermos que testar esses dois cenários seja importante, com um pouco de senso comum já começamos a suspeitar de que isso não seja suficiente…

Isso ocorre porque nesse caso condições de contorno também precisam ser testadas. Por exemplo, se fossemos investigar um argumento do tipo lista geralmente testaríamos quando a lista está vazia, quando há apenas um elemento na lista, quando a lista contém elementos mutáveis, imutáveis, ou até mesmo listas dentro de listas.

Com números não é muito diferente, especialmente no caso dos floats. Precisamos testar números muito pequenos e números muito grandes, ou valores típicos de um determinado problema. Para a função sqrt() faz sentido olharmos para os números da tabela a seguir:

Ou seja, é impraticável testar tudo, por isso é importante ter sempre uma boa estratégia de testes, garantindo que se teste o suficiente.

Um pouco sobre fases de teste

Geralmente testes são divididos em duas fases, testes unitários + testes de integração e testes funcionais.

Testes unitários são os primeiros testes a serem realizados, eles consistem em verificar o funcionamento individual de cada parte do código (e.g: funções, classes, etc), seguidos por testes de integração que são desenhados para verificar o funcionamento coletivo das unidades previamente testadas.

Por último são executados os testes funcionais, que como discutido anteriormente servem para verificar se o software se comporta como é esperado.

Você também já deve ter ouvido falar de testes de regressão, esses são testes realizados antes que uma alteração no software seja aplicada para o usuário final; testes de regressão consistem em garantir que qualquer mudança não possua regressão/regresso, ou seja, que uma alteração no software não quebre nada que já estivesse funcionando anteriormente.

Por fim John traz uma reflexão que eu acredito ser extremamente importante: a distinção entre duas dimensões de bugs de runtime (bugs que ocorrem durante a execução de um software).

Tipos de bug

Overt → covert
Em português: Escancarado → disfarçado

Persistent → intermittent
Em português: Persistente → Intermitente

Um bug escancarado tem manifestação óbvia, e.g., um software que quebra durante a execução ou que demora demais para executar o que deveria (às vezes para sempre). Já bugs disfarçados são aqueles que podem até executar sem problemas aparentes, mas quando submetidos a algumas situações específicas apresentam resultados errados.

Muitos bugs se encaixam entre esses dois extremos e o que define se ele vai ser escancarado é o quão cuidadosamente você analisa o comportamento de um software.

Por isso é interessante que a pessoa realizando os testes mantenha um caminho bem definido de passos para cada cenário, uma boa descrição de passos de teste possibilita reproduzir sem muita dificuldade eventuais bugs disfarçados.

Bugs persistentes são aqueles que ocorrem toda vez que o software é executado com os mesmos parâmetros de entrada enquanto que intermitentes podem ou não ocorrer mesmo executando o software com os mesmos parâmetros de entrada. Bugs intermitentes são mais comuns quando lidando com fatores aleatórios.

O melhor tipo de bug é aquele que é escancarado e persistente e bons programadores tentam escrever softwares de forma que eventuais erros resultem em bugs overt-persistent, essa técnica é chamada de defensive programming (programação defensiva).

Gostou ? Tenha acesso ao material completo do John no MIT OCW, incluindo notas de aula e gravações. Para outros materiais semelhantes acompanhe o meu repositório no Github.

Até a próxima! :)

--

--

Gabriel Barberini
Inside SumUp

Electrical Engineering and Computer Science; QA Engineer at SumUp