Aula 05
Teste Automatizado – Partes I e II
Alessandro Garcia LES/DI/PUC-Rio
Março de 2019
2 / 30 LES/DI/PU
Exercício sobre Introdução à Teste
• Se organizem em grupos (2 a 3 pessoas): de preferência o grupo que irá realizar os trabalhos na disciplina
• Escrevam em uma folha de papel o seguinte cabeçalho:
EXERCÍCIO 1: INTRODUÇÃO À TESTE Data: (data de hoje)
Integrantes: (nome dos integrantes do grupo)
Atividade 1: Casos de teste para uma função
Exemplo: criando casos de teste
• Um bom conjunto de casos de teste é aquele que possui alta probabilidade em revelar defeitos na função ou no módulo que a contém
int verificaTriangulo (int segmentoAB, int segmentoAC, int segmentoBC);
EXERCÍCIO 1: INTRODUÇÃO À TESTE Data: (data de hoje)
Integrantes: (nome dos integrantes do grupo)
Atividade 1: Casos de teste para uma função
•C1: (a, b, c) -> (d)
•C2: (x, y, z) -> (k)
•...
4 / 30 LES/DI/PU
Criando casos de teste com a especificação
EXERCÍCIO 1: INTRODUÇÃO À TESTE Data: (data de hoje)
Integrantes: (nome dos integrantes do grupo)
•Atividade 1: Casos de teste para uma função
•C1: (a, b, c) -> (d)
•C2: (x, y, z) -> (k)
•...
•Atividade 2: Casos de teste com especificação
•C1: (a, b, c) -> (d)
•C2: (x, y, z) -> (k)
Criando casos de teste com a especificação
A função recebe como entrada três valores inteiros. Cada valor
corresponde ao segmento de um lado do triângulo. Assim, o primeiro valor corresponde ao segmento do lado AB, o segundo valor ao segmento do lado AC e o terceiro valor correspondem ao segmento do lado BC. Esses três segmentos correspondem aos três lados que formam o triângulo ABC.
A função recebe os três valores como entrada e verifica que tipo de triângulo é formado com base nos três segmentos. Por fim, a função
retorna a quantidade de lados que possuem o mesmo tamanho. Assim, se o triângulo for equilátero, ou seja, os três lados possuem o mesmo
tamanho, então a função retorna 3. Se o triângulo for isósceles (dois lados com o mesmo tamanho), então a função retorna 2. Se o triângulo for
escaleno (os três lados possuem tamanhos diferentes), então a função
retorna 0. Caso os três valores não correspondam a um triângulo, a função retorna o código -1.
int verificaTriangulo (int segmentoAB, int segmentoAC, int segmentoBC);
6 / 30 LES/DI/PU
Casos de teste
• Um dos problemas comuns ao testar software é determinar quando os módulos foram suficientemente testados
• Mesmo usando a especificação, é difícil produzir bons casos de teste que sejam capazes de revelar a existência de defeitos
• Por exemplo, para o problema do triângulo, é necessário apenas quatro casos de testes para se testar a função em termos dos três tipos de triângulo
– 1 caso válido para triângulo equilátero (retorna 3) – 1 casos válidos para triângulo isósceles (retorna 2) – 1 caso válido para triângulo escaleno (retorna 0)
– 1 caso válido para valores que não correspondem a um triângulo (retorno -1)
Criando casos de teste com a especificação
int verificaTriangulo(int segAB, int segAC, int segBC){
if ((segAB == segAC) && (segAC == segBC)){
return 3; //equilatero }
if (((segAB== segAC) && (segAC != segBC)) ||
((segAC == segBC) && (segAB != segAC)) ||
((segAB == segBC) && (segBC != segAB))){
return 2; //isosceles }
if ( (segAB != segAC) && (segAC != segBC)) { return 0; //escaleno
}
return -1;
}
Qual é a saída para o caso de teste (2, 1, 1)?
8 / 30 LES/DI/PU
Criando casos de teste com a especificação
• Qual é a saída para o caso de teste (2, 1, 1)?
– 2 -> triângulo isósceles
• Esses valores não correspondem a um triângulo.
• Para se ter um triângulo, é necessário que ele tenha em cada um dos seus lados, uma medida menor que a soma das medidas dos outros dois lados.
AB < AC + BC AC < AB + BC BC < AB + AC
–Observação: Se AB for o maior lado, para existir um triângulo, basta que AB < AC + BC.
Função verificaTriangulo
int verificaTriangulo(int segAB, int segAC, int segBC){
if ( (segAB < segAC+ segBC) && (segAC< segAB + segBC) && (segBC< segAB + segAC)){
if ((segAB == segAC) && (segAC == segBC)){
return 3; //equilatero }
if (((segAB== segAC) && (segAC != segBC)) ||
((segAC == segBC) && (segAB != segAC)) ||
((segAB == segBC) && (segBC != segAC))){
return 2; //isosceles }
if ( (segAB != segAC) && (segAC != segBC)) { return 0; //escaleno
} }
return -1;
} E agora, o código está livre de defeitos???
10 / 30 LES/DI/PU
Completude dos casos de teste
• Um bom conjunto de casos de teste seria aquele que cobrisse os seguintes casos:
– 1 caso válido para triângulo equilátero (retorna 3) – 1 caso válido para triângulo escaleno (retorna 0) – 3 casos válidos para triângulo isósceles (retorna 2) – 3 casos para testar Lk > Li + Lj (retorna -1)
– 3 casos para testar Lk = Li + Lj (retorna -1)
– 3 casos para testar um dos lados igual a 0 (retorna -1)
– 3 casos para testar um dos lados menor do que 0 (retorna -1)
• Testes somente são capazes de mostrar a presença de
faltas, mas não a ausê ncia delas.
Especificação
• Objetivo dessa aula
– Apresentar os uma técnica para a criação testes automatizados e mostrar como desenvolver dirigido por testes.
• Referência básica:
– Monografia: Arcabouço para a Automação de Testes de Programas Redigidos em C; contido no arquivo TesteAutomatizado.zip acessível para download através do site da disciplina, aba: Software
• Referência adicional:
– Beck, K.; Test-Driven Development by Example; Reading, Massachusetts:
Addison-Wesley; 2003
• Slides adaptados de: Staa, A.v. Notas de Aula em Programacao Modular;
2008.
12 / 27
Sumário
• Automação simplória
• Arcabouço de apoio ao teste automatizado
• Linguagem de diretivas de teste
• Interpretador de diretivas
• Exemplo de uso do arcabouço
• Arquitetura do arcabouço
• Vantagens e desvantagens
Como testar um módulo?
• Uso de um módulo controlador do teste
desenvolvido que auxilia a execução de casos de teste para testar um módulo sob teste
• Três formas:
– Teste manual: controlador exibe um menu e
comparação é feita pelo próprio testador à olho nu – Teste de comparação automatizado:
• Casos de teste são implementados em C
• Casos de teste são redigidos em scripts em uma linguagem com uma sintaxe própria
14 / 30
Exemplo simplório
. . .
ContaCaso ++ ;
if ( CriarArvore( ) != ARV_CondRetOK ) {
printf( "\nErro ao criar árvore" ) ; ContaFalhas ++ ;
}
ContaCaso ++ ;
if ( InserirEsquerda('a' ) != ARV_CondRetOK ) {
printf( "\nErro ao inserir nó raiz da árvore" ) ; ContaFalhas ++ ;
}
ContaCaso ++ ;
if ( IrPai( ) != ARV_CondRetEhRaiz ) {
printf( "\nErro ao ir para pai de nó raiz" ) ; ContaFalhas ++ ;
}
. . .
instância de caso de teste
compara obtido com o esperado
É NECESSÁRIO REPETIR O MESMO CÓDIGO PARA CADA CASO DE TESTE
Por que é simplório?
• O código é muito repetitivo
– viola a regra de evitar duplicações de código
16 / 30 LES/DI/PU
Exemplo simplório
. . .
ContaCaso ++ ;
if ( CriarArvore( ) != ARV_CondRetOK ) {
printf( "\nErro ao criar árvore" ) ; ContaFalhas ++ ;
}
ContaCaso ++ ;
if ( InserirEsquerda('a' ) != ARV_CondRetOK ) {
printf( "\nErro ao inserir nó raiz da árvore" ) ; ContaFalhas ++ ;
}
ContaCaso ++ ;
if ( IrPai( ) != ARV_CondRetEhRaiz ) {
printf( "\nErro ao ir para pai de nó raiz" ) ; ContaFalhas ++ ;
}
. . .
Muita repetição
Por que é simplório?
• O código é muito repetitivo
– viola a regra de evitar duplicações de código
• O módulo controlador do teste pode tornar-se muito extenso
– dificulta verificar se o teste é um bom teste
• O código não produz um laudo do teste
– as mensagens impressas não explicitam o porquê da falha
• quais foram os valores que causaram a mensagem?
18 / 27
Como reduzir o volume de repetição?
• Criar um arcabouço (framework) com as funções de comparação.
Framework
Pontos de extensão
Aplicação
Serviço do arcabouço
Exemplo de uma função de comparação
TST_tpCondRet TST_CompararInt( long ValorEsperado , long ValorObtido , char * pMensagem ) {
if ( ValorObtido != ValorEsperado ) {
ContaFalhas ++ ;
fprintf( pArqLog , "\nErro >>> %s" , pMensagem ) ; fprintf( pArqLog , "Deveria ser: %ld É: %ld" ,
ValorEsperado , ValorObtido ) ; return TST_CondRetErro ;
} /* if */
return TST_CondRetOK ;
} /* Fim função: TSTG &Comparar inteiro */
mensagem de erro gera-se um laudo de
erros
20 / 27
Como fica o código agora?
. . .
if ( TST_CompararInt( ARV_CondRetOK , CriarArvore( ) ,
"Erro ao criar árvore" ) != TST_CondRetOK ) {
return ARV_CondRetOK;
}
if ( TST_CompararInt( ARV_CondRetOK , InserirEsquerda('a' ) ,
"Erro ao inserir nó raiz da árvore" ) != TST_CondRetOK ) {
return ARV_CondRetOK;
}
if ( TST_CompararInt( ARV_CondRetEhRaiz , IrPai( ) ,
"Erro ao ir para pai na raiz" ) != TST_CondRetOK ) {
return ARV_CondRetEhRaiz;
}
. . .
Opção 1: massa de teste escrita com a própria linguagem de programação
Como fica o código agora? Melhorou?
. . .
if ( TST_CompararInt( ARV_CondRetOK , CriarArvore( ) ,
"Erro ao criar árvore" ) != TST_CondRetOK ) {
return ; }
if ( TST_CompararInt( ARV_CondRetOK , InserirEsquerda('a' ) ,
"Erro ao inserir nó raiz da árvore" ) != TST_CondRetOK ) {
return ; }
if ( TST_CompararInt( ARV_CondRetEhRaiz , IrPai( ) ,
"Erro ao ir para pai na raiz" ) != TST_CondRetOK ) {
return ; }
if ( TST_CompararInt( ARV_CondRetOK , InserirEsquerda(‘b' ) ,
"Erro ao inserir nó raiz da árvore" ) != TST_CondRetOK ) {
return ; }
. . .
ainda é repetitivo, verboso
22 / 30
Melhorou?
• Sim, pode-se adicionar mais funcionalidades ao arcabouço
– outros comparadores
• Sim, ganhou-se uniformidade
• Não, o código continua extenso
• Não, pára na primeira falha encontrada
3ª. Opção: nossa solução
• A solução que adotamos é um módulo interpretador inspirado no JUnit
– permite-se criar linguagens de script para definição da massa de teste
– Objetivos:
• redução da complexidade e repetição do código de teste
• abstração de detalhes para o testador
– escreve-se cada caso de teste em uma única linha – desnecessário alterar o módulo controlador de teste
• torna-se a linguagem muito próxima da “linguagem natural”
• Para quem programa Java, C++, C# e outros, existem
diversos arcabouços: JUnit, CppUnit, CSUnit, ...
24 / 27
Exemplo de uma massa de teste
. . .
== Criar árvore
=criar OK
=irdir ArvoreVazia
== Inserir à direita
=insdir CharA OK
== Obter o valor inserido
=obter CharA OK
== Ir para no pai, não tem
=irpai EhRaiz
== Inserir à esquerda
=insesq CharB OK
=obter CharB OK
== Ir para no pai, tem
=irpai OK
=obter CharA OK . . .
Exemplo de uma massa de teste
. . .
== Criar árvore
=criar OK
=irdir ArvoreVazia
== Inserir à direita
=insdir CharA OK
== Obter o valor inserido
=obter CharA OK
== Ir para no pai, não tem
=irpai EhRaiz
== Inserir à esquerda
=insesq CharB OK
=obter CharB OK
== Ir para no pai, tem
=irpai OK
=obter CharA OK . . .
Se fôssemos escrever estes mesmos casos de teste com a abordagem anterior, teríamos que produzir 9 x 5 = 45 linhas de código em C.
Definição da Linguagem de Script de Teste
• Passo 1 – definir comandos (não são declarados no início do script):
Sintaxe: =<nome da função> <lista parâmetros> <condição retorno>
Exemplo – Módulo Árvore:
=criar < > <OK, FaltouMemoria, etc...>
=irdir < > <OK, ArvoreVazia, etc...>
=insdir <char> <OK, ValorInvalido, FaltouMemoria, etc..>
=irpai < > <OK, EhRaiz, etc...>
=obter <char> <OK, ValorInvalido, etc...>
etc..
• Passo 2 - declaração dos valores simbólicos
Sintaxe: =declararparm <nome simbólico> <tipo> <valor>
• tipos conhecidos: int , char , float , string
Exemplos
== Declarar as condições de retorno
=declararparm OK int 0
=declararparm ArvoreVazia int 5
== Declarar os valores contidos nos nós
=declararparm CharA char 'a'
26 / 27
Arquitetura simplificada da nossa abordagem
Programa principal
Módulo em teste
Teste de item de interface Função
de controle específica
Módulo de controle
genérico Diretivas
de teste
Log do teste Módulo de
leitura LERPARM.h
GENERICO.h PRINCIP.h
coordena a realização dos testes; disponibiliza funções genéricas de comparação
TST_ESPC.h
interpreta os comandos genéricos e específicos
*.script
Construindo módulo controlador de teste...
28 / 27
• Exemplo: TestaArvore.c
– interface: TST_ESPC.h (interface única para todos módulos de teste)
– Passo 1 – incluindo as interfaces necessárias
#include "TST_ESPC.H"
#include "generico.h"
#include "lerparm.h"
#include "arvore.h"
interface do módulo sendo testado...
interfaces do arcabouço...
Construindo módulo controlador de teste...
• Exemplo: TestaArvore.c
– Passo 2 – declarando comandos da linguagem de script de teste
/* Nomes dos comandos de teste específicos */
#define CRIAR_ARV_CMD "=criar"
#define INS_DIR_CMD "=insdir"
#define INS_ESQ_CMD "=insesq"
#define IR_PAI_CMD "=irpai"
#define IR_ESQ_CMD "=iresq"
#define IR_DIR_CMD "=irdir"
#define OBTER_VAL_CMD "=obter"
#define DESTROI_CMD "=destruir"
Instrução de pré-processamento para definição de constantes...
Construindo módulo controlador de teste...
30 / 27
• Exemplo: TestaArvore.c
– Passo 3 – implementar função de tratamento e execução do comando de teste
/*****************************************************************
******
*
* $FC Função: Efetuar operações de teste específicas para árvore
*
* $EP Parâmetros
* $P ComandoTeste - String contendo o comando
*
******************************************************************
*****/
TST_tpCondRet TST_EfetuarComando( char * ComandoTeste ) {
}
completar tratamento de cada comando sequencia de if e else ifs
Exemplo de fragmento do controlador
if ( strcmp( ComandoTeste , CRIAR_ARV_CMD ) == 0 ) {
…
/* Testar ARV Adicionar filho à direita */
else if ( strcmp( ComandoTeste , INS_DIR_CMD ) == 0 ) {
NumLidos = LER_LerParametros( "ci" ,
&ValorDado , &CondRetEsperada ) ; if ( NumLidos != 2 )
{
return TST_CondRetParm ; } /* if */
return TST_CompararInt( CondRetEsperada , ARV_InserirDireita( ValorDado ) ,
"Retorno errado inserir a direita." );
} /* fim: Testar ARV Adicionar filho à direita */
Ago 2008 Arndt von Staa © LES/DI/PUC-Rio 32 / 27
Como usar?
• Forma “big bang”
1. Especificar a interface do módulo
• arquivo .h
2. Especificar a linguagem de diretivas de teste, sintaxe
• =<nome da função> <lista parâmetros> <condição retorno>
3. Redigir a massa de teste nesta linguagem
• serve como uma especificação executável!
4. Redigir o módulo de teste específico 5. Redigir o módulo a ser testado
6. Rever cuidadosamente os cinco artefatos 7. Compilar
• usar a biblioteca: ArcaboucoTeste.lib
8. Executar o teste
9. Corrigir até zero falhas observadas pelo teste
Como usar?
• Forma iterativa
– preparação inicial
1. Especificar parcialmente a interface do módulo a desenvolver 2. Especificar a linguagem de diretivas de teste
3. Redigir parte da massa de teste nesta linguagem 4. Redigir a versão inicial do módulo de teste específico
– todos os comandos retornam TST_CondRetNaoImplementado 5. Redigir o enchimento (stub) do módulo a ser testado
– todas as funções fazem nada, se necessário retornam um valor neutro 6. Rever cuidadosamente os cinco artefatos
7. Compilar
– usar a biblioteca: ArcaboucoTeste.lib 8. Executar o teste
9. Corrigir até que sejam gerados somente erros de comando não implementado
Ago 2008 Arndt von Staa © LES/DI/PUC-Rio 34 / 27
Como usar?
• Forma iterativa
– iteração
1. Escolher as funções a serem implementadas 2. Rever ou completar a interface do módulo 3. Rever a linguagem de diretivas de teste 4. Redigir a massa de teste nesta linguagem
5. Redigir a parte do módulo de teste específico visando as funções 6. Redigir as funções do módulo a ser testado
7. Rever cuidadosamente os cinco artefatos 8. Compilar
– usar a biblioteca: ArcaboucoTeste.lib 9. Executar o teste
10.Corrigir até que – esteja tudo correto
– sejam gerados somente erros de comando não implementado
Exemplo prático
• Exemplo \arcabouc\simples
Ago 2008 Arndt von Staa © LES/DI/PUC-Rio 36 / 27
Quais seriam as vantagens?
• Teste automatizado exige rigor ao escrever
– as especificações das interfaces – o script de teste
– embora alguns não acreditem, rigor é sempre vantagem!
• Facilita o desenvolvimento incremental do módulo
– a cada incremento pode-se retestar integralmente o que já foi feito (teste de regressão)
• o que não foi alterado ou acrescido deveria continuar operando tal como esperado
• o que foi alterado e acrescido tem o teste ajustado
• A função de teste específico serve como exemplo de uso do
módulo
Quais seriam as vantagens?
• O script de teste serve como especificação executável do módulo
– apesar de ser uma especificação incompleta e baseada em exemplos, freqüentemente é mais precisa do que
especificações textuais
• Os problemas encontrados são repetíveis, facilitando a depuração
– reduz significativamente o esforço de teste quando se leva em conta a necessidade de reteste após corrigir ou evoluir o
módulo
• Caso a realização do teste gere um arquivo de log
– documenta os laudos dos testes realizados
– assegura a existência da história da evolução durante o teste.
– passa a ser um atestado da qualidade do módulo
Ago 2008 Arndt von Staa © LES/DI/PUC-Rio 38 / 27
Quais seriam as vantagens?
• O esforço de redação do módulo de teste específico é pouco maior do que o esforço de redação de um controlador de teste manual.
• Através da linguagem de diretivas de teste pode-se realizar testes tão complexos e detalhados quanto se queira.
– casos de teste selecionados segundo critérios de seleção bem definidos
– redigir as diretivas de teste requer monos esforço do que redigir um controlador de teste contendo o roteiro
• Permite documentar os casos de teste
– os títulos ( “==” ) informam a intenção do caso de teste – necessário para que outros possam mantê-los
Quais seriam as vantagens?
• Permite estabelecer com precisão onde o debugger deve ser ativado.
– permite longos processamentos necessário para estabelecer o contexto
– comando de teste genérico: =breakpoint
• Pode ser combinado com funções de teste complexas disparadas por diretivas
• Reduz o estresse do desenvolvedor
– é possível particionar o desenvolvimento em etapas – desenvolvimento incremental
– cada qual culminando com um módulo parcial porém corretamente implementado
Ago 2008 Arndt von Staa © LES/DI/PUC-Rio 40 / 27
E quais seriam as desvantagens?
• O arquivo de diretivas é na realidade um programa
– pode conter defeitos
– se não tomar cuidado, a linguagem e/ou o script de teste tornam-se extensos e complicados
• Ao encontrar uma falha é necessário determinar se é
– defeito no módulo em teste
– defeito no módulo de teste específico – defeito no script de teste
– defeito na especificação do módulo
• A linguagem ad hoc utilizada aqui não permite a redação de subprogramas (por enquanto )
– subprogramas podem ser codificados no módulo de teste específico e disparados por um comando.
E quais seriam as desvantagens?
• Custo inicial maior
– ganha-se ao retestar, ou seja, sempre
• Ao alterar um módulo, obriga a evoluir coevolução
– obviamente o módulo sob teste – o módulo de teste específico
– o script de teste, isso pode tornar-se um problema
• se os casos de teste forem mal documentados
• se o script de teste for mal organizado
• se o script de teste não utilizar parâmetros simbólicos
• Nem sempre é possível utilizar teste automatizado
– exibição (rendering) de figuras, gráficos
– interfaces com os sistemas GUI (ex. windows) – leiaute de janelas
– . . .
42 / 30 LES/DI/PU
PARA CASA: exemplos práticos de uso do arcabouço
• Teste manual:
– Exemplo \arcabouc\manual: mostrado como construir um módulo e o respectivo controlador de teste manual
• Teste automatizado:
– Exemplo \arcabouc\simples: mostrado como construir um módulo e o respectivo módulo de teste específico utilizando a biblioteca do arcabouço de teste
• Comparar a implementação dos dois
– os exemplos tratam de um módulo editor de árvores binárias
• VIDE VÍDEOS EM “INSTALAÇÃO E USO DO ARCABOUÇO”
Aula 05
Teste Automatizado – Parte I e II
Alessandro Garcia LES/DI/PUC-Rio
Março de 2019