Curso Intensivo de Python Uma Introducao Pratica e Baseada em Projetos A Programacao
Curso Intensivo de Python Uma Introducao Pratica e Baseada em Projetos A Programacao
Curso Intensivo de Python Uma Introducao Pratica e Baseada em Projetos A Programacao
TESTE DE SOFTWARE
INTRODUÇÃO AO
TESTE DE SOFTWARE
Márcio Eduardo Delamaro
José Carlos Maldonado
Mario Jino
Consultoria Editorial
Sergio Guedes
Núcleo de Computação Eletrônica
Universidade Federal do Rio de Janeiro
4ª Tiragem
c 2007, Elsevier Editora Ltda.
Todos os direitos reservados e protegidos pela Lei 9.610 de 19/02/1998.
Nenhuma parte deste livro, sem autorização prévia por escrito da editora,
poderá ser reproduzida ou transmitida sejam quais forem os meios empregados:
eletrônicos, mecânicos, fotográficos, gravação ou quaisquer outros.
Copidesque:
Gypsi Canetti
Editoração Eletrônica:
Márcio Delamaro
Revisão Gráfica:
Marília Pinto de Oliveira
Renato Carvalho
Projeto Gráfico
Elsevier Editora Ltda.
A Qualidade da Informação.
Rua Sete de Setembro, 111 – 16o andar
20050-006 Rio de Janeiro RJ Brasil
Telefone: (21) 3970-9300 FAX: (21) 2507-1991
E-mail: info@elsevier.com.br
Escritório São Paulo:
Rua Quintana, 753/8o andar
04569-011 Brooklin São Paulo SP
Tel.: (11) 5105-8555
Nota: Muito zelo e técnica foram empregados na edição desta obra. No entanto, podem ocorrer erros de digita-
ção, impressão ou dúvida conceitual. Em qualquer das hipóteses, solicitamos a comunicação à nossa Central de
Atendimento, para que possamos esclarecer ou encaminhar a questão.
Nem a editora nem os autores assumem qualquer responsabilidade por eventuais danos ou perdas a pessoas ou
bens, originados do uso desta publicação.
Central de atendimento
Tel.: 0800-265340
Rua Sete de Setembro, 111, 16o andar – Centro – Rio de Janeiro
e-mail: info@elsevier.com.br
site: www.campus.com.br
CIP-BRASIL. CATALOGAÇÃO-NA-FONTE
SINDICATO NACIONAL DOS EDITORES DE LIVROS, RJ
I48
Introdução ao teste de software / organização Márcio
Eduardo Delamaro, José Carlos Maldonado, Mario Jino
– Rio de Janeiro : Elsevier, 2007 – 4a reimpressão
ISBN 978-85-352-2634-8
1. Software – Testes. I. Delamaro, Márcio, 1963–.
II. Maldonado, José Carlos, 1954 –. III. Jino, Mario.
Temos notado um enorme crescimento do interesse, por parte dos desenvolvedores, nas ques-
tões relacionadas à qualidade de software. Em particular, a indústria tem despertado para a
extrema importância da atividade de teste que, por um lado, pode contribuir para a melho-
ria da qualidade de um determinado produto e, por outro, representar um custo significativo
dentro dos orçamentos empresariais.
Por isso, torna-se indispensável que, no processo de desenvolvimento de software, sejam
adotados métodos, técnicas e ferramentas que permitam a realização da atividade de teste de
maneira sistematizada e com fundamentação científica, de modo a aumentar a produtividade
e a qualidade e a diminuir custos.
Um dos problemas que sentimos é a escassez de mão-de-obra especializada nessa área.
Assim, o primeiro objetivo deste livro é servir como livro-texto para disciplinas de cursos
relacionados ao desenvolvimento de software como Ciência ou Engenharia da Computação e
Sistemas de Informação.
Acreditamos servir, também, como um texto introdutório para profissionais da área que
necessitam de uma fonte de consulta e aprendizado. Neste livro tal profissional poderá en-
contrar as informações básicas relativas às técnicas de teste, bem como formas de aplicá-las
nos mais variados domínios e tipos de software.
Podemos dividir o livro em três partes. A primeira, composta pelos Capítulos 1 a 5, apre-
senta as principais técnicas de teste, mostrando a teoria e os conceitos básicos relacionados
a cada uma delas. Sempre que possível, apresenta-se um histórico de como surgiu e como
evoluiu cada técnica. Na segunda parte, que vai do Capítulo 6 ao 9, discute-se como essas
técnicas têm sido empregadas em diversos tipos de software e diversos domínios de aplica-
ção como: aplicações Web, programas baseados em componentes, programas concorrentes e
programas baseados em aspectos. Na terceira parte, composta pelos Capítulos 10 a 13, são
tratados temas intimamente relacionados ao teste de software e que devem ser, também, de
interesse dos desenvolvedores: geração de dados de teste, depuração e confiabilidade.
Em cada um dos capítulos procuramos dar uma visão relativamente aprofundada de cada
um dos temas e, também, uma visão de quais são os mais importantes trabalhos realizados e
que constituem o estado da arte na área. Uma farta lista de referências bibliográficas foi in-
cluída para permitir aos interessados o aprofundamento nesta atividade tão necessária dentro
da indústria de desenvolvimento de software e nesse tão fascinante campo de pesquisa.
ix
Os autores
A tabela a seguir indica os nomes dos autores e os capítulos para os quais eles contribuíram.
Capítulos
Autores
1 2 3 4 5 6 7 8 9 10 11 12 13
Adalberto
Adenilso
André
Auri
Chaim
Cláudia
Delamaro
Ellen
Jino
Maldonado
Masiero
Otávio
Paulo
Reginaldo
Sandra
Silvia
Simone
x
Maldonado: José Carlos Maldonado, professor titular e pesquisador do ICMC/USP, São
Carlos (jcmaldon@icmc.usp.br).
Masiero: Paulo César Masiero, professor titular e pesquisador do ICMC/USP, São Carlos
(masiero@icmc.usp.br).
Otávio: Otávio Augusto Lazzarini Lemos, doutorando em Ciência da Computação no
ICMC/USP, São Carlos (oall@icmc.usp.br).
Paulo: Paulo Sérgio Lopes de Souza, professor adjunto e pesquisador do ICMC/USP, São
Carlos (pssouza@icmc.usp.br).
Reginaldo: Reginaldo Ré, professor de 1o e 2o graus e pesquisador da UTFPR, Campo
Mourão (reginaldo@utfpr.edu.br).
Sandra: Sandra C. Pinto Ferraz Fabbri, professora adjunta e pesquisadora da UFSCar, São
Carlos (sfabbri@dc.ufscar.br).
xi
Capítulo 1
Conceitos Básicos
1.1 Introdução
Para que possamos tratar de um assunto tão vasto quanto o proposto neste livro é necessário,
primeiro, estabelecer uma linguagem própria. Como acontece em muitas das áreas da Com-
putação e das ciências em geral, termos comuns adquirem significados particulares quando
usados tecnicamente.
Este capítulo tem o objetivo de estabelecer o escopo no qual o restante do livro se insere,
de apresentar os principais termos do jargão da área e introduzir conceitos que serão úteis
durante a leitura do restante do texto.
Por isso, a atividade de teste é dividida em fases com objetivos distintos. De uma forma
geral, podemos estabelecer como fases o teste de unidade, o teste de integração e o teste de
sistemas. O teste de unidade tem como foco as menores unidades de um programa, que po-
dem ser funções, procedimentos, métodos ou classes. Nesse contexto, espera-se que sejam
identificados erros relacionados a algoritmos incorretos ou mal implementados, estruturas
de dados incorretas, ou simples erros de programação. Como cada unidade é testada sepa-
radamente, o teste de unidade pode ser aplicado à medida que ocorre a implementação das
unidades e pelo próprio desenvolvedor, sem a necessidade de dispor-se do sistema totalmente
finalizado.
No teste de integração, que deve ser realizado após serem testadas as unidades indivi-
dualmente, a ênfase é dada na construção da estrutura do sistema. À medida que as diversas
partes do software são colocadas para trabalhar juntas, é preciso verificar se a interação entre
elas funciona de maneira adequada e não leva a erros. Também nesse caso é necessário
um grande conhecimento das estruturas internas e das interações existentes entre as partes
do sistema e, por isso, o teste de integração tende a ser executado pela própria equipe de
desenvolvimento.
Depois que se tem o sistema completo, com todas as suas partes integradas, inicia-se o
teste de sistema. O objetivo é verificar se as funcionalidades especificadas nos documentos
de requisitos estão todas corretamente implementadas. Aspectos de correção, completude
e coerência devem ser explorados, bem como requisitos não funcionais como segurança,
performance e robustez. Muitas organizações adotam a estratégia de designar uma equipe
independente para realizar o teste de sistemas.
Além dessas três fases de teste, destacamos, ainda, o que se costuma chamar de “teste de
regressão”. Esse tipo de teste não se realiza durante o processo “normal” de desenvolvimento,
mas sim durante a manutenção do software. A cada modificação efetuada no sistema, após
a sua liberação, corre-se o risco de que novos defeitos sejam introduzidos. Por esse motivo,
é necessário, após a manutenção, realizar testes que mostrem que as modificações efetuadas
estão corretas, ou seja, que os novos requisitos implementados (se for esse o caso) funcionam
como o esperado e que os requisitos anteriormente testados continuam válidos.
Independentemente da fase de teste, existem algumas etapas bem definidas para a execu-
ção da atividade de teste. São elas: 1) planejamento; 2) projeto de casos de teste; 3) execução;
e 4) análise.
Isso significa que, na prática, é impossível mostrar que um programa está correto por meio
de teste. Isso porque, aliado a algumas limitações teóricas citadas a seguir, ao se escolher um
subconjunto de D(P ) corre-se sempre o risco de deixar de fora algum caso de teste que
poderia revelar a presença de um defeito.
Por isso costumamos dizer que o objetivo da atividade de teste não é, como podem pen-
sar muitos, mostrar que um programa está correto. Ao invés disso, o objetivo é mostrar a
presença de defeitos caso eles existam. Quando a atividade de teste é realizada de maneira
criteriosa e embasada tecnicamente, o que se tem é uma certa “confiança” de que se comporta
corretamente para grande parte do seu domínio de entrada.
Algumas limitações teóricas são importantes na definição de critérios de teste, princi-
palmente daqueles baseados no código, ou seja, que utilizam a estrutura do programa para
derivar os requisitos de teste. Podemos citar, por exemplo:
Não existe, em princípio, nenhuma restrição sobre o tipo de critérios de teste que pode
ser definido e utilizado. Entretanto, a fim de se obter um nível mínimo de qualidade para os
conjuntos adequados a um critério C para um programa P, deve-se requerer que:
Capítulo 2
Teste Funcional
2.1 Introdução
Teste funcional é uma técnica utilizada para se projetarem casos de teste na qual o programa
ou sistema é considerado uma caixa preta e, para testá-lo, são fornecidas entradas e avaliadas
as saídas geradas para verificar se estão em conformidade com os objetivos especificados.
Nessa técnica os detalhes de implementação não são considerados e o software é avaliado
segundo o ponto de vista do usuário.
Em princípio, o teste funcional pode detectar todos os defeitos, submetendo o programa
ou sistema a todas as possíveis entradas, o que é denominado teste exaustivo [294]. No
entanto, o domínio de entrada pode ser infinito ou muito grande, de modo a tornar o tempo
da atividade de teste inviável, fazendo com que essa alternativa se torne impraticável. Essa
limitação da atividade de teste, que não permite afirmar, em geral, que o programa esteja
correto, fez com que fossem definidas as técnicas de teste e os diversos critérios pertencentes
a cada uma delas. Assim, é possível conduzir essa atividade de maneira mais sistemática,
podendo-se inclusive, dependendo do critério utilizado, ter uma avaliação quantitativa da
atividade de teste.
O que distingue essencialmente as três técnicas de teste – Funcional, Estrutural e Baseada
em Erros – é a fonte utilizada para definir os requisitos de teste. Além disso, cada critério de
teste procura explorar determinados tipos de defeitos, estabelecendo requisitos de teste para
os quais valores específicos do domínio de entrada do programa devem ser definidos com o
intuito de exercitá-los.
Considerando a técnica de teste funcional, um de seus critérios é o Particionamento de
Equivalência, como será visto com mais detalhe adiante. O teste de partição tem o objetivo
de fazer uma divisão (particionamento) do domínio de entrada do programa de tal modo que,
quando o testador seleciona casos de teste dos subconjuntos (partições ou subdomínios), o
conjunto de testes resultante é uma boa representação de todo o domínio. Segundo a visão de
alguns autores, o teste de partição e o teste randômico são, basicamente, as duas abordagens
i i
i i
i i
2.2 Histórico
Desde os anos 70 encontra-se literatura sobre teste funcional [294, 180, 31]. Nessa época
também surgiram vários métodos para apoiar a especificação de sistemas, como a Análise e
Projeto Estruturado [147], no qual eram mencionados, embora não diretamente, aspectos de
validação do sistema com relação à satisfação dos seus requisitos funcionais. Nesse sentido,
algumas técnicas usadas para descrever tais requisitos de forma completa e não ambígua são
técnicas que também estão incorporadas em critérios funcionais, como é o caso das Tabelas
de Decisão, utilizadas no critério Grafo Causa-Efeito, como será visto neste capítulo.
Roper [344] faz uma discussão sobre a atividade de teste ao longo do ciclo de vida de
desenvolvimento, considerando vários aspectos e características dos métodos de análise e
projeto em relação ao teste funcional e às demais técnicas abordadas neste livro.
Outro critério do teste funcional, o Particionamento de Equivalência, que a princípio
considerava a elaboração de casos de teste baseados apenas no domínio de entrada do pro-
grama [294], passou a ser tratado posteriormente de dois pontos de vista, o de entrada e o de
saída [344].
i i
i i
i i
2.3. Critérios 11
Mais recentemente, motivados pela baixa cobertura dos critérios funcionais em relação a
outros critérios estruturais e baseados em erros, Linkman et al. [243] propuseram o critério
Teste Funcional Sistemático, que tem o objetivo de promover maior cobertura em relação a
erros típicos considerados no critério Análise de Mutantes, o qual será abordado no Capí-
tulo 5.
2.3 Critérios
Os critérios mais conhecidos da técnica de teste funcional são Particionamento de Equivalên-
cia, Análise do Valor Limite, Grafo Causa-Efeito e Error-Guessing. Além desses, também
existem outros, como, por exemplo, Teste Funcional Sistemático [243], Syntax Testing, State
Transition Testing e Graph Matrix [216].
Como todos os critérios da técnica funcional baseiam-se apenas na especificação do pro-
duto testado, a qualidade de tais critérios depende fortemente da existência de uma boa es-
pecificação de requisitos. Especificações ausentes ou mesmo incompletas tornarão difícil a
aplicação dos critérios funcionais. Além disso, tais critérios também apresentam a limitação
de não garantir que partes essenciais ou críticas do produto em teste sejam exercitadas [31].
Por outro lado, os critérios funcionais podem ser aplicados em todas as fases de testes
e em produtos desenvolvidos com qualquer paradigma de programação, pois não levam em
consideração os detalhes de implementação [92].
A seguir apresenta-se a especificação do programa “Cadeia de Caracteres”, extraída de
Roper [344], que será utilizada para exemplificar alguns desses critérios.
Especificação do programa “Cadeia de Caracteres”:
O programa solicita do usuário um inteiro positivo no intervalo entre 1 e 20 e então
solicita uma cadeia de caracteres desse comprimento. Após isso, o programa solicita um
caractere e retorna a posição na cadeia em que o caractere é encontrado pela primeira vez
ou uma mensagem indicando que o caractere não está presente na cadeia. O usuário tem a
opção de procurar vários caracteres.
i i
i i
i i
tamanho passível de ser tratado durante a atividade de teste. Além do domínio de entrada,
alguns autores consideram também o domínio de saída, identificando neste possíveis alterna-
tivas que poderiam determinar classes de equivalência no domínio de entrada [344, 92].
Para ajudar na identificação das partições, pode-se observar a especificação procurando
termos como “intervalo” e “conjunto” ou palavras similares que indiquem que os dados são
processados da mesma forma.
Uma classe de equivalência representa um conjunto de estados válidos ou inválidos para
as condições de entrada. As classes podem ser definidas de acordo com as seguintes diretri-
zes [294]: 1) se a condição de entrada especifica um intervalo de valores, são definidas uma
classe válida e duas inválidas; 2) se a condição de entrada especifica uma quantidade de va-
lores, são definidas uma classe válida e duas inválidas; 3) se a condição de entrada especifica
um conjunto de valores determinados e o programa pode manipulá-los de forma diferente, é
definida uma classe válida para cada um desses valores e uma classe inválida com outro valor
qualquer; 4) se a condição de entrada especifica uma situação do tipo “deve ser de tal forma”,
são definidas uma classe válida e uma inválida.
Caso seja observado que as classes de equivalência se sobrepõem ou que os elementos de
uma mesma classe não devem se comportar da mesma maneira, elas devem ser reduzidas a
fim de separá-las e torná-las distintas.
Uma vez identificadas as classes de equivalência, devem-se determinar os casos de teste,
escolhendo-se um elemento de cada classe, de forma que cada novo caso de teste cubra o
maior número de classes válidas possível. Já para as classes inválidas, devem ser gerados
casos de testes exclusivos, uma vez que um elemento de uma classe inválida pode mascarar a
validação do elemento de outra classe inválida [92].
Exemplo de aplicação
De acordo com a descrição do critério, os dois passos a serem realizados são: i) identi-
ficar as classes de equivalência, o que deve ser feito observando-se as entradas e as saídas
do programa com o objetivo de particionar o domínio de entrada e; ii) gerar casos de teste
selecionando um elemento de cada classe, de forma a ter o menor número de casos de teste
possível.
Considerando a especificação dada anteriormente, têm-se quatro entradas:
i i
i i
i i
2.3. Critérios 13
Essa informação pode ser usada para fazer outra partição no domínio de entrada: uma
contendo caracteres que são encontrados na cadeia e a outra contendo caracteres que não
pertencem à cadeia. A Tabela 2.1 apresenta as classes de equivalência identificadas.
Avaliação do critério
A força desse critério está na redução que ele possibilita no tamanho do domínio de
entrada e na criação de dados de teste baseados unicamente na especificação. Ele é especial-
mente adequado para aplicações em que as variáveis de entrada podem ser identificadas com
facilidade e assumem valores específicos. No entanto, o critério não é tão facilmente aplicá-
vel quando o domínio de entrada é simples, mas o processamento é complexo. Um problema
possível de ocorrer é que, embora a especificação possa sugerir que um grupo de dados seja
processado de forma idêntica, na prática isso pode não acontecer. Além disso, a técnica não
fornece diretrizes para a determinação dos dados de teste e para encontrar combinações entre
eles que permitam cobrir as classes de equivalência de maneira mais eficiente [344].
i i
i i
i i
De acordo com Meyers [294], a experiência mostra que casos de teste que exploram condi-
ções limites têm uma maior probabilidade de encontrar defeitos. Tais condições correspon-
dem a valores que estão exatamente sobre ou imediatamente acima ou abaixo dos limitantes
das classes de equivalência.
Assim, esse critério é usado em conjunto com o particionamento de equivalência, mas
em vez de os dados de teste serem escolhidos aleatoriamente, eles devem ser selecionados de
forma que o limitante de cada classe de equivalência seja explorado. Segundo Meyers [294],
além da escolha seletiva dos dados de teste, o outro ponto que distingue esse critério do Par-
ticionamento de Equivalência é a observação do domínio de saída, diferentemente de outros
autores, que já fazem essa consideração no próprio critério de Particionamento de Equiva-
lência.
Embora não existam diretrizes bem definidas que levem à determinação dos dados de
teste, Meyers [294] sugere que as seguintes recomendações sejam seguidas:
Exemplo de aplicação
Considerando as classes de equivalência segundo o critério Particionamento de Equiva-
lência (Tabela 2.1) e lembrando que devem ser explorados os limites tanto do ponto de vista
de entrada como de saída, os casos de teste que satisfazem o critério Análise do Valor Limite
são apresentados na Tabela 2.3. Observa-se que a linha dupla na tabela denota uma segunda
execução do programa, pois só assim todos os casos de teste necessários para satisfazer o
critério podem ser exercitados.
Um outro exemplo que caracteriza bem a diferença entre o Particionamento de Equiva-
lência e Análise do Valor Limite é o exemplo do triângulo, dado por Meyers [294]. Para que
três valores representem um triângulo, eles devem ser inteiros maiores que zero e a soma de
quaisquer desses dois valores deve ser maior que o terceiro valor. Assim, ao definirem-se
as classes de equivalência, a válida seria aquela em que essa condição é satisfeita e a classe
inválida seria aquela em que a soma de dois dos valores não é maior que o terceiro valor. Para
i i
i i
i i
2.3. Critérios 15
1. Valores numéricos
Para o domínio de entrada, selecionar valores de entrada da seguinte forma:
i i
i i
i i
Para o domínio de saída, selecionar valores de entrada que resultem nos valores de saída
que devem ser gerados pelo software. Os tipos das saídas podem não coincidir com os
tipos dos valores de entrada; por exemplo, valores de entrada distintos podem produzir
um intervalo de valores de saída, dependendo de outros fatores, ou um intervalo de
valores de entrada pode produzir somente um ou dois valores de saída como verdadeiro
e falso. Assim, devem-se escolher como entrada valores que explorem os valores de
saída da seguinte forma:
3. Valores ilegais
Valores que correspondem a entradas ilegais também devem ser incluídos nos casos
de teste, para assegurar que o software os rejeita. Deve-se também tentar gerar valores
ilegais, os quais não devem ser bem-sucedidos. É importante que sejam selecionados os
limites dos intervalos numéricos, tanto inferior quanto superior, valores imediatamente
fora dos limites desses intervalos e também os valores imediatamente subseqüentes aos
limites do intervalo e pertencentes a ele.
4. Números reais
Valores reais envolvem mais problemas do que valores inteiros, uma vez que, na en-
trada, são fornecidos como números decimais, para processamento, são armazenados
na forma binária e, como saída, são convertidos em decimais novamente. Verificar o
limite para números reais pode não ser exato, mas, ainda assim, essa verificação deve
ser incluída como caso de teste. Deve ser definida uma margem de erro de tal forma
que, se ultrapassada, então o valor pode ser considerado distinto. Além disso, devem
ser selecionados números reais bem pequenos e também o zero.
5. Intervalos variáveis
Ocorre quando o intervalo de uma variável depende do valor de outra variável. Por
exemplo, suponha que o valor da variável x pode variar de zero ao valor da variável
y e que y é um inteiro positivo. Nesse caso, os seguintes dados de entrada devem ser
definidos: i) x = y = 0; ii) x = 0 < y; iii) 0 < x = y; iv) 0 < x < y. Além disso,
devem-se também selecionar os seguintes valores ilegais: i) y = 0 < x; ii) 0 < y < x;
iii) x < 0; e iv) y < 0.
6. Arranjos
Quando se usa arranjo, tanto como entrada como saída, deve-se considerar o fato de o
tamanho do arranjo ser variável bem como de os dados serem variáveis. Os elementos
i i
i i
i i
2.3. Critérios 17
do arranjo devem ser testados como se fossem variáveis comuns, como mencionado
no item anterior. Além disso, o tamanho do arranjo deve ser testado com valores in-
termediários, com os valores mínimo e máximo. Para simplificar o teste, podem-se
considerar as linhas e colunas de um arranjo como se fossem subestruturas, a serem
testadas em separado. Assim, o arranjo deve ser testado primeiro como uma única es-
trutura, depois como uma coleção de subestruturas, e cada subestrutura deve ser testada
independentemente.
7. Dados tipo texto ou string
Neste caso o dado de entrada deve explorar comprimentos variáveis e também validar
os caracteres que o compõem, pois algumas vezes os dados de entrada podem ser ape-
nas alfabéticos, outras vezes, alfanuméricos, e algumas vezes podem possuir caracteres
especiais.
Além desses casos, como foi dito inicialmente, o critério Funcional Sistemático requer
que dois elementos de uma mesma classe de equivalência sejam explorados para evitar erros
do seguinte tipo: suponha que o programa receba um valor de entrada e produza seu quadrado.
Se o valor de entrada for 2, e o valor produzido como saída for 4, embora o resultado esteja
correto, ainda assim não se pode afirmar qual das operações o programa realizou, pois poderia
ser 2 ∗ 2 como também 2 + 2.
Exemplo de aplicação
Como o critério Funcional Sistemático estende os critérios Particionamento de Equiva-
lência e Análise do Valor Limite, os casos de testes definidos nas Tabelas 2.2 e 2.3 deveriam
ser selecionados.
Além desses, os casos de teste da Tabela 2.4 seriam também desenvolvidos.
i i
i i
i i
Avaliação do critério
Por ser baseado nos critérios Particionamento de Equivalência e Análise do Valor Limite,
o critério Funcional Sistemático apresenta os mesmos problemas que estes. Entretanto, assim
como o critério Análise do Valor Limite, este critério fornece diretrizes para facilitar a geração
de casos de testes e enfatiza mais fortemente a seleção de mais de um caso de teste por
partição e/ou limite, aumentando assim a chance de revelar os defeitos sensíveis a dados e
também a probabilidade de obter uma melhor cobertura do código do produto que está sendo
testado.
Grafo Causa-Efeito
Uma das limitações dos critérios anteriores é que eles não exploram combinações dos dados
de entrada. Já o critério Grafo Causa-Efeito ajuda na definição de um conjunto de casos de
teste que exploram ambigüidades e incompletude nas especificações. O grafo é uma lingua-
gem formal na qual a especificação é traduzida e o processo para derivar casos de teste a
partir desse critério pode ser resumido nos seguintes passos [294]:
i i
i i
i i
2.3. Critérios 19
Os efeitos são:
70 - a atualização é realizada;
71 - a mensagem X é enviada;
72 - a mensagem Y é enviada.
i i
i i
i i
• Restrição E: no máximo um entre “1” e “2” pode ser igual a 1 (ou seja, “1” e “2” não
podem ser 1 simultaneamente).
• Restrição I: no mínimo um entre “1”, “2” e “3” deve ser igual a 1 (ou seja, “1”, “2” e
“3” não podem ser 0 simultaneamente).
• Restrição O: um e somente um entre “1” e “2” deve ser igual a 1.
• Restrição R: para que “1” seja igual a 1, “2” deve ser igual a 1 (ou seja, é impossível
que “1” seja 1 se “2” for 0).
• Restrição M: se o efeito “1” é 1 o efeito “2” é forçado a ser 0.
i i
i i
i i
2.3. Critérios 21
1. Quando o nó for do tipo OR e a saída deva ser 1, nunca atribuir mais de uma entrada
com valor 1 simultaneamente. O objetivo disso é evitar que alguns erros não sejam
detectados pelo fato de uma causa mascarar outra.
2. Quando o nó for do tipo AND e a saída deva ser 0, todas as combinações de entrada
que levem à saída 0 devem ser enumeradas. No entanto, se a situação é tal que uma
entrada é 0 e uma ou mais das outras entradas é 1, não é necessário enumerar todas as
condições em que as outras entradas sejam iguais a 1.
i i
i i
i i
3. Quando o nó for do tipo AND e a saída deva ser 0, somente uma condição em que todas
as entradas sejam 0 precisa ser enumerada. (Se esse AND estiver no meio do grafo, de
forma que suas entradas estejam vindo de outros nós intermediários, pode ocorrer um
número excessivamente grande de situações nas quais todas as entradas sejam 0.)
Seguindo essas diretrizes, a tabela de decisão para o grafo apresentado na Figura 2.4 é
apresentada na Figura 2.5.
1 0 0 1 0 1
2 0 0 0 1 0
3 0 1 1 1 0
70 0 0 1 1 0
71 1 1 0 0 0
72 1 0 0 0 1
O passo final é converter cada coluna da tabela de decisão em um caso de teste, como
será visto no exemplo a seguir.
Exemplo de aplicação
Considerando-se a especificação do programa “Cadeia de Caracteres” que vem sendo
usada para exemplificar os critérios anteriores, podem ser identificadas as seguintes causas:
e os seguintes efeitos:
i i
i i
i i
2.3. Critérios 23
1 0 1 1 -
2 - 1 0 -
3 - 1 1 0
20 1 0 0 0
21 0 1 0 0
22 0 0 1 0
23 0 0 0 1
Avaliação do critério
A vantagem do Grafo Causa-Efeito é que esse critério exercita combinações de dados de
teste que, possivelmente, não seriam considerados. Além disso, os resultados esperados são
produzidos como parte do processo de criação do teste, ou seja, eles fazem parte da própria
tabela de decisão. As dificuldades do critério estão na complexidade em se desenvolver o
grafo booleano, caso o número de causas e efeitos seja muito grande, e também na conversão
do grafo na tabela de decisão, embora esse processo seja algorítmico e existam ferramentas
de auxílio. Uma possível solução para isso é tentar identificar subproblemas e desenvolver
subgrafos Causa-Efeito para eles. A eficiência desse critério, assim como os outros critérios
funcionais, depende da qualidade da especificação, para que sejam identificadas as causas e
os efeitos e, também, da habilidade e experiência do testador. Uma especificação muito deta-
lhada pode levar a um grande número de causas e efeitos e, por outro lado, uma especificação
muito abstrata pode não gerar dados de teste significativos.
i i
i i
i i
Error Guessing
Essa técnica corresponde a uma abordagem ad-hoc na qual a pessoa pratica, inconsciente-
mente, uma técnica para projeto de casos de teste, supondo por intuição e experiência alguns
tipos prováveis de erros e, a partir disso, definem-se casos de teste que poderiam detectá-
los [294]. A idéia do critério é enumerar possíveis erros ou situações propensas a erros e
então definir casos de teste para explorá-las. Por exemplo, se o que está sendo testado é um
módulo de ordenação, as seguintes situações poderiam ser exploradas: i) uma lista de entrada
vazia; ii) uma lista com apenas uma entrada; iii) todas as entradas com o mesmo valor; e iv)
uma lista de entrada já ordenada.
i i
i i
i i
adequados a outro critério funcional, de forma cumulativa. Na Tabela 2.6 apresentam-se esses
dados. Foi gerado um total de 863 mutantes, dos quais 66 são equivalentes. Recomenda-se
que, após a leitura do Capítulo 5, o leitor volte a avaliar esses resultados.
i i
i i
Capítulo 3
3.1 Introdução
Uma especificação é um documento que representa o comportamento e as características que
um sistema deve possuir. Ela pode ser definida de diversas formas. Por exemplo, pode-se
criar um documento que descreve textualmente o comportamento em uma linguagem natural
(português, inglês, etc.). Um risco potencial com a descrição textual é a possibilidade de se
incluírem inconsistências na especificação, uma vez que as linguagens naturais geralmente
são ambíguas e imprecisas. O uso de outras formas de especificação torna-se importante em
contextos nos quais tais imprecisões podem causar problemas.
É importante notar que o uso de linguagem natural não é, por si só, um problema. Afi-
nal, o que é necessário é a precisão e o rigor da especificação. Por exemplo, uma descrição
textual feita de forma rigorosa e clara pode ser mais fácil de se compreender e implementar
do que uma especificação formal complexa. A especificação formal só é útil se for possível,
a partir dela, aumentar a compreensão do sistema, aliando-se a possibilidade de analisar al-
goritmicamente a especificação. Por algoritmicamente, entenda-se que se trata de uma forma
sistemática de verificar as propriedades da especificação.
A modelagem permite que o conhecimento sobre o sistema seja capturado e reutilizado
durante diversas fases de desenvolvimento. Boa parte da atividade de teste é gasta buscando-
se identificar o que o sistema deveria fazer. Ou seja, antes de se perguntar se o resultado está
correto, deve-se saber qual seria o resultado correto. Um modelo é muito importante nessa
tarefa, pois, se bem desenvolvido, captura o que é essencial no sistema. No caso de modelos
que oferecem a possibilidade de execução (ou simulação, como é muitas vezes o caso), o
modelo pode ser utilizado como um oráculo, definindo a linha que separa o comportamento
adequado do comportamento errôneo. Contudo, utilizar o modelo como oráculo coloca a
seguinte questão: como saber que o modelo está correto? Ou seja, o modelo torna-se um
artefato que deve ser testado tanto quanto o próprio sistema. Uma vez verificado o problema
de o modelo estar ou não adequado (ou, ao menos, aumentada a confiança de que ele esteja),
ele é extremamente valioso para o teste, servindo tanto como oráculo quanto como base para
a geração de scripts e cenários de teste.
28 Introdução ao Teste de Software ELSEVIER
Durante a realização dos testes, um grande obstáculo é determinar exatamente quais são
os objetivos de teste, ou seja, quais itens serão testados e como. Em geral, especificações
não rigorosas deixam grande margem a opiniões e especulações. A forma da especificação
pode variar de um grafo de fluxos de chamadas intermódulos a um guia de usuário. Uma
especificação clara é importante para definir o escopo do trabalho de desenvolvimento e,
em especial, o de teste. Se a única definição precisa de o que o sistema deve fazer é o
próprio sistema, os testes podem ser improdutivos. Considere um exemplo apresentado por
Apfelbaum [15], no qual a seguinte frase é apresentada em um documento de requisitos: “Se
um dígito inválido é fornecido, ele deve ser tratado da maneira adequada.” O que vem a
ser uma ‘maneira adequada’ não é definida. Um desenvolvedor pode julgar que a maneira
adequada é permitir que o usuário tente digitar novamente. Outro desenvolvedor pode julgar
que o mais adequado é abortar o comando. Qualquer uma das duas implementações são
plausíveis, mas apenas uma delas deve ser utilizada. O testador fica em uma posição na qual
recusa tudo que não está de acordo com o próprio julgamento, ou aceita tudo o que pode estar
correto no julgamento de alguém. O leitor pode argumentar que a situação é artificial, mas,
ainda assim, esse tipo de sentença em especificações não é incomum.
A criação de modelos não é uma nova habilidade que deve ser adquirida. Os testadores
sempre constroem um modelo, ainda que informal. (De outra forma, não haveria como deter-
minar se o comportamento foi aceitável.) O ponto é qual é o formato do modelo. Um modelo
informal ou que está apenas na cabeça do testador dificulta a automatização dos testes. Para
escrever um script de teste ou um plano de teste, o testador deve entender os passos básicos
necessários para usar o sistema.
As seqüências de ações que podem ocorrer durante o uso do sistema são definidas. Em
geral, existe mais de uma opção de “próximas ações” que podem ocorrer em um ponto espe-
cífico do processo. Algumas técnicas permitem descrever quais são as próximas ações em um
grafo nos quais os nós representam as configurações (ou estados) e as arestas representam as
transições entre as configurações. Essas técnicas são coletivamente denominadas “Máquinas
de Transições de Estados”. Algumas técnicas permitem que os modelos sejam decompos-
tos hierarquicamente, fazendo com que comportamentos complexos sejam decompostos em
comportamentos de mais baixo nível e mais simples. Capacidades adicionais incluem o uso
de variáveis e condicionais, fazendo com que as transições dependam de variáveis ou do
contexto atual do sistema.
Existem diversas técnicas baseadas em Máquinas de Transições de Estados. Elas diferem
entre si em características relativas à forma como certos elementos são explícita ou implici-
tamente representados. O modelo mais simples são as Máquinas de Estados Finitos (MEFs),
que serão usadas como base para a apresentação dos conceitos deste capítulo. Em uma MEF,
as configurações e as transições são representadas explicitamente. Entretanto, para sistemas
que possuem um grande número de possíveis configurações distintas, uma MEF talvez não
seja adequada, pois o número de estados pode ser excessivamente grande. Outras técnicas
foram propostas para resolver esse problema (conhecido como problema da explosão de esta-
dos). Por exemplo, uma MEF estendida adiciona às MEFs tradicionais conceitos de variáveis
de contexto e transições parametrizadas. Uma configuração particular do sistema não é mais
explicitamente representada no modelo. Em vez disso, uma configuração é composta pelo
estado atual da MEF e os valores das variáveis de contexto.
O propósito deste capítulo é apresentar algumas técnicas para a condução de testes ba-
seados em modelos. O capítulo será centrado em Máquinas de Transições de Estados e apre-
3.2. Máquinas de Estados Finitos 29
sentará algumas técnicas desenvolvidas para se gerarem casos de teste a partir de um modelo
formal. Tradicionalmente, essas técnicas são utilizadas no teste de protocolos. Contudo, elas
podem ser utilizadas em outros contextos, fazendo-se a devida avaliação de custo/benefício.
Por exemplo, a especificação do comportamento esperado de um sistema pode ser conside-
rada um protocolo que, se a implementação estiver em conformidade, permitirá ao sistema
ser utilizado corretamente e ao usuário se comunicar adequadamente com o sistema.
Este capítulo está organizado da seguinte forma. Na Seção 3.2, são apresentados os prin-
cipais conceitos das MEFs, assim como suas propriedades. Na Seção 3.3, definem-se as
principais seqüências básicas utilizadas pelos métodos de geração de seqüências de teste. Na
Seção 3.4, apresentam-se alguns métodos de geração de seqüências de teste, buscando ilus-
trar as diferenças entre eles e enfatizando o papel das seqüências básicas. Na Seção 3.5, os
métodos apresentados são comparados, permitindo identificar o custo/benefício de cada um.
Por fim, na Seção 3.6, são feitas as considerações finais deste capítulo.
• s0 ∈ S é o estado inicial;
• fz : (S × X) → Z é a função de saída;
• fs : (S × X) → S é a função de próximo estado.
A Figura 3.1 apresenta um exemplo, extraído de [79], de uma Máquina de Estados Fi-
nitos que especifica o comportamento de um extrator de comentários (Comment Printer).
O conjunto de entrada consiste em uma seqüência composta pelos símbolos ‘*’, ‘/’ e ‘v’,
sendo que ‘v’ representa qualquer caractere diferente de ‘*’ e ‘/’. A entrada é uma cadeia de
30 Introdução ao Teste de Software ELSEVIER
XXX
XXXEntrada * / v * / v
Estado XXX
X
1 ignore ignore ignore 1 2 1
2 empty-bf ignore ignore 3 2 1
3 acc-bf acc-bf acc-bf 4 3 3
4 acc-bf deacc-bf; print acc-bf 4 1 3
Figura 3.1 – Máquina de Estados Finitos: diagrama e tabela de transição de estados [79].
caracteres e apenas os comentários são impressos (usando a sintaxe da linguagem C). Um co-
mentário é qualquer cadeia de caracteres entre ‘/*’ e ‘*/’. As operações usadas no exemplo
são apresentadas na Tabela 3.1.
Uma vez que, pela própria definição, o conjunto de estados atingíveis em uma Máquina
de Estados Finitos é finito, é possível responder a quase todas as questões sobre o modelo,
o que permite a automatização do processo de validação. No entanto, a classe de sistemas
que podem ser modelados por uma máquina de estados finitos é consideravelmente limitada.
Mesmo para os casos nos quais a modelagem é possível, o modelo resultante pode ser exces-
sivamente grande. Por exemplo, a impossibilidade da representação explícita da concorrência
leva à explosão combinatória do número de estados. Com o propósito de aumentar o poder
de modelagem, foram propostas várias extensões às Máquinas de Estados Finitos, tais como
Statecharts [163] e Estelle [250, 52].
Para o restante de capítulo, é importante definir algumas propriedades que uma MEF pode
satisfazer. Essas propriedades são apresentadas a seguir.
Definição 2. Diz-se que a MEF é completamente especificada se ela trata todas a entradas
em todos os estados. Caso contrário, ela é parcialmente especificada. Para máquinas par-
cialmente especificadas o teste de conformidade é fraco. Caso contrário, ele é forte. Quando
3.3. Seqüências básicas 31
as transições não especificadas de uma MEF são completadas com fins de realização dos
testes, diz-se que assume uma suposição de completude.
Definição 3. Uma MEF é fortemente conectada se para cada par de estados (si , sj ) existe
um caminho por transições que vai de si a sj . Ela é inicialmente conectada se a partir do
estado inicial é possível atingir todos os demais estados da MEF.
Definição 4. Uma MEF é minimal (ou reduzida) se na MEF não existem quaisquer dois esta-
dos equivalentes. Dois estados são equivalentes se possuem as mesmas entradas e produzem
as mesmas saídas.
Definição 5. Uma MEF é determinística quando, para qualquer estado e para uma dada
entrada, a MEF permite uma única transição para um próximo estado. Caso contrário, ela
é não-determinística.
Uma seqüência de sincronização para um estado s (expressa por SS(s)) é uma seqüência de
símbolos de entrada que leva a MEF ao estado s, independentemente do estado original da
MEF. Por exemplo, considerando a MEF da Figura 3.1, uma seqüência de sincronização para
o estado 1 é ∗, ∗, /, v. Para verificar isso, observe que, não importa qual estado seja esco-
lhido como origem, a aplicação dessa seqüência leva sempre ao estado 1, como apresentado
na Tabela 3.2. Observe que a tabela indica os estados que são atingidos após a aplicação de
cada entrada da seqüência.
A seqüência de sincronização é útil em situações nas quais se deseja garantir que a MEF
vá para um estado em particular. Por exemplo, uma seqüência de sincronização pode ser uti-
lizada para posicionar a MEF no estado inicial sem a necessidade de uma operação de reset.
Contudo, deve-se ter em mente que a seqüência de sincronização da MEF não é necessaria-
mente uma seqüência de sincronização para a implementação, uma vez que a implementação
pode conter falhas.
32 Introdução ao Teste de Software ELSEVIER
O cálculo das seqüências de sincronização pode ser feito com o conceito de conjunto de
estados que representa um dos possíveis estados em que a MEF pode estar. Por exemplo,
um conjunto {3, 4} indica que a MEF está no estado 3 ou no estado 4. Pode-se definir uma
função fs : PS × X → PS, que, dados um conjunto de estados e uma entrada, calcula o
conjunto de estados resultante, da seguinte forma:
Assim, a seqüência ∗, ∗, /, v é uma seqüência de sincronização para 4 porque fs ({1, 2, 3, 4},
∗, ∗, /, v) = {4}.
As seqüências de sincronização podem ser calculadas por meio da construção de uma
árvore especial, denominada árvore de sincronização. Em uma árvore de sincronização cada
nó representa um conjunto de estados. A raiz da árvore é o conjunto de estados formado
por todos os estados S. Para cada símbolo de entrada x, um nó que representa o conjunto
de estados A terá um nó filho que representa o conjunto de estados B = fs (A, x). Uma
seqüência de sincronização para um estado s é formada pelos símbolos de entrada no caminho
que vai da raiz a um nó com um conjunto de estados {s}. A Figura 3.2 apresenta a árvore de
sincronização para a MEF da Figura 3.1. Pode-se verificar, na árvore de sincronização, como
a seqüência de sincronização para o estado 1 foi calculada.
Um seqüência de distinção (DS) é uma seqüência de símbolos de entrada que, quando apli-
cada aos estados da MEF, produzem saídas distintas para cada um dos estados. Ou seja,
observando-se a saída produzida pela MEF como resposta à DS, pode-se determinar em qual
estado a MEF estava originalmente.
3.3. Seqüências básicas 33
Para a MEF da Figura 3.1, que foi utilizada até aqui para ilustrar os conceitos apresen-
tados, não existe uma seqüência de distinção. Será utilizado outro exemplo para ilustrar os
conceitos e apresentar o algoritmo para o cálculo de uma seqüência de distinção. Considere a
MEF da Figura 3.3. Uma DS para essa MEF é a seqüência 0, 0, 1, como pode ser verificado
pela Tabela 3.3. Cada estado é representado por uma linha e cada coluna representa uma
entrada da seqüência de distinção. As células representam as saídas produzidas. Observe que
cada um dos estados produz uma saída distinta para essa seqüência. Por exemplo, se a saída
for 1, 0, 1, pode-se ter certeza de que o estado original era 3 (apesar de a MEF não estar
mais nesse estado ao final da aplicação da DS). Obviamente, qualquer seqüência que tenha
uma DS como prefixo também é uma DS (já que todos os estados foram distintos, posso
concatenar qualquer seqüência de símbolos de entrada, e a seqüência continuará fazendo a
distinção). Uma DS é própria se nenhum de seus prefixos é também uma DS. Em geral,
tem-se interesse em uma DS própria.
Tabela 3.3 – Saídas geradas pela MEF da Figura 3.3 em resposta à seqüência de distin-
ção 0, 0, 1
0 0 1
1 0 0 0
2 0 1 0
3 1 0 1
4 0 0 1
5 0 1 1
6 1 0 0
Como pôde ser observado com a MEF da Figura 3.1, uma DS pode não existir. Isto é,
não existe uma seqüência capaz de distinguir todos os estados. Existem outras seqüências
34 Introdução ao Teste de Software ELSEVIER
que podem ser utilizadas nesse caso, como as UIO e o conjunto W, apresentados nas seções
seguintes.
Para o cálculo de uma DS, pode-se utilizar o conceito de grupos de incerteza. Um grupo
de incerteza é um conjunto de estados que ainda não foram distinguidos e para os quais
ainda é necessário que alguma entrada seja anexada à seqüência para ela se tornar uma DS.
Por exemplo, um grupo [{2, 3, 5, 6}, {2, 4}] indica que os estados 2, 3, 5 e 6 precisam ser
distinguidos, assim como os estados 2 e 4. Um grupo é trivial se todos os conjuntos de
estados são unitários ou vazios.
Um grupo g pode ser particionado em relação a um símbolo de entrada x, criando um
grupo g no qual todos os conjuntos de estados são subconjuntos dos conjuntos em g. No
particionamento, são mantidos juntos os estados que produzem a mesma saída em relação a
x. Por exemplo, o grupo [{2, 3, 5, 6}, {2, 4}] pode ser particionado em relação a 0, criando o
grupo [{2, 5}, {3, 6}, {2, 4}].
Após ser particionado considerando-se a saída, devem-se calcular os próximos estados
correspondentes ao grupo obtido em relação à entrada em questão (uma vez que, após a
aplicação da entrada, a MEF estará no respectivo próximo estado). Por exemplo, após
ser particionado e atualizado em relação a 0, o grupo [{2, 3, 5, 6}, {2, 4}] gerará o grupo
[{3, 6}, {4, 2}, {5, 3}]. Ao se atualizar um grupo, pode ocorrer de o grupo ficar inconsistente.
Um grupo é inconsistente caso, ao calcular os próximos estados, se perceba que dois estados
que ainda não foram distinguidos levam ao mesmo estado (e, portanto, não poderão ser mais
distinguidos!). É possível verificar que, ao se particionar o grupo [{2, 3, 5, 6}, {2, 4}] em re-
lação a 1, se obtém o grupo [{2, 5, 6}, {3}, {4, 2}], que é inconsistente, pois tanto o estado 2
quanto o estado 6 levam ao estado 5 quando a entrada 1 é aplicada.
Agora, de posse das operações de particionamento e atualização, pode-se construir uma
árvore de distinção. Em uma árvore de distinção cada nó é formado por um grupo de incer-
teza. O nó raiz é formado pelo grupo no qual todos os estados ainda não foram distinguidos
(ou seja, [S]). Cada nó possui um filho para cada símbolo de entrada. O grupo do nó filho
é calculado a partir do grupo de nó pai, particionando-o e, caso o grupo resultante não seja
inconsistente, atualizando-o. A árvore é construída por nível a partir da raiz. Um nó é folha
(i) se for inconsistente ou (ii) se for trivial. Uma DS é a seqüência de símbolos de entrada
presentes no caminho que vai da raiz da árvore a um nó cujo grupo seja trivial.
A Figura 3.4 mostra a árvore de distinção para a MEF da Figura 3.3. Pode-se observar
como os grupos são particionados e atualizados. Note que os conjuntos unitários são des-
cartados da representação porque eles não têm influência alguma sobre o cálculo do DS. O
mesmo é feito com os conjuntos vazios. Os grupos inconsistentes são marcados com retân-
gulos. Os grupos triviais são marcados com elipses. Observe que a MEF em questão possui
outras três DS próprias (no caso, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1).
Como pôde ser observado na seção anterior, nem todas as MEFs possuem DSs. Entretanto,
mesmo nessas situações, pode-se fazer a distinção entre um estado e os demais. Para isso,
usa-se uma Seqüência Única de Entrada e Saída (UIO). Uma UIO é utilizada para verificar se
a MEF está em um estado particular. Assim, para cada estado da MEF, pode-se ter uma UIO
distinta. Isso é verdade apenas se forem consideradas tanto as entradas como as saídas. A
3.3. Seqüências básicas 35
seqüência de entradas da UIO de um estado pode ser igual à seqüência de entradas da UIO de
outro estado. Assim, pode-se concluir que se uma MEF possui uma DS, ela também possui
UIOs para todos os seus estados. Contudo, em geral, pode-se obter UIOs mais curtas.
Considere a MEF da Figura 3.3. A seqüência 1/0, 1/0 é uma UIO para o estado 4
(anotaremos a saída logo após a entrada, separada por um ’/’). Isso pode ser comprovado
pelas saídas produzidas por todos os estados apresentada na Tabela 3.4. A UIO(4) deve ser
usada como a seguir. Aplica-se a seqüência de entrada 1, 1. Se a seqüência de saída for
0, 0, então pode-se garantir que o estado original era o estado 4. No entanto, diferentemente
da DS, nem sempre é possível dizer qual era o estado se a saída não for a esperada. Por
exemplo, se a saída for 0, 1, não há como saber se o estado original era o estado 1 ou o
estado 3. Pode-se notar também que a 1, 1 também pode formar uma UIO para o estado 5
(que, nesse caso, seria a seqüência 1/1, 1/0).
Tabela 3.5 – Saída gerada para cada estado pelas seqüências do conjunto de caracterização
0 1 1
1 0 0 1
2 0 1 1
3 1 0 1
4 0 0 0
5 0 1 0
6 1 1 1
da saída gerada pela implementação com a saída especificada na seqüência de teste. Relações
formais de equivalência entre especificações e implementação foram trabalhadas em [398],
mas não serão discutidas aqui. Os métodos mais clássicos para a geração de seqüências de
teste a partir de MEFs são:
Método TT (Transition Tour) Para uma dada MEF, o transition tour é uma seqüência que
parte do estado inicial atravessa todas as transições pelo menos uma vez e retorna ao
estado inicial. Permite a detecção de erros de saída, mas não garante erros de transfe-
rência, ou seja, erros em que a transição leva a MEF a um estado diferente do qual ela
deveria levar.
Método UIO (Unique input/output) Produz uma seqüência de identificação de estado. Não
garante cobertura total dos erros, pois a seqüência de entrada leva a uma única saída na
especificação correta, mas isso não é garantido para as implementações com erro.
Outros métodos como Método Wp, UIO-v, método-E, etc. são discutidos em (Lai, 2002).
A seguir, são apresentados os métodos TT, UIO, DS e W.
3.4.1 Método TT
O método TT é relativamente simples, quando comparado com os outros três métodos. Ele
gera um conjunto de seqüências que passam por todas as transições da MEF, ou seja, as
seqüências geradas cobrem todas as transições.
Em alguns casos, o conjunto de seqüências é unitário, isto é, uma única seqüência passa
por todas as transições. Contudo, para algumas MEFs, pode não ser possível calcular uma
única seqüência. Por exemplo, a MEF pode possuir dois conjuntos de estados tais que,
uma vez entrando em um deles, não se consegue chegar a um estado do outro conjunto, o
que pode ser considerado um caso particular de MEF não completamente conexa. Nesse
caso, pode-se utilizar um conjunto com mais seqüências.
O algoritmo para calcular as seqüências do método TT depende das propriedades que a
MEF satisfaz. Em geral, algoritmos para grafos podem ser adaptados para esse propósito
[152].
3.4.2 Método DS
O método DS [159] utiliza a seqüência de distinção DS para gerar as seqüências de teste. São
utilizadas também as seqüências de sincronização. Assim, sua aplicabilidade fica condicio-
nada à existência dessas seqüências para a MEF em questão.
3.4. Geração de seqüências de teste 39
Para aplicar o método DS, deve-se primeiramente selecionar uma seqüência DS. Como
essa seqüência é utilizada diversas vezes ao longo do método, é importante que se selecione
a menor seqüência. Chamaremos essa seqüência DS de Xd . Por exemplo, para a MEF da
Figura 3.3, a seqüência Xd é 0, 0, 1.
Em seguida, gera-se um grafo, denominado grafo Xd , no qual cada nó representa um
estado da MEF. Para cada estado si , existe no grafo um estado correspondente e existe uma
aresta que liga si ao estado resultante da aplicação de Xd a si . O exemplo do grafo Xd para a
MEF da Figura 3.3 e a seqüência Xd definida anteriormente são apresentados na Figura 3.5.
Gera-se a seqüência de verificação, a qual é formada por duas subseqüências, que são:
para verificar se o estado atingido foi realmente o estado 5. O novo estado inicial só pode ser
o estado 6 (que é o único estado origem restante). Deve-se aplicar uma seqüência que leve
a MEF do estado 5 ao estado 6. Nesse caso, a seqüência 0 pode ser utilizada. Uma vez
estando no estado 6, repete-se o procedimento anterior. A seqüência-α obtida é:
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1
Uma redução do grafo-β e, conseqüentemente, das seqüências usadas para cobertura pode
ser feita, considerando-se que a seqüência-α já foi aplicada. O que se pode observar é que,
na seqüência-α, todos os estados já foram verificados. Assim, a última transição da aplicação
de cada Xd já foi verificada. Por exemplo, aplicando a seqüência Xd (0, 0, 1) no estado 2, a
MEF vai para o estado 1, passando pelos estados 3 e 4. Se a transição que vai de 2 a 3 com a
entrada 0 e a transição que vai de 3 a 4 com entrada 0 tiverem sido verificadas, não há necessi-
dade de se verificar a transição que vai de 4 a 1 com entrada 1, visto que essa verificação já foi
feita pela seqüência-α (caso contrário, se outro estado houvesse sido atingido, a seqüência-α
3.4. Geração de seqüências de teste 41
teria identificado). Assim, podemos descartar do grafo-β todas as arestas que correspondem
ao último passo da aplicação do Xd . Observe na Tabela 3.6, passo a passo, a aplicação da
seqüência Xd a cada um dos estados da MEF. As transições representadas pela aplicação da
última entrada da seqüência Xd na penúltima coluna da tabela não precisam ser verificadas.
Assim, as arestas partindo dos estados 2, 3, 4, 5 e 6 com a entrada 1 podem ser retiradas do
grafo-β. Do mesmo modo, também pode ser retirada a última transição da aplicação de uma
seqüência incluída para ligar as componentes desconexas da geração da seqüência-α. Nesse
caso, a seqüência 0, por exemplo, foi aplicada para levar a MEF do estado 5 ao estado 6.
Assim, a aresta partindo do estado 5 com a entrada 0 pode ser retirada do grafo-β. O grafo
resultante é apresentado na Figura 3.7.
Agora, pode-se calcular a seqüência-β obtendo-se uma cobertura mínima das arestas do
grafo-β. Partindo-se do estado 1, a seqüência 0Xd , 1Xd , 0Xd , 0Xd cobre quatro arestas,
e pára no estado 5. Para cobrir as demais arestas, é necessário incluir uma seqüência que
leva a outro estado que é origem de alguma aresta não coberta. Nesse caso, pode-se utilizar a
seqüência 0 que leva ao estado 6. Então, cobre-se essa aresta com 0Xd , parando no estado
42 Introdução ao Teste de Software ELSEVIER
1. Utiliza-se a seqüência 0, 0 para levar a MEF ao estado 3 (que ainda possui uma aresta
não coberta). Com a seqüência 0Xd , cobre-se a última aresta. A seqüência-β, portanto, é
0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1 0, 0, 0, 0, 1 0, 0, 0, 0, 0, 1
3.4.4 Método W
/ ∗
∗/ ∗∗
v/ v∗
// /∗
/ ∗ / / ∗ ∗
/v/ /v∗
/// //∗
/ ∗ ∗/ / ∗ ∗∗
/ ∗ v/ / ∗ v∗
/ ∗ // / ∗ /∗
/ ∗ ∗ ∗ / / ∗ ∗ ∗ ∗
/ ∗ ∗v/ / ∗ ∗v∗
/ ∗ ∗// / ∗ ∗/∗
De acordo com Sidhu [357], o principal fato que contribui para o comprimento das
seqüências de teste reside na escolha das seqüências de identificação. Assim, se o com-
primento de uma seqüência de distinção é maior que o das seqüências UIO, então o método
DS gerará subseqüências mais longas que o método UIO. Discussões semelhantes podem ser
feitas para os demais métodos. Ainda de acordo com Sidhu [357], por meio de experimenta-
ção, verificou-se que o método UIO produz as seqüências de teste mais curtas que os métodos
DS e W, ao passo que o método W produz as seqüências mais longas.
Teste Estrutural
4.1 Introdução
Conforme discutido nos capítulos anteriores, técnicas e critérios de teste fornecem ao proje-
tista de software uma abordagem sistemática e teoricamente fundamentada para a condução
da atividade de teste. Além disso, constituem um mecanismo que pode auxiliar na garantia
da qualidade dos testes e na maior probabilidade em revelar defeitos no software. Várias
técnicas podem ser adotadas para se conduzir e avaliar a qualidade da atividade de teste,
sendo diferenciadas de acordo com a origem das informações utilizadas para estabelecer os
requisitos de teste [102].
A técnica estrutural (ou caixa branca) estabelece os requisitos de teste com base em uma
dada implementação, requerendo a execução de partes ou de componentes elementares do
programa [294, 329]. Os caminhos lógicos do software são testados, fornecendo-se casos
de teste que põem à prova tanto conjuntos específicos de condições e/ou laços bem como
pares de definições e usos de variáveis. Os critérios pertencentes à técnica estrutural são
classificados com base na complexidade, no fluxo de controle e no fluxo de dados [261, 329,
429], e serão discutidos mais detalhadamente no decorrer deste capítulo.
A técnica estrutural apresenta uma série de limitações e desvantagens decorrentes das
limitações inerentes à atividade de teste de programa como estratégia de validação [141,
191, 302, 337]. Tais aspectos introduzem sérios problemas na automatização do processo de
validação de software [261]:
• não existe um procedimento de teste de propósito geral que possa ser usado para provar
a correção de um programa;
• dados dois programas, é indecidível se eles computam a mesma função;
48 Introdução ao Teste de Software ELSEVIER
4.2 Histórico
Os primeiros critérios estruturais para o teste de programas eram baseados essencialmente
no fluxo de controle dos programas. O critério de McCabe [276], utilizando uma medida de
complexidade de software baseada na representação de fluxo de controle de um programa
– a complexidade ciclomática –, foi um dos primeiros critérios estruturais definidos. Já os
critérios Todos-Nós, Todas-Arestas e Todos-Caminhos são os mais conhecidos da classe de
critérios baseados em fluxo de controle e exigem, respectivamente, que cada comando, cada
desvio e cada caminho do programa em teste seja executado pelo menos uma vez [294, 329].
De modo geral, o teste baseado unicamente nos critérios Todos-Nós e Todas-Arestas tem
se mostrado pouco eficaz para revelar a presença de defeitos simples e triviais, mesmo para
programas pequenos. Por outro lado, apesar de desejável, executar todos os caminhos de um
programa é, na maioria das vezes, uma tarefa impraticável. De fato, para muitos programas
1 Esta pode ser considerada uma limitação fundamental para qualquer estratégia de teste.
4.2. Histórico 49
diferentes contextos, extensões dos critérios estruturais também têm sido propostas para o
teste de programas OO e de componentes, e para o teste de aspectos. Tais extensões serão
discutidas em detalhes nos Capítulos 6 e 7.
• uma vez que o primeiro comando do bloco é executado, todos os demais são executados
seqüencialmente;
• não existe desvio de execução para nenhum comando dentro do bloco.
A partir do GFC podem ser escolhidos os elementos que devem ser executados, caracte-
rizando assim o teste estrutural.
Seja um GFC G = (N, E, s) em que N representa o conjunto de nós, E o conjunto de
arcos, e s o nó de entrada. Um “caminho” é uma seqüência finita de nós (n1 , n2 , . . . , nk ),
k ≥ 2, tal que existe um arco de ni para ni + 1 para i = 1, 2, . . . , k − 1. Um caminho é
um “caminho simples” se todos os nós que compõem esse caminho, exceto possivelmente o
primeiro e o último, são distintos; se todos os nós são distintos, diz-se que esse caminho é
um “caminho livre de laço”. Um “caminho completo” é aquele em que o primeiro nó é o nó
de entrada e o último nó é um nó de saída do grafo G.
Seja IN (x) e OU T (x) o número de arcos que entram e que saem do nó x, respectiva-
mente. Assumimos IN (s) = 0, tal que s é o nó de entrada, e OU T (o) = 0, tal que o é o nó
de saída.
52 Introdução ao Teste de Software ELSEVIER
As ocorrências de uma variável em um programa podem ser uma “definição”, uma “in-
definição” ou um “uso”. Usualmente, os tipos de ocorrências de variáveis são definidos por
um modelo de fluxo de dados.
Conforme o modelo de fluxo de dados definido por Maldonado [261], uma definição de
variável ocorre quando um valor é armazenado em uma posição de memória. Em geral, em
um programa, uma ocorrência de variável é uma definição se ela está: (i) no lado esquerdo
de um comando de atribuição; (ii) em um comando de entrada; ou (iii) em chamadas de pro-
cedimentos como parâmetro de saída. A passagem de valores entre procedimentos por meio
de parâmetros pode ser por valor, referência ou por nome [150]. Se a variável for passada por
referência ou por nome, considera-se que seja um parâmetro de saída. As definições decor-
rentes de possíveis definições em chamadas de procedimentos são diferenciadas das demais
e são ditas definidas por referência. Por outro lado, diz-se que uma variável está indefinida
quando não se tem acesso ao seu valor ou sua localização deixa de estar definida na memória.
A ocorrência de uma variável é um uso quando a referência a essa variável não a estiver
definindo. Dois tipos de usos são caracterizados: “c-uso” e “p-uso”. O primeiro tipo afeta
diretamente uma computação realizada ou permite que o resultado de uma definição ante-
rior possa ser observado; o segundo tipo afeta diretamente o fluxo de controle do programa.
Observa-se que enquanto c-usos estão associados aos nós do GFC, p-usos estão associados a
seus arcos.
Considere uma variável x definida em um nó i, com uso em um nó j ou em um arco que
chega em j. Um caminho (i, n1 , . . . , nm , j), m ≥ 0 que não contenha definição de x nos nós
n1 , . . . , nm , é chamado de “caminho livre de definição” c.r.a x do nó i ao nó j e do nó i ao
arco (nm , j).
Um nó i possui uma “definição global” de uma variável x se ocorre uma definição de x
no nó i e existe um caminho livre de definição de i para algum nó ou para algum arco que
contém um c-uso ou um p-uso, respectivamente, da variável x. Um c-uso da variável x em
um nó j é um “c-uso global” se não existir uma definição de x no nó j precedendo este c-uso;
caso contrário, é um “c-uso local”.
A título de ilustração, considere o programa identifier (Programa 4.1). O programa
é responsável por determinar se um identificador é válido ou não – um identificador válido
deve começar com uma letra e conter apenas letras ou dígitos; além disso, deve ter no mí-
nimo 1 e no máximo 6 caracteres de comprimento. É importante observar que o programa
identifier contém um defeito.
Programa 4.1
1 main ()
2 /* 1 */ {
3 /* 1 */ char achar;
4 /* 1 */ int length, valid_id;
5 /* 1 */ length = 0;
6 /* 1 */ valid_id = 1;
7 /* 1 */ printf ("Identificador: ");
8 /* 1 */ achar = fgetc (stdin);
9 /* 1 */ valid_id = valid_s(achar);
10 /* 1 */ if(valid_id)
11 /* 2 */ {
12 /* 2 */ length = 1;
13 /* 2 */ }
14 /* 3 */ achar = fgetc (stdin);
4.3. Definições e conceitos básicos 53
15 /* 4 */ while(achar != ’\n’)
16 /* 5 */ {
17 /* 5 */ if(!(valid_f(achar)))
18 /* 6 */ {
19 /* 6 */ valid_id = 0;
20 /* 6 */ }
21 /* 7 */ length++;
22 /* 7 */ achar = fgetc (stdin);
23 /* 7 */ }
24 /* 8 */ if(valid_id && (length >= 1) && (length < 6))
25 /* 9 */ {
26 /* 9 */ printf ("Valido\n");
27 /* 9 */ }
28 /* 10 */ else
29 /* 10 */ {
30 /* 10 */ printf ("Invalid\n");
31 /* 10 */ }
32 /* 11 */ }
33
34 int valid_s(char ch)
35 /* 1 */ {
36 /* 1 */ if(((ch >= ’A’) && (ch <= ’Z’)) || ((ch >= ’a’) && (ch <= ’z’)))
37 /* 2 */ {
38 /* 2 */ return (1);
39 /* 2 */ }
40 /* 3 */ else
41 /* 3 */ {
42 /* 3 */ return (0);
43 /* 3 */ }
44 /* 4 */ }
45
46 int valid_f(char ch)
47 /* 1 */ {
48 /* 1 */ if(((ch >= ’A’) && (ch <= ’Z’)) || ((ch >= ’a’) && (ch <= ’z’)) ||
49 ((ch >= ’0’) && (ch <= ’9’)))
50 /* 2 */ {
51 /* 2 */ return (1);
52 /* 2 */ }
53 /* 3 */ else
54 /* 3 */ {
55 /* 3 */ return (0);
56 /* 3 */ }
57 /* 4 */ }
58
*
O primeiro bloco de comandos (nó) é caracterizado da linha 2 à linha 10. O segundo bloco
é formado pelas linhas de 11 a 13. O terceiro bloco refere-se à linha 14, e assim por diante.
Ao todo, 11 nós constituem o GFC referente à função main do programa identifier, 4
nós constituem o grafo referente à função valid_s e 4 nós constituem o grafo da função
valid_f. Na Figura 4.1 é ilustrado o grafo obtido referente à função main, gerado pela
ferramenta ViewGraph 3 [412].
O comando if(valid_id) (linha 10) ilustra um desvio de execução entre os nós do
programa. Caso sejam exercitados os comandos internos ao if, tem-se um desvio de execu-
ção do nó 1 para o nó 2, representado no grafo pelo arco (1,2). Do contrário, se os comandos
3 A ViewGraph é uma ferramenta para visualização de Grafos de Fluxo de Controle e informações de teste.
Figura 4.1 – Grafo de Fluxo de Controle do programa identifier gerado pela ViewGraph.
1. o número de regiões em um GFC. Uma região pode ser informalmente descrita como
uma área incluída no plano do grafo. O número de regiões é computado contando-se
todas as áreas delimitadas e a área não delimitada fora do grafo; ou
• Todos-Nós: exige que a execução do programa passe, ao menos uma vez, em cada
vértice do GFC; ou seja, que cada comando do programa seja executado pelo menos
uma vez;
• Todas-Arestas (ou Todos-Arcos): requer que cada aresta do grafo, isto é, cada desvio
de fluxo de controle do programa, seja exercitada pelo menos uma vez;
• Todos-Caminhos: requer que todos os caminhos possíveis do programa sejam execu-
tados.
Os critérios baseados em fluxo de dados utilizam a análise de fluxo de dados [176] como
fonte de informação para derivar os requisitos de teste. Uma característica comum aos cri-
térios dessa categoria é que eles requerem o teste das interações que envolvam definições de
variáveis e subseqüentes referências a essas definições [178, 231, 301, 337, 400]. Portanto,
para a derivação de casos de teste, tais critérios baseiam-se nas associações entre a definição
de uma variável e seus possíveis usos subseqüentes.
Uma motivação para a introdução dos critérios baseados em fluxo de dados foi a indi-
cação de que, mesmo para programas pequenos, o teste baseado unicamente no fluxo de
controle não era eficaz para revelar a presença mesmo de defeitos simples e triviais. Nesse
sentido, a introdução dessa classe de critérios procurou estabelecer uma hierarquia entre os
critérios Todas-Arestas e Todos-Caminhos, visando a tornar o teste estrutural mais rigoroso.
De acordo com Ural [400], critérios baseados em fluxo de dados são mais adequados para
certas classes de defeitos, tais como defeitos computacionais, uma vez que dependências de
dados são identificadas e, portanto, segmentos funcionais são requeridos como requisitos de
teste.
A seguir são apresentadas duas famílias de critérios baseados em fluxo de dados: a família
de critérios proposta por Rapps e Weyuker [336, 337] e a família de critérios Potenciais-Usos,
proposta por Maldonado [261].
Entre os critérios de fluxo de dados, destacam-se os critérios de Rapps e Weyuker [336, 337],
introduzidos nos anos 80. Para derivar os requisitos de teste exigidos por tais critérios, Rapps
e Weyuker propuseram alguns conceitos e definições. Um deles foi o “Grafo Def-Uso” (def-
use graph), o qual consiste em uma extensão do GFC. Nesse grafo são adicionadas infor-
mações a respeito do fluxo de dados do programa, caracterizando associações entre pontos
do programa nos quais é atribuído um valor a uma variável (definição da variável) e pontos
nos quais esse valor é utilizado (referência ou uso da variável). Os requisitos de teste são
determinados com base em tais associações.
O Grafo Def-Uso é obtido a partir do GFC associando-se a cada nó i os conjuntos
c-uso(i) = {variáveis com c-uso global no bloco i} e def(i) = {variáveis com definições
globais no bloco i}, e a cada arco (i, j) o conjunto p-uso(i, j) = {variáveis com p-usos no
arco (i, j)}. Dois conjuntos foram definidos: dcu(x, i) = {nós j, tal que x ∈ c-uso(j) e
existe um caminho livre de definição c.r.a x do nó i para o nó j} e dpu(x, i) = {arcos (j, k),
tal que x ∈ p-uso(j, k) e existe um caminho livre de definição c.r.a x do nó i para o arco
(j, k)}.
Adicionalmente, foram definidos os conceitos de “du-caminho”, “associação definição-
c-uso”, “associação definição-p-uso” e “associação”. Um caminho (n1 , n2 , ..., nj , nk ) é um
“du-caminho” c.r.a variável x se n1 tiver uma definição global de x e: (1) nk tem um c-uso de
x e (n1 , n2 , ..., nj , nk ) é um caminho simples livre de definição c.r.a x; ou (2) (nj , nk ) tem
um p-uso de x e (n1 , n2 , ..., nj , nk ) é um caminho livre de definição c.r.a x e n1 , n2 , ..., nj é
um caminho livre de laço.
58 Introdução ao Teste de Software ELSEVIER
• Todas-Definições: requer que cada definição de variável seja exercitada pelo menos
uma vez, não importa se por um c-uso ou por um p-uso;
• Todos-Usos: requer que todas as associações entre uma definição de variável e seus
subseqüentes usos (c-usos e p-usos) sejam exercitadas pelos casos de teste, por pelo
menos um caminho livre de definição, ou seja, um caminho em que a variável não é re-
definida. Os critérios Todos-p-Usos, Todos-p-Usos/Alguns-c-Usos e Todos-c-Usos/Al-
guns-p-Usos representam variações do critério Todos-Usos;
• Todos-Du-Caminhos: requer que toda associação entre uma definição de variável e
subseqüentes p-usos ou c-usos dessa variável seja exercitada por todos os caminhos
livres de definição e livres de laço que cubram essa associação.
Critérios Potenciais-Usos
Conforme será visto no Capítulo 10, a relação de inclusão e a complexidade dos critérios são
propriedades importantes associadas aos critérios de teste, sendo utilizadas para avaliá-los,
do ponto de vista teórico.
Em linhas gerais, a “complexidade de um critério C” é definida como o número máximo
de casos de teste requerido pelo critério no pior caso. Ou seja, dado um programa qualquer P ,
se existir um conjunto de casos de teste T que seja C-adequado para P , então a cardinalidade
de T é menor ou igual à complexidade do critério C.
Já a relação de inclusão estabelece uma ordem parcial entre os critérios, caracterizando
uma hierarquia entre eles. Dados dois critérios C1 e C2 , diz-se que C1 inclui C2 se, para
qualquer programa P , todo conjunto de teste C1 -adequado é também C2 -adequado. O crité-
rio C1 inclui estritamente o critério C2 , denotado por C1 ⇒ C2 , se C1 inclui C2 e C2 não
inclui C1 . Quando nem C1 inclui C2 nem C2 inclui C1 , diz-se que os critérios C1 e C2 são
incomparáveis [337].
A complexidade e a relação de inclusão refletem, em geral, as propriedades básicas que
devem ser consideradas no processo de definição de um critério de teste C, a saber [261]:
4.4. Critérios de teste estrutural 59
• “associação potencial-definição-p-uso” como a tripla i, (j, k), x, em que x ∈ defg(i)
e (j, k) ∈ pdpu(x, i); e
60 Introdução ao Teste de Software ELSEVIER
A partir dessas definições, é possível estabelecer os critérios básicos que fazem parte da
família de critérios Potenciais-Usos [261]:
Os critérios Potenciais-Usos são os únicos critérios baseados em fluxo de dados que sa-
tisfazem as três propriedades mínimas esperadas de um critério de teste C discutidas an-
teriormente; ou seja, estabelecem uma hierarquia entre os critérios Todas-Arestas e Todos-
Caminhos, mesmo na presença de caminhos não executáveis. Além disso, nenhum outro
critério de teste baseado em fluxo de dados inclui os critérios Potenciais-Usos. Outro ponto
importante é que, apesar de terem complexidade de ordem exponencial, o que poderia ser um
limitante para a aplicação efetiva desses critérios (assim como dos demais critérios baseados
em fluxo de dados), na prática tais critérios requerem um número pequeno de casos de teste.
Esses aspectos serão retomados e discutidos no Capítulo 10.
Para ilustrar a aplicação de alguns dos principais critérios estruturais serão utilizados o pro-
grama identifier (Programa 4.1) e o GFC correspondente (Figura 4.1), ambos apresen-
tados na Seção 4.3. Com relação ao programa identifier é importante observar que os
subcaminhos (1,3,4,8,9), (2,3,4,8,10) e (6,7,4,8,9) são não executáveis e, desse modo, quais-
quer caminhos completos que os incluam também serão considerados não executáveis.
Considere inicialmente o critério de McCabe, baseado na complexidade. De acordo com o
GFC, determina-se a complexidade ciclomática V (G) conforme uma das seguintes maneiras:
• V (G) = 5 regiões; ou
• V (G) = 14 arcos − 11 nós + 2 = 5; ou
• V (G) = 4 nós predicativos + 1 = 5.
1, (8, 10), length.4 Observe que a associação 1, (8, 9), length é não executável –
o único caminho livre de definição possível de exercitá-la seria um caminho que incluísse
o subcaminho (1,3,4,8,9). Já para a associação 1, 7, length qualquer caminho com-
pleto executável que inclua um dos subcaminhos (1,3,4,5,6,7) ou (1,3,4,5,7) é suficiente para
exercitá-la. Essa mesma análise deve ser feita para todas as demais variáveis e associações
pertinentes, a fim de satisfazer o critério. Na Tabela 4.5 é ilustrado o conjunto completo de
associações requeridas pelo critério Todos-Usos.
Finalmente, considere o critério Todos-Potenciais-Usos, pertencente à família de critérios
Potenciais-Usos [261]. Na Figura 4.3 é ilustrado o Grafo Def do programa identifier.
Utilizando-se o conceito de potencial-uso, tem-se que a variável length definida no nó
1 poderia ter um uso predicativo nos arcos (5, 6) e (5, 7), fazendo com que as potenciais-
associações 1, (5, 6), length e 1, (5, 7), length sejam requeridas pelo critério Todos-
Potenciais-Usos. É importante observar que as potenciais-associações 1, (5, 6), length e
1, (5, 7), length não seriam requeridas pelos demais critérios de fluxo de dados que não
fazem uso do conceito de potencial-uso. Note-se, ainda, que, conforme observado anterior-
4 As notações i,j,var e i,(j,k),var indicam que a variável var é definida no nó i e existe um uso computacional
de var no nó j ou um uso predicativo de var no arco (j, k), respectivamente, bem como pelo menos um caminho
livre de definição do nó i ao nó j ou ao arco (j, k).
64 Introdução ao Teste de Software ELSEVIER
4.5 Ferramentas
A aplicação de critérios de teste sem o apoio de ferramentas automatizadas tende a ser uma
atividade propensa a erros e limitada a programas muito simples. Tal aspecto motiva o de-
senvolvimento de ferramentas automáticas para auxiliar na condução de testes efetivos e na
análise dos resultados obtidos.
As ferramentas de apoio ao teste estrutural possibilitam, em sua maioria, a análise de co-
bertura de um conjunto de casos de teste segundo algum critério selecionado. Algumas delas
oferecem suporte à obtenção dos dados de entrada necessários para satisfazer um particular
requisito de teste. Em geral, tal suporte é baseado na execução simbólica do programa em
teste. Apesar de não auxiliarem diretamente na determinação das entradas de um programa,
66 Introdução ao Teste de Software ELSEVIER
necessárias para a execução de caminhos específicos, grande parte das ferramentas de teste
apresenta ao usuário os requisitos de teste exigidos para que os critérios sejam satisfeitos. As-
sim, de certa maneira, orientam e auxiliam o usuário na elaboração dos casos de teste [261].
É importante observar que a existência de caminhos não executáveis é um aspecto bas-
tante restritivo para a automatização dos critérios estruturais, assim como da atividade de
teste de um modo geral. Heurísticas têm sido introduzidas para lidar com esse aspecto [141].
As primeiras ferramentas comerciais de apoio ao teste estrutural foram a RXVP80 [118]
e a TCAT [341], as quais apoiavam a aplicação de critérios estruturais baseados somente
no Grafo de Fluxo de Controle. A ferramenta RXVP80, distribuída pela General Research
Corporation, é um sistema que realiza basicamente a análise de cobertura do teste de arcos
em programas escritos em Fortran. Além do suporte ao teste dinâmico, via instrumenta-
ção, essa ferramenta fornece ainda: análise estática do código-fonte com a geração do Grafo
de Chamada dos módulos (unidades) que compõem o sistema em teste; geração do GFC
dos módulos; verificação de anomalias no código-fonte; verificação de tipos nas chamadas
de procedimentos; e geração de alguns relatórios que fornecem referências cruzadas entre
código-fonte, variáveis e módulos.
A ferramenta TCAT (Test-Coverage Analysis Tool), distribuída pela Software Research
Corporation, realiza o teste de unidades segundo o critério Teste de Ramos Lógicos (arcos).
Esse tipo de teste de ramos divide os predicados encontrados em uma condição em vários
“if’s”, no qual cada “if” só contém um dos predicados. Essa ferramenta insere código adicio-
nal (além do de monitoração) para mapear os ramos lógicos.
A ferramenta SCORE [82] apóia o teste de arcos de programas escritos em Pascal, tendo
sido desenvolvida na Hitachi. Implementa duas medidas de cobertura distintas. A primeira
medida é baseada na forma tradicional que consiste no quociente entre o número de arcos
executados e o número total de arcos. A segunda medida baseia-se na aplicação do conceito
4.5. Ferramentas 67
ramenta JaBUTi (Java Bytecode Understanding and Testing) [422], por exemplo, apóia a
aplicação do teste estrutural intramétodo em programas e componentes Java. Basicamente,
a ferramenta fornece suporte a diferentes critérios de teste estruturais para a análise de co-
bertura, um conjunto de métricas estáticas para avaliar a complexidade das classes que com-
põem o programa/componente e, ainda, implementa algumas heurísticas de particionamento
de programas que buscam auxiliar a localização de defeitos. Outras versões da ferramenta
JaBUTi também estão disponíveis: (1) JaBUTi/AJ (Java Bytecode Understanding and Tes-
ting/AspectJ) [235], para o teste estrutural de unidade de programas orientados a aspectos
(OA), baseados na linguagem AspectJ; (2) JaBUTi/MA (Java Bytecode Understanding and
Testing/Mobile Agents) [112], para o teste estrutural de agentes móveis; e (3) JaBUTi/DB
(Java Bytecode Understanding and Testing/Database) [297], para o teste estrutural de aplica-
ções de banco de dados. Uma síntese das principais ferramentas de apoio ao teste estrutural de
programas OO e de componentes bem como detalhes referentes à ferramenta JaBUTi podem
ser encontrados no Capítulo 6.
4.5.1 POKE-TOOL
Figura 4.6 – Parte dos elementos requeridos pelos critérios Todos-Potenciais-Usos e Todos-
Potenciais-Usos/Du.
litado continua fazendo parte do conjunto de casos de teste, mas não é utilizado em execuções
posteriores até que seja novamente habilitado.
A título de ilustração, considere o conjunto de casos de teste T0 = {(a1, Válido), (2B3,
Inválido), (Z-12, Inválido), (A1b2C3d, Inválido)}, gerado a fim de satisfazer o critério Partici-
onamento em Classes de Equivalência (Capítulo 2) para o teste do programa identifier.
Para a condução dos testes estruturais, considere os critérios Todas-Arestas e Todos-Poten-
ciais-Usos.
Nas Figuras 4.7 e 4.8 é apresentada a cobertura obtida com a execução do conjunto de
casos de teste T0 em relação aos critérios Todas-Arestas e Todos-Potenciais-Usos, respec-
tivamente. Os elementos requeridos e os elementos não executados para ambos os critérios
também são ilustrados como parte dos relatórios gerados pela ferramenta. No caso do critério
Todas-Arestas, não há arcos não executados, visto que a cobertura atingida foi de 100%.
É interessante observar que somente com os casos de teste funcionais foi possível co-
brir o critério Todas-Arestas, ao passo que para se cobrir o critério Todos-Potenciais-Usos
ainda é necessário analisar as associações que não foram executadas. Deve-se ressaltar que
o conjunto T0 é Todas-Arestas-adequado, ou seja, o critério Todas-Arestas foi satisfeito; no
entanto, o programa identifier contém um defeito cuja presença não foi revelada. Cer-
tamente, um conjunto adequado ao critério Todas-Arestas que revelasse a presença do defeito
poderia ter sido gerado; o que se ilustra aqui é que isso nem sempre acontece.
Caso se deseje melhorar a cobertura em relação ao critério Todos-Potenciais-Usos, novos
casos de teste devem ser inseridos com vistas a cobrir as associações que ainda não foram exe-
cutadas. Antes disso, porém, deve-se verificar entre as associações não executadas se existem
associações não executáveis. A sessão de teste deve prosseguir nessa iteração de analisar ele-
mentos não executáveis e inserir casos de teste até que um conjunto adequado ao critério seja
obtido. No caso, as associações 1, (8, 9), {length, valid_id}, 2, (8, 10), {length}
e 6, (8, 9), {valid_id} são não executáveis.
72 Introdução ao Teste de Software ELSEVIER
Na Tabela 4.7 é ilustrada a evolução da sessão de teste até que se atinja a cobertura de
100% para o critério Todos-Potenciais-Usos. Do total de 29 associações executáveis requeri-
das pelo critério Todos-Potenciais-Usos, 20 foram cobertas pelo conjunto T0 . A fim de cobrir
as associações restantes, três novos casos de teste foram adicionados, resultando no conjunto
T1 = T0 ∪ {(1#, Inválido), (%, Inválido), (c, Válido)} e na cobertura de mais seis associa-
ções. As três associações restantes foram cobertas com a adição de um novo caso de teste ao
conjunto T1 , totalizando oito casos de teste necessários para satisfazer o critério – T2 = T1 ∪
{(#-%, Inválido)}. O símbolo indica quais associações foram cobertas por quais conjuntos
de casos de teste e o símbolo × mostra quais são as associações não executáveis.
4.5. Ferramentas 73
teste (ABCDEF, Válido) a fim de exercitar a associação 1, (8, 10), length. Por outro
lado, utilizando-se o critério Análise de Mutantes (apresentado no Capítulo 5), esse defeito
teria sido necessariamente revelado, já que, ao se tentar “matar” os mutantes gerados pela
aplicação do critério, o testador seria “forçado” a escolher esse tipo de caso de teste a fim de
mostrar a diferença de comportamento entre o programa original e os programas mutantes.
Essa análise fica como exercício para o leitor.
Na realidade, considerando esse contexto, motiva-se a pesquisa de critérios de teste que
exercitem os elementos requeridos com maior probabilidade de revelar a presença de de-
feitos [404]. Além disso, outra perspectiva que se coloca é utilizar uma estratégia de teste
incremental, a qual informalmente procurou-se ilustrar nesta seção. Em primeiro lugar, foram
exercitados os requisitos de teste requeridos pelo critério funcional Particionamento em Clas-
ses de Equivalência (apresentado no Capítulo 2). Em seguida, foram considerados os critérios
estruturais Todas-Arestas e Todos-Potenciais-Usos. Posteriormente, poder-se-ia considerar o
critério Análise de Mutantes.
É importante observar que os requisitos de teste exigidos pelos critérios estruturais dis-
cutidos neste capítulo limitam-se ao escopo da unidade. De fato, tais critérios foram inicial-
mente propostos para o teste de unidade de programas procedimentais. No entanto, esforços
na tentativa de estender o uso de critérios estruturais para o teste de integração também podem
ser identificados [160, 171, 200, 244].
Linnenkugel e Müllerburg [244] propuseram uma série de critérios que estendem os cri-
térios baseados em fluxo de controle e em fluxo de dados para o teste de integração. O
modelo de integração utilizado representa o programa por meio de um “Grafo de Chamada”
(call-graph), no qual os módulos do programa são associados aos nós do grafo e o fluxo de
controle (chamadas) é associado aos arcos. Os critérios de integração baseados em fluxo
de controle são definidos sobre o Grafo de Chamada da mesma forma que os critérios para o
teste de unidade são definidos sobre o GFC. Para definir os critérios de integração baseados
em fluxo de dados, foi necessário estender o conceito de associação definição-uso para incluir
caminhos interprocedimentais. Tais critérios baseiam-se na família de critérios de fluxo de
dados definida por Rapps e Weyuker [336, 337].
Harrold e Soffa [171] também definiram uma abordagem para o teste de integração basea-
da nos critérios de Rapps e Weyuker. A principal diferença com o trabalho de Linnenkugel
e Müllerburg é o modelo de integração utilizado. O programa é representado por um “Grafo
de Fluxo de Dados Resumido” (summary-graph), no qual os nós representam regiões de có-
digo de interesse interprocedimental (entradas de procedimentos, saídas de procedimentos,
chamadas de procedimentos e retornos) e os arcos representam informações de fluxo de con-
trole.
Haley e Zweben [160] propuseram um critério para selecionar caminhos em um mó-
dulo que deveria ser testado novamente na fase de integração com base em sua interface.
Jin e Offutt [200] definiram alguns critérios baseados em uma classificação de acoplamento
entre módulos. Vilela [411], com base no conceito de potencial-uso, estendeu os critérios
Potenciais-Usos para o teste de integração. Assim como Linnenkugel e Müllerburg, também
é utilizado o Grafo de Chamada para representar a estrutura do programa em teste.
Uma das desvantagens do teste estrutural é a existência de elementos requeridos não
executáveis. Existe também o problema de caminhos ausentes, ou seja, quando uma certa
funcionalidade deixa de ser implementada no programa, não existe um caminho que corres-
ponda àquela funcionalidade e, como conseqüência, nenhum caso de teste será requerido para
exercitá-la. Mesmo assim, tais critérios estabelecem de forma rigorosa os requisitos de teste
a serem exercitados, em termos de caminhos, associações definição-uso, ou outras estrutu-
ras do programa, fornecendo medidas objetivas sobre a adequação de um conjunto de teste
para o teste de um dado programa. O rigor na definição dos requisitos favorece, ainda, a
automatização desses critérios.
Por fim, é importante ressaltar o aspecto complementar das diversas técnicas e critérios
de teste e a relevância de se conduzirem estudos experimentais para a formação de um corpo
de conhecimento que favoreça o estabelecimento de estratégias de teste incrementais que
explorem as diversas características dos critérios. Nessas estratégias seriam aplicados ini-
cialmente critérios “mais fracos” e talvez menos eficazes para a avaliação da adequação do
conjunto de casos de teste e, em função da disponibilidade de orçamento e de tempo, incre-
mentalmente, poderiam ser utilizados critérios mais “fortes” e eventualmente mais eficazes,
porém, em geral, mais caros. Estudos experimentais têm sido conduzidos no sentido de ava-
liar os aspectos de custo, strength e eficácia dos critérios de teste, buscando contribuir para
76 Introdução ao Teste de Software ELSEVIER
Teste de Mutação
5.1 Introdução
Myers [294] destaca que o objetivo da atividade de teste é revelar defeitos, uma vez que não
se pode, por meio da execução do programa, provar sua correção. Já que o teste bem-sucedido
é o que revela a presença de um defeito, um bom caso de teste é o que tem alta probabilidade
de revelar um defeito ainda não descoberto.
Em princípio, é impossível medir-se a probabilidade de um determinado caso teste revelar
ou não um defeito. Por isso, técnicas de teste como a funcional ou estrutural buscam aumen-
tar a probabilidade de revelar defeitos por meio da escolha sistemática de um conjunto de
casos de teste que represente as principais características do domínio de entrada do programa
em teste. Os critérios dessas técnicas subdividem o domínio em subdomínios e obrigam o
testador a selecionar dados de teste em cada um dos subdomínios, na esperança de que o
conjunto construído dessa maneira possa revelar possíveis defeitos. Por exemplo, ao definir
que determinado caminho do programa deve ser exercitado, estabelece-se um subconjunto do
domínio de entrada, formado por aqueles dados de teste capazes de executar tal caminho e
do qual um caso de teste deve ser selecionado.
Embora essa divisão em subdomínios seja útil, uma vez que força a escolha de casos de
teste com características distintas e que exercitem o programa de maneiras diversas, não existe
uma relação direta entre um subdomínio e a capacidade de um caso de teste dele extraído
em revelar defeitos. Por exemplo, dado um caminho a ser exercitado, no qual existe um
defeito, podem existir no subdomínio correspondente casos de teste que revelem a presença
do defeito, ou seja, que levem o programa a falhar, e outros que, apesar de executar o caminho
desejado, não revelem a presença de defeitos.
O Teste de Mutação ou Análise de Mutantes, como também é conhecido, é um critério de
teste da técnica baseada em defeitos. Nessa técnica são utilizados defeitos típicos do processo
de implementação de software para que sejam derivados os requisitos de teste. No caso do
78 Introdução ao Teste de Software ELSEVIER
Teste de Mutação, o programa que está sendo testado é alterado diversas vezes, criando-
se um conjunto de programas alternativos ou mutantes, como se defeitos estivessem sendo
inseridos no programa original. O trabalho do testador é escolher casos de teste que mostrem
a diferença de comportamento entre o programa original e os programas mutantes.
Assim como nas demais técnicas, cada mutante determina um subdomínio do domínio
de entrada, formado por aqueles dados capazes de distinguir o comportamento do programa
original e do mutante. A diferença, nesse caso, é que cada subdomínio está claramente rela-
cionado com a capacidade de revelar um defeito específico. A partir da “hipótese do progra-
mador competente” e do “efeito de acoplamento”, discutidos adiante neste capítulo, pode-se
argumentar que um conjunto de casos de teste capaz de distinguir um conjunto de mutantes
escolhido de forma criteriosa, e que represente boa parte dos defeitos mais comuns, seria
capaz de revelar, também, outros tipos de defeitos.
Este capítulo está organizado como se segue. Na Seção 5.2 é feita uma revisão histórica
do surgimento e desenvolvimento do Teste de Mutação. Em seguida, na Seção 5.3 são apre-
sentadas as definições e os conceitos básicos associados ao critério. Os passos envolvidos
na aplicação do Teste de Mutação são descritos na Seção 5.4. Na Seção 5.5 são apresenta-
das e discutidas as principais características de algumas ferramentas de suporte ao Teste de
Mutação, em particular da ferramenta Proteum [107].
Embora seja um critério voltado principalmente ao teste de unidade, é possível adaptar
o Teste de Mutação para outras fases do processo de software. Em particular, é discutida na
Seção 5.6 uma extensão do critério que procura modelar defeitos relacionados a erros de inte-
gração entre as unidades do programa. Como esses erros estão, em geral, relacionados com a
interface de uma unidade, esse critério é chamado de Mutação de Interface. Na Seção 5.7 são
brevemente discutidos outros trabalhos relacionados ao Teste de Mutação, em particular sua
aplicação a modelos de especificação como Máquinas de Estados Finitos e Redes de Petri.
Os comentários finais sobre o tema são apresentados na Seção 5.8.
5.2 Histórico
O artigo “Hints on test data selection. Help for the practicing programmer”, publicado em
1978 na revista Computer, por pesquisadores do Georgia Institute of Technology e da Yale
University [115], é considerado o marco inicial do Teste de Mutação. Nesse trabalho, os
autores – DeMillo, Lipton e Sayward – descrevem as idéias principais sobre a técnica, apre-
sentando as duas hipóteses que fundamentam a sua utilização: a hipótese do programador
competente e o efeito de acoplamento. A primeira, apesar de não ser explicitamente apresen-
tada com essa denominação no artigo, aparece fortemente relacionada com a segunda.
Os autores também apresentam uma série de exemplos que ilustram como casos de teste
que revelam defeitos simples podem melhorar a qualidade da atividade de teste. Apresentam,
ainda, uma comparação da técnica de mutação com critérios de teste baseado em fluxo de
controle e com o teste randômico, mostrando as vantagens da técnica por eles proposta. Já
nesse primeiro artigo os autores comentam sobre o “sistema de mutação” desenvolvido pelo
grupo de pesquisa em que trabalham e que daria suporte à aplicação do Teste de Mutação.
Nos anos seguintes, muitos trabalhos foram desenvolvidos, abordando diversos aspectos
do Teste de Mutação, como, por exemplo, aspectos teóricos, criação de critérios alternati-
vos, avaliação do critério, definição de arquiteturas para a execução eficiente dos mutantes
5.2. Histórico 79
revelar a existência de defeitos. Por isso, diversos trabalhos na área de Engenharia de Soft-
ware experimental utilizam o Teste de Mutação como um “padrão” para verificar a efetividade
de novos critérios de teste.
Para provar a correção de um programa, um conjunto de teste deve ser confiável (reliable),
de acordo com a definição de Howden [187]:
Se por um lado é simples mostrar que para qualquer programa existe sempre um conjunto
de teste finito e confiável, por outro lado não existe um procedimento efetivo para gerar casos
de teste confiáveis ou avaliar se um conjunto de teste é ou não confiável [116].
Com o intuito de obter uma definição mais realista, no sentido de que possa ser empregada
na prática, outra abordagem para correção se apresenta. Nessa abordagem, considera-se um
conjunto Φ de programas alternativos, que depende de P e do qual P é membro. Tal conjunto
Φ(P ) é chamado de “vizinhança” de P.
A intenção é mostrar que um programa é correto por meio de um processo de eliminação,
mostrando que nenhum programa na vizinhança de P, não equivalente a P, é correto. Isso
pode ser feito usando-se um conjunto de teste T para o qual todos os membros de Φ(P ), não
equivalentes a P, falhem em pelo menos um caso de teste. Tal conjunto é definido da seguinte
maneira:
Budd mostra que sempre que existir um aceitador existirá também um gerador e vice-
versa, mas isso nem sempre acontece. A existência desses dois procedimentos está relacio-
nada com a existência de um “verificador de equivalência” S(P1 , P2 ) que responde verda-
deiro quando os programas P1 e P2 são equivalentes e falso caso contrário. Mais precisa-
mente, mostra que existe um aceitador se e somente se existir um gerador, se e somente se
existir um verificador de equivalência.
5.4. Aplicação do Teste de Mutação 83
É importante notar: tal procedimento exige, além da execução do programa que está
sendo testado, que algumas tarefas sejam executadas de forma automática. Pela grande quan-
tidade de processamento que demandam tarefas como gerar e executar os mutantes e compa-
rar os resultados produzidos, é praticamente impossível executá-las com precisão e eficiência
sem apoio computacional. Assim, na prática, deve-se associar à aplicação do critério uma
ferramenta de suporte que interaja com o testador na execução do teste. Tal ferramenta, além
de interativa, é também iterativa, pois envolve a repetição de três passos básicos: 1) criar
casos de teste; 2) executar os mutantes; e 3) analisar o resultado da execução. Em seguida,
serão analisados com mais detalhes os passos descritos anteriormente. O Programa 5.1 será
utilizado como exemplo.
Programa 5.1
1 void main()
2 {
3 int x, y, pow;
4 float z, ans;
5
6 scanf("%d %d", &x, &y);
7 if (y >= 0)
8 pow = y;
9 else
10 pow = -y;
11 z = 1;
12 while (pow-- > 0)
13 z = z*x;
14 if (y < 0)
15 z = 1 / z;
16 ans = z + 1;
17 printf("%-5.2f", ans);
18 }
*
84 Introdução ao Teste de Software ELSEVIER
Esta é a primeira fase da aplicação do critério. De acordo com a teoria apresentada an-
teriormente, constrói-se a vizinhança Φ(P ) efetuando-se pequenas alterações em P. Cada
programa gerado dessa forma é um mutante de P.
Esse é o passo-chave da Análise de Mutantes. O sucesso da aplicação do critério depende
totalmente da escolha da vizinhança Φ(P ), que deve ter as seguintes características:
• ser abrangente, de modo que um conjunto de casos de teste adequado de acordo com a
vizinhança gerada consiga revelar a maior parte dos erros de P; e
• ter cardinalidade pequena, para que o problema de gerar ou de verificar se um conjunto
de casos de teste é ou não adequado seja tratável.
A hipótese do programador competente afirma que todo programa criado por um progra-
mador competente está correto ou está próximo do correto. Caso se assuma essa hipótese
como válida, pode-se afirmar que erros são introduzidos nos programas por meio de desvios
sintáticos que, apesar de não causarem erros sintáticos, alteram a semântica do programa e,
como conseqüência, conduzem o programa a um comportamento incorreto. Para revelar tais
erros, a análise de mutantes identifica os desvios sintáticos mais comuns e, aplicando peque-
nas transformações sobre o programa em teste, encoraja o testador a construir casos de teste
que mostrem que tais transformações conduzem a um programa incorreto [7].
Além disso, alguns estudos como [2, 51] mostram, como conclusão de procedimentos
experimentais, que conjuntos de casos de teste que revelam erros simples como os descritos
acima conseguem, em geral, revelar outros tipos de erros. Esse é o chamado “efeito de
acoplamento” (coupling effect) que, segundo DeMillo [115], pode ser descrito da seguinte
maneira:
Para modelar os desvios sintáticos mais comuns, gerando o conjunto de mutantes Φ(P ),
utilizam-se “operadores de mutação” (mutant operators). Um operador de mutação aplicado
a um programa P transforma P em um programa similar, ou seja, um mutante. Em geral, a
aplicação de um operador de mutação gera mais que um mutante, pois, se P contém várias
entidades que estão no domínio do operador, então esse operador é aplicado a cada uma
dessas entidades, uma de cada vez. Como exemplo, considere o operador de mutação que
retira um comando de P. Todos os comandos do programa estão no domínio desse operador
que, quando aplicado em P, gera tantos mutantes quantos forem os seus comandos. Cada
mutante contém todos os comandos de P exceto um, retirado pelo operador de mutação.
Um mutante criado como o descrito é um “mutante de erro induzido” (fault induced
mutant). Outro tipo de operador de mutação é o que gera “mutantes instrumentados” (instru-
mented mutants). Nesse caso, uma “função armadilha” (trap function) é incluída no programa
P para gerar o mutante. Esse tipo de operador não tem como objetivo modelar algum tipo
especial de erro e sim garantir que os casos de teste tenham alguma característica particular
como a cobertura de todos os comandos do programa.
5.4. Aplicação do Teste de Mutação 85
Além dos tipos de erros que se desejam revelar e da cobertura que se quer garantir, a
escolha de um conjunto de operadores de mutação depende também da linguagem em que
os programas a serem testados estão escritos. Budd [49] relaciona 22 operadores de mutação
utilizados pelo sistema de mutação EXPER (Experimental Mutation System) [49], para pro-
gramas em Fortran. Para Cobol, são 27 operadores, utilizados pelo sistema CMS [2]. Já para
a linguagem C, Agrawal et al. [7] definem um conjunto de 77 operadores de mutação. Esse
conjunto de operadores foi adaptado e implementado na ferramenta Proteum [107], descrita
na Seção 5.5.
Tradicionalmente, não existe uma maneira direta de definir os operadores de mutação
para uma dada linguagem. Em geral, os operadores de mutação são projetados tendo por
base a experiência no uso de dada linguagem, bem como os enganos mais comuns cometidos
durante a sua utilização. Nos trabalhos de Kim [212, 214] foi proposto o uso de uma técnica
chamada “Hazard and Operability Studies – (HAZOP)” para se chegar ao conjunto de 13
operadores de mutação de classe para programas Java. Embora a técnica HAZOP não difira
substancialmente da abordagem usual, ela tenta sistematizar e tornar mais rigorosa a forma
como os operadores de mutação são gerados. Primeiramente, a técnica identifica na gramática
de Java as construções alvo de mutações e, com base em algumas palavras-chave, tais como
NO/NONE, MORE, LESS, AS WELL AS, PART OF, REVERSE, OTHER THAN, NAR-
ROWING, WIDENING e EQUIVALENT, os operadores de mutação são definidos [213].
A idéia é que essas palavras-chave guiem a construção dos operadores de mutação. À me-
dida que mais palavras-chave são aplicadas nas diversas partes que compõem determinado
comando, os operadores de mutação vão sendo definidos. Esse processo é repetido conside-
rando cada um dos comandos aos quais se desejam aplicar mutações.
Como exemplo, considere os seguintes operadores de mutação para C [7], aplicados sobre
o programa exemplo da Figura 5.1:
• if (armadilha_se_verdadeiro(e)) S;
• if (armadilha_se_falso(e)) S;
todos os desvios especificados por comandos if sejam exercitados pelo menos uma vez. A
Tabela 5.3 mostra como ficam os mutantes instrumentados com essas funções.
Com o operador “troca de referência a variável escalar” (Vsrr – scalar variable reference
replacement), cada referência a uma variável não estruturada (escalar) é substituída pela refe-
rência a todas as outras variáveis escalares do programa, uma de cada vez. O erro modelado
por esse operador é o uso de uma variável incorreta. A Tabela 5.4 mostra como trechos do
programa devem ser alterados para criar os mutantes. Note-se o grande número de mutantes
criados por esse operador. Como observa Budd [49], esse tipo de operador é o que, em geral,
produz o maior número de mutantes.
É importante notar que, em princípio, é possível gerar mutantes com a aplicação de mais
de um operador de mutação de uma só vez. Mutantes gerados a partir de k alterações si-
multâneas no programa P que está sendo testado são chamados de mutantes de ordem k.
Estudos anteriores [51] mostram que mutantes de ordens superiores, além de não contribuir
de forma significativa para a construção de casos de teste melhores, têm um custo de geração
e execução demasiado alto. Portanto, tem-se utilizado no Teste de Mutação uma vizinhança
composta apenas dos mutantes de primeira ordem.
Este passo no Teste de Mutação não é diferente de outros critérios de teste. Consiste em
executar o programa P usando os casos de teste selecionados e verificar se seu comportamento
é o esperado. Se não for, ou seja, se o programa apresenta resultados incorretos para algum
caso de teste, então um erro foi detectado. Caso contrário, o teste continua no passo seguinte.
Em geral, a tarefa de oráculo, ou seja, decidir se o resultado produzido é correto ou não,
é desempenhada pelo testador. Isso não acontece apenas na Análise de Mutantes, mas na
maioria das técnicas e dos critérios de teste.
Suponha que, inicialmente, dispõe-se do conjunto T = {(2, 1)} para o teste do programa
exemplo, ou seja, T possui apenas um caso de teste com os valores X = 2 e Y = 1. De
acordo com a especificação, pode-se calcular o resultado esperado, que é 21 + 1 = 3.
Executando o programa P do exemplo com esse caso de teste obtém-se a impressão do
valor “3.00”, ou seja, o programa comportou-se como o esperado.
Neste passo, cada um dos mutantes é executado usando os casos de teste de T. Se um mu-
tante M apresenta resultado diferente de P, isso significa que os casos de teste utilizados são
88 Introdução ao Teste de Software ELSEVIER
sensíveis e conseguiram expor a diferença entre P e M; nesse caso, M está morto e é descar-
tado. Por outro lado, se M apresenta comportamento igual a P, diz-se que M continua vivo.
Isso indica ou uma fraqueza em T, pois não conseguiu distinguir P de M, ou que P e M são
equivalentes.
Este passo pode ser totalmente automatizado, não requerendo a intervenção do testador.
Os resultados obtidos com a execução dos mutantes são comparados com os produzidos pelo
5.4. Aplicação do Teste de Mutação 89
programa original no passo anterior, e a própria ferramenta de mutação pode marcar como
mortos os mutantes que apresentam resultados diferentes.
Após a execução dos mutantes, pode-se ter uma idéia da adequação dos casos de teste
utilizados, por meio do “escore de mutação” (mutation score). O escore de mutação varia
entre 0 e 1 e fornece uma medida objetiva de quanto o conjunto de casos de teste analisado
aproxima-se da adequação. Dado o programa P e o conjunto de casos de teste T, calcula-se
o escore de mutação ms(P, T ) da seguinte maneira:
DM (P, T )
ms(P, T ) =
M (P ) − EM (P )
onde:
Logo, o escore de mutação pode ser obtido calculando a razão entre o número de mutantes
efetivamente mortos por T e o número de mutantes que se pode matar, dado pelo número total
de mutantes gerados subtraído do número de mutantes equivalentes. Observe que apenas
DM (P, T ) depende do conjunto de casos de teste utilizado. Apesar disso, não se conhece,
a princípio, o número de mutantes equivalentes gerados. EM (P ) é obtido iterativamente
à medida que o testador decide, no passo seguinte do teste, marcar como equivalente um
mutante M que continua vivo.
Existem alguns problemas relacionados com esse passo da Análise de Mutantes. Um de-
les é decidir se um mutante está morto ou vivo; mais precisamente, determinar que entidades
devem ser comparadas para decidir se o comportamento do mutante e o do programa original
são os mesmos.
A opção mais simples é capturar as seqüências de caracteres enviadas pelos programas
para os dispositivos de saída e comparar as seqüências produzidas pelo programa original
e pelos mutantes. Alternativamente, podem-se comparar somente os caracteres não brancos
[443]. Outros pontos podem ser usados para tentar distinguir o comportamento dos progra-
mas. O código de retorno, valor que indica se a execução terminou normalmente ou qual foi
a causa da terminação, é também usado com esse propósito.
É importante observar que essas características excluem outros tipos de eventos de entrada
e saída, como, por exemplo, em programas com interface gráfica. Nesses casos torna-se
mais difícil capturar os casos de teste, reexecutar os mutantes e comparar os seus resultados
utilizando tais eventos. Por isso, tende-se a utilizar o Teste de Mutação para o teste de unidade
ou integração, e não em sistemas completos.
Se a Análise de Mutantes estiver sendo aplicada em um ambiente interpretado, no qual
a ferramenta de teste tem controle sobre a execução dos mutantes e do programa em teste,
então pode-se usar também o valor final das variáveis ou até mesmo estados intermediários
do programa para decidir se um mutante deve morrer ou não [309].
90 Introdução ao Teste de Software ELSEVIER
mesmo operador de mutação sobre o mesmo ponto do programa e executar as partes comuns
desses mutantes de forma concorrente, um mutante em cada EP.
Choi et al. [77] descreve uma implementação de um sistema de mutação que utiliza uma
arquitetura hipercubo para distribuir a execução dos mutantes. Basicamente, tem-se o sis-
tema de mutação rodando numa máquina hospedeira e comunicando-se com um pool de
processadores com memória local arranjados na forma de um hipercubo. Cada processador
do hipercubo é alocado para executar e avaliar os resultados de um mutante de cada vez.
No hospedeiro existe um escalonador que cuida da comunicação com os processadores do
hipercubo fornecendo dados, ou seja, o mutante a ser executado, os casos de teste a serem
empregados e os resultados esperados, e recebendo de cada nó uma resposta se o mutante foi
morto ou não.
Para diminuir o tempo de execução dos mutantes existe também a solução por meio de
software. Como cada mutante difere do programa original em apenas um ponto e como a
execução de ambos até esse ponto é igual, então não é necessário executar o trecho que ambos
têm em comum duas vezes. A idéia é, por meio da execução interpretada dos programas, fazer
com que cada mutante inicie sua execução somente a partir do ponto no qual ele difere do
programa original [309].
Uma abordagem completamente diferente das anteriores é tentar selecionar, entre os mu-
tantes gerados, apenas um subconjunto deles. O objetivo é diminuir o número de mutantes a
serem executados e analisados, porém sem perder a qualidade.
A idéia por trás dessas abordagens é sempre a mesma. Tomando um programa P e sua
vizinhança Φ(P ), gerada por um conjunto de operadores de mutação, estudos experimentais
mostram ser possível selecionar um subconjunto Φ (P ) de forma que, ao construir um con-
junto de casos de teste T adequado de acordo com Φ (P ), tem-se que T é também adequado
ou está muito próximo da adequação em relação a Φ(P ). Em outras palavras, em vez de utili-
zar o conjunto total de mutantes para a aplicação do critério, pode-se utilizar um subconjunto
desses mutantes, obtendo um conjunto de casos de teste de qualidade similar.
A questão que se coloca, então, é como selecionar esse subconjunto de mutantes, de
modo a maximizar a adequação em relação ao conjunto total de mutantes. A primeira forma,
e também a mais simples, utilizada para diminuir o número de mutantes a serem executados
é a amostragem simples. Em vez de considerar todos os mutantes gerados, seleciona-se
aleatoriamente uma porcentagem deles e a análise fica restrita apenas a esses mutantes sele-
cionados. Em geral, mesmo uma amostragem com pequena porcentagem do total de mutantes
é suficiente para construir bons casos de teste. Estudos conduzidos com programas Fortran [2]
mostraram que conjuntos de teste gerados usando a mutação aleatória com apenas 10% dos
mutantes chegaram a alcançar escore de mutação de 0,99 em relação ao total de mutantes.
Tais resultados foram confirmados por Mathur e Wong para programas Fortran e C [274, 437,
439].
A mutação aleatória não leva em consideração características particulares de cada opera-
dor de mutação no que diz respeito à eficácia em revelar a presença de defeitos. Assim, foi
proposta a mutação restrita, na qual são selecionados apenas alguns dos operadores de mu-
tação para serem empregados no teste. Os experimentos de Mathur e Wong [274, 437, 439]
também exploram a mutação restrita, a qual se mostrou efetiva na redução do número de
mutantes e com alta adequação em relação ao total de mutantes.
92 Introdução ao Teste de Software ELSEVIER
83 mutantes gerados por operadores que podem introduzir anomalias, sobram 28. Ou seja,
obtém-se uma redução de 66% desses mutantes e de 58% do total de mutantes.
O Programa 5.2 mostra o mutante 36, que é um dos mutantes eliminados por possuir
anomalias de fluxo de dados. Claramente pode-se constatar uma anomalia no predicado do
primeiro comando if. A variável y é utilizada no predicado sem ter sido inicializada. Isso
acontece porque o comando que fazia a leitura do valor de y foi alterado, substituindo a
leitura de y pela leitura de pow.
Programa 5.2
1 void main()
2 {
3 int x, y, pow;
4 float z, ans;
5
6 scanf("%d %d", &x, &pow);
7 if (y >= 0)
8 pow = y;
9 else
10 pow = -y;
11 z = 1;
12 while (pow-- > 0)
13 z = z*x;
14 if (y < 0)
15 z = 1 / z;
16 ans = z + 1;
17 printf("%-5.2f", ans);
18 }
*
Uma vez eliminados os mutantes com anomalias de fluxo de dados, resta executar os
demais com os casos de teste de T, que até agora possui um único caso de teste (2, 1), e
comparar os resultados com a execução de P. O resultado da execução está sumariado na
Tabela 5.5.
Note-se que, dos mutantes mortos, alguns apresentaram resultado diferente de P, alguns
tiveram erros de execução e outros morreram por “timeout”, ou seja, o tempo de execução do
mutante superou muito o tempo de execução de P, indicando que possivelmente o mutante
entrou num laço sem fim.
Dos 43 mutantes usados, 21 foram mortos pelo caso de teste. Considerando-se que até
agora não se conhece nenhum mutante equivalente, pode-se calcular o escore de mutação
como:
21
ms(P, T ) = = 0, 49
43
Este é o passo que requer mais intervenção humana. Primeiro, é preciso decidir se o teste
continua ou não. Se o escore de mutação for 1,0 – ou se estiver bem próximo de 1,0, de
acordo com a avaliação do testador –, então, pode-se encerrar o teste e considerar T um bom
conjunto de casos de teste para P.
94 Introdução ao Teste de Software ELSEVIER
Caso se decida continuar o teste, é preciso analisar os mutantes que sobreviveram à exe-
cução com os casos de teste disponíveis e decidir se esses mutantes são ou não equivalentes
ao programa original. Em geral, é indecidível se dois programas são equivalentes. Essa li-
mitação teórica não significa que o problema deva ser abandonado por não ter solução. Na
verdade, alguns métodos e algumas heurísticas têm sido propostos para determinar a equiva-
lência de programas em uma grande porcentagem dos casos de interesse [306, 311]. E, em
último caso, o testador deve decidir sobre a equivalência.
Nessa direção, podem ser destacados alguns trabalhos, como o de Offut e Craft [306],
que visa à identificação de alternativas que permitam a determinação automática de mutantes
equivalentes. Nesse estudo são apresentadas seis técnicas baseadas em estratégias de otimi-
zação de compiladores e análise de fluxo de dados para determinar mutantes equivalentes. As
técnicas são:
• propagação de constantes;
• propagação de invariantes;
5.4. Aplicação do Teste de Mutação 95
A aplicação de tais técnicas mostra que, dependendo do tipo de programa que está sendo
analisado, podem ser obtidos resultados bastante diferentes. Nos experimentos realizados por
Offut e Craft, o número de mutantes equivalentes corretamente identificados varia entre 0 e
50%. Complementando esse trabalho, Offut e Pan [311] propuseram três estratégias baseadas
em restrições matemáticas, originalmente desenvolvidas para a geração de dados de teste,
para determinar mutantes equivalentes e caminhos não alcançáveis. Essas estratégias também
chegam a detectar cerca de metade dos mutantes equivalentes, dependendo das características
dos programas analisados.
No trabalho de Jorge [203] foram realizados estudos sobre os operadores de mutação para
a linguagem C considerando os aspectos de equivalência. A partir dessa análise, foram deter-
minadas heurísticas para a determinação de mutantes equivalentes, tanto pela semântica do
operador como para o domínio de aplicação. Também foi feita uma análise estatística sobre
os operadores de mutação, com o objetivo de fornecer diretrizes ao testador para auxiliá-lo
na atividade de análise dos mutantes vivos. Observou-se que, apesar de não ser uma regra
geral, seis operadores de mutação referentes ao teste de unidade e sete referentes ao teste de
integração estão entre os dez operadores com custo mais elevado tanto em termos do número
de mutantes gerados quanto do número de mutantes equivalentes. Assim, ao selecionar os
operadores de mutação, deve-se evitar a utilização de tais operadores.
Uma vez decidido que um mutante não é equivalente ao programa original, deve-se, caso
se deseje construir um conjunto de casos de teste adequado para P, elaborar um caso de teste
que mate tal mutante. Em [116] é apresentada uma solução para tentar criar automatica-
mente casos de teste que matem os mutantes sobreviventes. O método, chamado de “geração
automática baseada em restrições” (constraint-based automatic test data generation) asso-
cia a cada operador de mutação um modelo que fornece, para cada mutante gerado por esse
operador, uma restrição que deve ser satisfeita na construção dos casos de teste.
Ao analisar os mutantes do Programa 5.1, são identificados alguns mutantes que são
equivalentes e outros que não são. Os equivalentes são os de número 12, 20 e 25. A título de
ilustração, considere o mutante 12. O programa original difere desse mutante na linha 7: o
comando if(y >= 0) (programa original) foi substituído por if(y > 0) (mutante 12)
como conseqüência da aplicação do operador ORRN, que realiza a troca de operadores relaci-
onais. Essa alteração, no entanto, não provoca diferença de comportamento entre o programa
original e o mutante. De fato, em ambos os programas, quando y = 0, pow é inicializada
com 0.
Com a identificação dos mutantes equivalentes obtém-se um novo valor para o escore de
mutação:
21
ms(P, T ) = = 0, 53
43 − 3
Para os demais mutantes podem ser incluídos novos casos de teste com o intuito de matá-
los. Por exemplo, analisando o mutante número 39, percebe-se que para tentar matá-lo deve
ser utilizado um caso de teste com os valores de x e y com os sinais opostos; por exemplo,
(2, −1).
96 Introdução ao Teste de Software ELSEVIER
Assim, o caminho é retornar ao segundo passo do critério, executar P com esse novo caso
de teste, executar os mutantes, recalcular o escore de mutação e assim por diante, até que se
consiga um bom conjunto T.
O Programa 5.1, executado com o novo caso de teste (2, −1), apresenta o resultado espe-
rado, ou seja, “1.50”. A Tabela 5.6 mostra o resultado da execução dos mutantes, obtendo-se
um escore de mutação de 0,95, bem próximo de 1.
Tabela 5.6 – Execução dos mutantes com T = {(2, 1), (2, −1)}
Mutante Resultado Estado
4 erro morto
8 3.00 morto
9 3.00 morto
16 2.00 morto
23 3.00 morto
28 armadilha morto
29 armadilha morto
39 2.00 morto
51 2.00 morto
56 2.00 morto
72 0.50 morto
73 1.50 vivo
79 3.00 morto
80 3.00 morto
81 3.00 morto
83 1.50 vivo
84 0.00 morto
85 erro morto
91 3.00 morto
5.5 Ferramentas
Uma linha importante no desenvolvimento do critério Análise de Mutantes refere-se à cons-
trução de ferramentas de suporte a sua aplicação. Diversas ferramentas foram construídas
nessa perspectiva, principalmente nos primeiros anos de utilização da técnica.
De acordo com a literatura, a primeira dessas ferramentas foi o FMS.1 (Fortran Mutation
System – versão 1), desenvolvida na Yale University, em um computador PDP-10, e mais
tarde em um PRIME 400 no Georgia Tech e em um VAX 11 em Berkeley. Essa ferramenta
tratava de um conjunto restrito da linguagem Fortran, ou seja, programas com apenas uma
rotina, com aritmética de inteiros apenas e sem comandos de entrada e saída. Esse projeto,
apesar de suas restrições, motivou a construção de sistemas mais elaborados como o FSM.2
e o CMS.1 (Cobol Mutation System) [2]. Nessa mesma linha de ferramentas devem ser
destacados o EXPER (Experimental Mutation System) [49] e a Mothra [76, 114], também
5.5. Ferramentas 97
para Fortran. Essa última foi, certamente, a que mais se popularizou e mais influenciou o
desenvolvimento do critério.
Na década de 1990, a família de ferramentas Proteum [107] iniciou um novo período na
utilização do Teste de Mutação, apresentando diversas abordagens novas e permitindo seu
uso de maneira mais abrangente pela comunidade científica. Essa família de ferramentas,
inicialmente projetada para dar suporte ao Teste de Mutação em programas C [109], mais
tarde foi estendida para outros modelos, tais como Máquinas de Estados Finitos, Redes de
Petri e Statecharts [134, 131, 135].
Nas próximas seções as ferramentas Mothra e Proteum serão discutidas, dando-se desta-
que à última, apresentada em detalhes.
5.5.1 Mothra
5.5.2 Proteum
A versão original (1.0) da ferramenta Proteum foi apresentada inicialmente no Simpósio Bra-
sileiro de Engenharia de Software de 1993, no Rio de Janeiro [109]. Era um programa mo-
nolítico, que possuía uma interface gráfica e implementava basicamente as mesmas funcio-
nalidades da Mothra para a linguagem C, utilizando o conjunto de operadores definidos por
Agrawal et al. [7].
5.5. Ferramentas 99
• Dois modos de execução dos mutantes: “teste” ou “pesquisa”. Na execução dos mu-
tantes, o comportamento normal é interromper a execução de um mutante e descartá-lo
quando se encontra o primeiro caso de teste que mate o mutante. Esse comportamento,
padrão, é chamado na Proteum de modo teste. No modo pesquisa, cada mutante é
executado, sempre, com todos os casos do conjunto de teste. Obviamente, esse com-
portamento é mais caro do que o modo teste, mas permite a coleta de informações
importantes, principalmente no ambiente de pesquisa científica. Por exemplo, permite
que se avalie o quanto um mutante é fácil de se matar contando-se o número de casos
de teste que o distingue.
• Operações para habilitar/desabilitar casos de teste. Após inserir casos de teste, o testa-
dor pode manipulá-los de maneira bastante flexível, habilitando-os ou desabilitando-os.
Um caso de teste desabilitado continua fazendo parte do conjunto de teste mas não é
utilizado na execução dos mutantes. Com isso, o testador pode experimentar diversas
combinações de casos de teste sem ter de efetivamente alterar o conjunto fornecido.
Essa característica é, também, bastante útil para estudos experimentais.
• Da mesma forma, conjuntos de mutantes podem ser selecionados ou desselecionados
de modo a permitir que o testador verifique o efeito de determinado conjunto de teste
sobre diversos conjuntos distintos de mutantes.
2. com um único parâmetro numérico: exibe o calendário do ano cujo valor foi passado
como parâmetro. Os valores válidos vão de 1 a 9999;
Criada a sessão, ela permanece “aberta”, permitindo que o testador execute diversas ope-
rações como criação de mutantes ou casos de teste. O testador pode, também, abandonar
102 Introdução ao Teste de Software ELSEVIER
Figura 5.5 – Visualização dos comandos correspondentes a uma operação da interface gráfica.
se o próprio usuário a tivesse iniciado, permitindo que o testador interaja com o programa
fornecendo os dados de tempo de execução, quando necessário.
Figura 5.6 – Janela na qual são fornecidos os argumentos para a execução do programa.
No caso do programa Cal, como não existe interação com o usuário, o programa é exe-
cutado completamente e a janela em que foi executado fecha-se ao seu término. Assim, para
verificar se o programa comportou-se corretamente com esse dado de teste, utiliza-se a ope-
ração “TestCase/View”, que apresenta a janela ilustrada na Figura 5.7. A janela mostra o
104 Introdução ao Teste de Software ELSEVIER
número do caso de teste, os argumentos fornecidos, o tempo de execução (que será utilizado
como parâmetro para decidir quando matar um mutante por “timeout”), o código de retorno
(indicando que o programa finalizou normalmente), as entradas fornecidas por meio da en-
trada padrão (stdin) e as saídas produzidas pelo programa (stdout e stderr). Pode-se conferir
que o resultado produzido corresponde ao esperado e, portanto, o programa foi executado
com sucesso. O testador pode, também, habilitar ou desabilitar o caso de teste. Os casos
de teste desabilitados não são utilizados nas próximas execuções dos mutantes, a menos que
sejam novamente habilitados.
Antes de gerar os mutantes e avaliar os casos de teste, podemos fornecer mais alguns
casos de teste que acharmos interessantes, como:
Utilizando a opção “Status”, pode-se conferir o andamento geral da sessão de teste, como
mostra a Figura 5.8. Observe que os sete casos de teste foram devidamente inseridos na
sessão.
Para avaliar a qualidade desse conjunto de casos de teste alguns mutantes são gerados e o
escore de mutação obtido é verificado. Inicialmente, é escolhida a opção “Mutants/Generate
Unit” para se ter acesso aos operadores de teste de unidade. Esses operadores são divididos
em quatro grupos distintos:
5.5. Ferramentas 105
São gerados mutantes apenas para o primeiro grupo. Dois parâmetros podem ser es-
colhidos para cada operador: na primeira coluna do lado direito na janela “Operators Unit
Mutants”, na Figura 5.9 pode-se escolher a porcentagem de mutantes desejada. Na segunda
coluna, pode-se estabelecer o número máximo de mutantes por ponto de aplicação. Por exem-
plo, no caso de troca de operador relacional têm-se sempre cinco mutantes gerados em cada
ponto de aplicação.1 Entretanto, caso seja estabelecido que o número máximo de mutantes
a ser gerado é dois, então são selecionados, aleatoriamente, apenas dois mutantes em cada
ponto de mutação. A terceira coluna ilustra, depois da geração, o número de mutantes ge-
rados. No exemplo da Figura 5.9 foram escolhidos 30% dos mutantes e o máximo de dois
mutantes para todos os operadores de mutação. Com esses valores, tem-se um total de 247
mutantes gerados.
Em seguida, os mutantes são executados usando-se a opção “Mutants/Execute”. A ferra-
menta utiliza o conjunto de casos de teste definido para executar cada um dos 247 mutantes e
decidir quais morrem e quais continuam vivos. Apesar do alto número de mutantes que pode
ser gerado, as estratégias de redução da execução de tempo da Proteum permitem que se gas-
tem apenas alguns segundos na execução desses mutantes. Ao consultar novamente a janela
de status, obtém-se o resultado mostrado na Figura 5.10.
De acordo com a Figura 5.10, o número de mutantes que não foram mortos pelos casos
de teste é 26, o que resulta em um escore de mutação de 0,894. Certamente, esse resultado
1 Considerando os operadores relacionais: < > == >= <= !=.
106 Introdução ao Teste de Software ELSEVIER
Ao se aplicar o Teste de Mutação a uma unidade que faz parte de um programa maior,
o conjunto de mutantes criado é sempre o mesmo, independente de como a unidade é usada
(chamada) no programa. Por exemplo, se a unidade é usada em diversos pontos do programa,
não se garante que todos esses pontos serão exercitados pelos casos de teste selecionados.
Podem-se criar casos de teste que distinguem os mutantes mas executem apenas caminhos
que passem por um desses pontos. Em resumo, o Teste de Mutação é um critério efetivo para
o teste da estrutura interna da unidade, mas não necessariamente para exercitar as interações
entre unidades num programa “integrado”.
No nível de integração é necessário testar não a unidade em si, mas principalmente as
interações com as outras unidades do programa. Esse é o objetivo do critério Mutação de
Interface [108]. De acordo com o modelo de falhas apresentado por Delamaro [108], erros de
integração são caracterizados por valores incorretos trocados por meio das conexões entre as
unidades. Assim, a Mutação de Interface procura, por meio da injeção de defeitos simples,
introduzir perturbações somente em objetos relacionados com a interação entre as unidades.
Em outras palavras, os mutantes a serem distinguidos pelos casos de teste no nível de inte-
gração modelam “erros simples de integração”. Mais precisamente, como define Offut [307],
um mutante deve ser um programa cuja diferença semântica em relação ao programa em
teste seja pequena. No caso da Mutação de Interface, essa diferença deve se manifestar nas
conexões entre as unidades do programa. Dessa forma, conjuntos adequados à Mutação de
Interface (MI-adequados) devem ser, de acordo com o efeito de acoplamento, efetivos para
revelar a presença de outros tipos de defeitos que se manifestem por meio de erros nas inte-
rações entre as unidades.
Em linguagens convencionais como C, Pascal, Fortran, etc., unidades são conectadas
por meio de chamadas de subprogramas (funções, procedimentos ou sub-rotinas). Em uma
chamada da unidade F à unidade G, existem quatro maneiras, não mutuamente exclusivas, de
se trocarem dados entre as unidades:
• dados são passados para G por meio de parâmetros de entrada (passagem por parâme-
tro);
• dados são passados para G e/ou retornados para F por meio de parâmetros de entrada-
saída (passagem por referência);
• dados são passados para G e/ou retornados para F por meio de variáveis globais; e
• dados são retornados para F por meio de comandos de retorno (como return em C).
A Figura 5.14 ilustra um programa, composto por algumas conexões, sobre o qual se
aplica a Mutação de Interface. O testador pode, por exemplo, escolher testar a conexão A-B,
e, nesse caso, um operador de mutação OP é aplicado aos pontos relacionados apenas a essa
conexão. Em outras palavras, o critério é sempre aplicado de uma forma “ponto a ponto”,
limitando os requisitos de teste a uma determinada conexão.
Para testar uma conexão A-B, introduzindo defeitos simples que levem a erros de inte-
gração, a Mutação de Interface aplica mutações em: 1) pontos nos quais a unidade A chama
a unidade B, portanto antes da execução de B; e 2) pontos dentro de B relacionados com sua
interface.
Um ponto importante para se aplicar Mutação de Interface é que os operadores de muta-
ção devem contemplar a idéia de que uma conexão é testada e não uma unidade. Por exemplo,
110 Introdução ao Teste de Software ELSEVIER
caso se deseje testar a conexão C-D, um mutante cuja mutação OP(v) seja efetuada dentro
da unidade D pode ser distinguido por meio de uma chamada de D feita em B. Nesse caso, a
conexão desejada não está sendo exercitada. Assim, a aplicação de OP, quando feita dentro
da unidade chamada, deve levar em consideração o ponto em que foi feita a chamada. No
exemplo, a mutação só pode ser “ativada” se a unidade D for chamada por C. Quando cha-
mada por B, D deve se comportar exatamente como no programa original, fazendo com que
a conexão C-D tenha de ser exercitada para distinguir o mutante. Para algumas linguagens,
como C por exemplo, isso significa que a decisão de aplicar-se ou não a mutação precisa ser
feita em tempo de execução do mutante. Para mutações feitas no ponto em que C chama
D, esse problema não existe pois, nesse caso, o ponto de mutação garante que a conexão
desejada está sendo exercitada.
Segundo DeMillo e Offut [116], existem três condições que devem ser satisfeitas para
que um caso de teste t distinga um mutante M:
Operadores de Mutação de Interface são relacionados a uma conexão entre duas unidades
e cada mutante é relacionado a uma chamada de função. Por exemplo, em um programa
com uma função f que faça duas chamadas a uma função g, como na Figura 5.15, a cada
chamada de g corresponde um conjunto de mutantes que só podem ser distinguidos por meio
da execução da chamada correspondente. Considerando g’ a função criada ao se aplicar um
operador OP em um ponto no interior de g, têm-se dois mutantes distintos relacionados ao
teste da conexão f-g. O primeiro é aquele no qual a primeira chamada de g é substituída
pela chamada a g’ e o segundo é aquele no qual a segunda chamada é substituída, como
exemplificado na Figura 5.16.
Por isso, como citado anteriormente, uma mutação aplicada dentro da função chamada só
deve ser efetivamente aplicada se a função foi chamada do ponto cuja conexão se deseja tes-
tar. No exemplo da Figura 5.16, no primeiro mutante, que foi gerado para a primeira chamada
a g, deve-se substituir g pela chamada a g’. No entanto, a segunda chamada deve permanecer
inalterada, ou seja, nessa segunda chamada a função g deve comportar-se exatamente como
no programa original, de modo que M 1 só possa ser distinguido por meio da primeira cha-
mada. Somente desse modo pode-se exercitar de maneira mais completa a conexão que, na
verdade, é composta por mais de uma chamada de função.
Figura 5.15 – Indicação dos conjuntos de mutantes requeridos para o teste da conexão f-g.
f() f()
{ {
S1; S1;
S2; S2;
. .
. .
. .
g’(); g();
. .
. .
. .
g(); g’();
. .
. .
. .
} }
dentro da função g. O segundo é de operadores aplicados nos pontos em que a função f chama
a função g. Na definição dos operadores são utilizados os seguintes conjuntos, considerando
que a Mutação de Interface está sendo aplicada na conexão entre as funções f e g:
P(g): é o conjunto dos parâmetros formais de g. Esse conjunto inclui também derreferên-
cias a parâmetros do tipo ponteiro ou vetor. Por exemplo, se um parâmetro formal v é
definido como “int *v”, v e “*v” pertencem a esse conjunto;2
G(g): é o conjunto de variáveis globais utilizadas na função g;
L(g): é o conjunto de variáveis declaradas em g (variáveis locais);
E(g): é o conjunto de variáveis globais não utilizadas em g; e
C(g): é o conjunto de constantes utilizadas em g.
passando-se como valor o endereço da variável, incluiu-se no conjunto P(g) a derreferenciação dos parâmetros
ponteiros ou vetores, de forma que esse tipo de interface seja exercitado de maneira completa.
5.7. Outros trabalhos 113
Por fim, com a definição de Mutação de Interface, criou-se uma nova versão da ferra-
menta Proteum (discutida na Seção 5.5) com vistas a apoiar a aplicação desse critério. A
PROTEUM/IM [108], como foi denominada, possui arquitetura e implementação similares
às da Proteum. O que as diferencia é o conjunto de operadores de mutação utilizados em cada
uma e o fato de a PROTEUM/IM oferecer recursos para testar a conexão entre as unidades
do software. Posteriormente, a PROTEUM/IM fundiu-se com a Proteum e criou-se, assim,
a versão atual da ferramenta que apóia, de forma integrada, um único ambiente de teste, o
Teste de Mutação e a Mutação de Interface.
de Petri [134] ou Statecharts [135]. Como exemplo, considere a MEF da Figura 5.18(a). Sua
execução com uma seqüência de eventos como s1 = TCreq , CC, DT produz como saída
o1 = CR, TCconf , TDT ind .
A MEF da Figura 5.18(b) representa um mutante da MEF original criado pela aplicação
do operador de mutação que troca uma das saídas da máquina. A transição afetada pela muta-
ção tem origem e destino no estado 4. É importante observar que a seqüência s1 não é capaz
de distinguir o comportamento desse mutante. Seria preciso, por exemplo, uma seqüência
s2 = TCreq , CC, TDT req , que na MEF original produziria a saída o2 = CR, TCconf , DT
e na máquina mutante produziria o2 = CR, TCconf , CC.
A ferramenta Proteum ganhou sucessores também nessa área. Foram construídas ferra-
mentas de suporte ao Teste de Mutação tanto para máquinas de estados quanto para Redes de
Petri e Statecharts. Na Figura 5.19 ilustra-se uma das janelas da Proteum-RS/PN que trata
de especificações desenvolvidas usando Redes de Petri [360]. Assim como a Proteum no
nível de programas, a Proteum-RS/PN possui uma interface gráfica e, também, uma interface
de comandos, o que permite a criação de sessões de teste em modo batch.
Algumas das principais características da ferramenta, descritas por Simão [361], são:
opções para a importação de casos de teste a partir de outra sessão da própria Proteum-
RS/PN ou de um arquivo ASCII. Um módulo para auxiliar a geração automática de
casos de teste também existe, incorporado à ferramenta;
• mutantes: a ferramenta implementa todos os operadores de mutação para RPs definidos
por Fabbri [130]. Para dar suporte a estudos experimentais, é possível escolher quais
operadores aplicar e que porcentagem de mutantes de cada operador será gerada. Os
mutantes podem então ser executados com os casos de teste e pode-se calcular o escore
de mutação. Apenas os mutantes e os casos de teste selecionados são considerados
durante a execução e o cálculo do escore de mutação; e
5.8. Considerações finais 117
todos os seus casos de teste. Obviamente, tudo funciona como o esperado. Não satisfeito,
seu chefe decide realizar outra prova: toma seu código-fonte e altera-o, de forma que um dos
comandos se comporte de maneira incorreta. Ao executar esse novo programa que seu chefe
criou, com o conjunto de casos de teste T, qual é o resultado que você espera obter?
Exato. Espera que, pelo menos para algum elemento de T, o resultado produzido seja
incorreto, pois, afinal, seu programa está correto com T e o de seu chefe não deveria estar.
Mas, se o resultado obtido pelo programa do seu chefe for exatamente igual ao seu, então
podemos desconfiar que seu conjunto de teste não é assim tão eficiente, pois nem conseguiu
“desbancar” o truque do seu chefe.
O Teste de Mutação nada mais é do que utilizar, de maneira sistemática, a idéia aplicada
pelo seu chefe, criando uma série de implementações alternativas e forçando o testador a
projetar casos de teste que revelem os defeitos nelas introduzidos. Os casos de teste gerados
dessa forma devem ser tão sensíveis que seriam capazes de revelar, também, outros tipos de
defeitos.
Capítulo 6
6.1 Introdução
No decorrer dos tempos, novos paradigmas de programação são desenvolvidos com o ob-
jetivo de suprir deficiências dos paradigmas já existentes. No caso da orientação a objetos,
essa surgiu para tentar suprir deficiências, principalmente relacionadas ao paradigma procedi-
mental. O objetivo do paradigma procedimental é estruturar um problema em termos de um
conjunto de dados e de um conjunto de procedimentos/funções que manipulam tais dados.
Entretanto, quando grandes problemas precisam ser resolvidos utilizando-se tal paradigma, a
dependência entre os procedimentos/funções e os dados torna-se muito grande, de modo que
pequenas alterações em como os dados estão organizados podem levar a alterações profundas
na forma com que os procedimentos/funções fazem acesso a esses dados.
Nesse sentido, o paradigma de programação orientada a objetos surgiu a fim de fornecer
um mecanismo para isolar os dados da forma como são manipulados. A idéia por trás da
orientação a objetos é agrupar em uma entidade (denominada classe) os dados (atributos) e
os procedimentos/funções (métodos) que realizam as operações sobre os dados. Assim, os
dados podem permanecer isolados (ou encapsulados na classe) e o acesso a eles só pode ser
realizado por meio dos métodos definidos na classe, ocasionando o chamado ocultamento de
informação. Desse modo, em vez de estruturar o problema em termos de dados e procedi-
mentos/funções, na orientação a objetos o desenvolvedor é incentivado a pensar em termos
de classes e objetos que podem ser encarados como uma abstração de mais alto nível. Con-
ceitualmente, deveria ser mais simples modelar um problema em termos de classes e objetos
do que em termos de dados e funções. Além do encapsulamento, a orientação a objetos é
também baseada em outras características, tais como herança, polimorfismo e acoplamento
dinâmico.
Além disso, um forte apelo dos defensores da orientação a objetos é o fato de, durante
as diferentes fases do ciclo de vida, serem utilizadas as mesmas definições e nomenclaturas,
facilitando o mapeamento e a verificação de consistência entre os diferentes modelos.
120 Introdução ao Teste de Software ELSEVIER
mos de código-fonte, mas em termos dos diferentes artefatos (produtos) gerados ao longo do processo de desenvol-
vimento. É possível, por exemplo, reutilizar partes de um projeto orientado a objetos e não somente de código-fonte.
6.2. Definições e conceitos básicos 121
Cada objeto tem uma cópia dos dados existentes na classe e encapsula estado e comporta-
mento.
Em vez da idéia procedimental de entrada e saída para procedimentos, os objetos inte-
ragem entre si e são ativados por meio de mensagens. Uma mensagem é uma solicitação
para que um objeto execute um de seus métodos. O método solicitado pode alterar o estado
interno do objeto e/ou enviar mensagens a outros objetos. Ao encerrar sua execução, ele re-
torna o controle, e possivelmente algum valor, ao objeto que enviou a mensagem solicitando
a operação.
A capacidade que um objeto tem de impedir que outros objetos tenham acesso aos seus
dados é denominada de encapsulamento. O encapsulamento é uma técnica empregada para
garantir o ocultamento de informações na qual a interface e a implementação de uma classe
são separadas sintaticamente. Com isso, somente os métodos pertencentes a um objeto podem
ter acesso aos dados encapsulados. O encapsulamento encoraja a modularidade do programa
e permite que decisões de projeto fiquem “escondidas” dentro da implementação de maneira
a restringir possíveis interdependências com outras classes, exceto por meio de sua interface.
Com isso, mudanças na implementação de um método não afetam outras classes, a menos
que a interface do método seja alterada.
Novas classes podem ser definidas em função de classes já existentes. Tal relacionamento
entre classes é obtido por meio de herança. Com a utilização de herança, as classes são
inseridas em uma hierarquia de especialização, de modo que uma classe mais especializada
herde todas as propriedades da(s) classe(s) mais genérica(s), ou seja, daquela(s) que está(ão)
níveis acima na hierarquia. À classe mais genérica dá-se o nome de superclasse, classe pai ou
classe-base, e à classe mais especializada dá-se o nome de classe-filho ou subclasse. Com o
mecanismo de herança, uma subclasse pode estender ou restringir as características herdadas
da superclasse.
Algumas linguagens de programação OO, tais como C++, permitem que uma subclasse
herde características de mais de uma superclasse, caracterizando o que é chamado de he-
rança múltipla. Entretanto, tal mecanismo pode trazer problemas quando duas superclasses
oferecem atributos ou métodos com o mesmo nome.
O termo polimorfismo representa a qualidade ou o estado de um objeto ser capaz de as-
sumir diferentes formas. Quando aplicado a linguagens de programação, indica que uma
mesma construção de linguagem pode assumir diferentes tipos ou manipular objetos de dife-
rentes tipos. Por exemplo, o operador “+” na linguagem C++ pode ser utilizado para fazer a
adição de dois valores inteiros ou ponto flutuante, bem como para concatenar duas strings.
Uma característica fortemente relacionada com herança e polimorfismo é o acoplamento
dinâmico. Em programas procedimentais, sempre que uma nova funcionalidade deve ser
acrescentada, a aplicação deve ser alterada e recompilada. Com o conceito de polimorfismo,
é possível acrescentar novos métodos a classes já existentes sem a necessidade de recompilar
a aplicação. Isto é possível utilizando-se a técnica de acoplamento dinâmico (dynamic bin-
ding), que permite que novos métodos sejam carregados e ligados (binding) à aplicação em
tempo de execução.
Outro termo utilizado no contexto de orientação a objetos é o de cluster. Cluster pode ser
definido como um conjunto de classes que cooperam entre si na implementação de determi-
nada(s) funcionalidade(s). As classes dentro de um cluster podem ser fortemente acopladas
122 Introdução ao Teste de Software ELSEVIER
Encapsulamento
Herança
Herança múltipla
A herança múltipla permite que uma subclasse herde características de duas ou mais su-
perclasses, as quais podem conter características comuns (atributos com mesmo nome e mé-
todos com mesmo nome e mesmo conjunto de atributos). Perry e Kaiser destacam que,
embora a herança múltipla leve a pequenas mudanças sintáticas, ela pode levar a grandes
mudanças semânticas [327]. Alguns dos riscos de defeitos causados por herança múltipla
são [35]:
Assim sendo, herança pública e privada, classes abstratas versus classes concretas e a
visibilidade dos dados membros da superclasse constituem os riscos de defeitos associados
com a herança múltipla.
2 Em C++, métodos virtuais são aqueles declarados na superclasse que precisam ser redefinidos nas subclasses.
Para ter acesso aos objetos de diferentes classes usando a mesma instrução, os métodos da superclasse que serão
reescritos nas subclasses devem ser declarados virtuais.
6.3. Tipos de defeitos em POO 125
Uma classe abstrata é a que fornece somente uma interface sem nenhuma implementação,
fornecendo um importante suporte para o reuso [35]. O teste de uma classe abstrata só poderá
ser realizado após esta ter sido especializada e uma classe concreta ter sido obtida. Não é
possível criar objetos ou instâncias de classes abstratas. Esse processo pode ser complicado
se um método concreto utiliza um método abstrato para implementar sua funcionalidade.
Classes genéricas, por sua vez, não são necessariamente abstratas, mas também fornecem
um importante suporte para o reuso, uma vez que são a base para a ocorrência do acoplamento
dinâmico. Uma classe genérica permite que sejam declarados atributos e parâmetros que
podem ser instanciados com objetos de tipos específicos. Por exemplo, suponha uma classe
genérica A a partir da qual foram derivadas as classes B e C. Suponha também a existência
de uma classe D derivada a partir de B. Declarando-se um atributo ou parâmetro do tipo
da classe genérica A, esse atributo ou parâmetro pode ser instanciado por qualquer um dos
objetos criados a partir das classes B, C ou D. Segundo Smith e Robson [365], os principais
problemas no teste de classes genéricas são:
não seja testada corretamente. O uso de algum mecanismo de restrições que impeça
que tipos inadequados sejam utilizados pode ser utilizado para reduzir a ocorrência de
defeitos.
Polimorfismo
• Extensibilidade de hierarquias
Existe um problema quando se deseja testar um ou mais métodos com parâmetros
polimórficos. O teste de um método consiste em verificar seu comportamento quando
6.3. Tipos de defeitos em POO 127
Para resolver esse problema e o anterior, Barbey e Strohmeier [22] sugerem o uso de
assertivas (assertions). As pré e pós-condições necessárias para executar cada método são
especificadas em termos de assertivas. Toda vez que um método é sobrescrito/refinado, as
assertivas são também refinadas, mas não podem ser relaxadas. Dessa forma, fica mais fácil
identificar quando objetos polimórficos estão executando métodos de classes indevidas ou
não esperadas.
Containers heterogêneos são estruturas de dados que armazenam componentes que podem
pertencer a várias classes diferentes, da mesma forma que nomes polimórficos.
Entretanto, alguns dos objetos armazenados podem não ter o mesmo conjunto de ope-
rações daquelas pertencentes à superclasse raiz de uma hierarquia. Para permitir que tais
objetos façam uso de todos os seus métodos, é possível fazer a conversão (casting) do objeto
contido na estrutura de dados heterogênea para qualquer classe da hierarquia. Isso pode levar
a dois tipos comuns de falha:
1. um objeto pode estar sendo convertido para uma classe à qual ele não pertence, sendo,
desse modo, incapaz de selecionar uma determinada característica ou invocar um mé-
todo da classe em questão. Esse problema é conhecido como downcasting;
Esses dois tipos de defeito, em geral, não são detectados durante a fase de compilação.
Assim, um cuidado especial deve ser tomado durante o desenvolvimento do conjunto de teste
para evitar que tais defeitos passem despercebidos.
Outros problemas
Além dos problemas apresentados, Binder [35] descreve ainda defeitos relacionados a seqüên-
cias de mensagens e estados dos objetos. O “empacotamento” de métodos dentro de uma
classe é fundamental na programação OO; como resultado, mensagens devem ser executadas
em alguma seqüência, originando a questão: “Quais seqüências de envio de mensagens são
corretas?”
128 Introdução ao Teste de Software ELSEVIER
Objetos são entidades criadas em tempo de execução, cada um podendo conter o próprio
conjunto de atributos em memória, caracterizando seu estado. Cada nova configuração assu-
mida por esse espaço de memória caracteriza um novo estado do objeto. Assim sendo, além
do comportamento encapsulado por um objeto por meio de seus métodos e atributos, objetos
também encapsulam estados.
Segundo McDaniel e McGregor [277] existem duas definições de estado: normal e basea-
da em projeto. A definição normal de estado refere-se a todas as possíveis combinações de
valores que os atributos de um objeto podem receber, caracterizando, em geral, um conjunto
de estados infinito. Por outro lado, estados baseados em projeto referem-se ao conjunto de
valores dos atributos que permitem claramente diferenciar e determinar o comportamento do
objeto que está sendo observado. Por exemplo, para ilustrar ambos os conceitos, considere
uma pilha de n elementos. Na visão tradicional, que considera um estado em separado para
cada atributo de dado, a pilha deveria ter n estados. Portanto, para se testar n possíveis esta-
dos seriam necessários n casos de teste. Considerando agora uma representação de estados
baseada em projeto, seria suficiente considerar os estados de pilha cheia, pilha vazia e, por
exemplo, o estado pilha com mais de um elemento e não cheia. Com isso, o número de
estados para representar uma pilha seria reduzido de n para três [277].
Ao examinar como a execução de um método pode alterar o estado de um objeto, quatro
possibilidades são observadas [277]: 1) ele pode levar o objeto a um novo estado válido; 2)
ele pode deixar o objeto no mesmo estado em que se encontra; 3) ele pode levar o objeto para
um estado indefinido; ou 4) ele pode alterar o estado para um estado não apropriado.
As opções 3 e 4 caracterizam estados de erro. A opção 2 pode caracterizar um erro se
o método executado devia ter se comportado como na opção 1. A opção 1 também pode
caracterizar um erro se a execução do método devia ter o comportamento da opção 2.
Por definição, uma classe engloba um conjunto de atributos e métodos que manipulam
esses atributos. Assim, considerando uma única classe, já é possível pensar-se em teste de
integração. Métodos da mesma classe podem interagir entre si para desempenhar funções
específicas que caracterizam uma integração entre métodos que deve ser testada: teste in-
termétodo [169]. No paradigma procedimental esta fase de teste também pode ser chamada
de teste interprocedimental.
Harrold e Rothermel [169] definem ainda outros dois tipos de teste para POO: teste in-
traclasse e teste interclasse. No teste intraclasse são testadas interações entre métodos públi-
cos fazendo-se chamadas a esses métodos em diferentes seqüências. O objetivo é identificar
possíveis seqüências de ativação de métodos inválidas que levem o objeto a um estado in-
consistente. Segundo os autores, como o usuário pode invocar seqüências de métodos públi-
6.5. Estratégias, técnicas e critérios de teste OO 131
cos em uma ordem indeterminada, o teste intraclasse aumenta a confiança de que diferentes
seqüências de chamadas interagem adequadamente. No teste interclasse o mesmo conceito
de invocação de métodos públicos em diferentes seqüências é utilizado; entretanto, esses
métodos públicos não necessitam estar na mesma classe.
Finalmente, após realizados os testes acima, o sistema todo é integrado e podem ser rea-
lizados os testes de sistema que, por se basear em critérios funcionais, não apresentam dife-
renças fundamentais entre o teste procedimental e orientado a objetos.
Pequenas variações quanto à divisão das fases de teste para POO são identificadas na
literatura. Por exemplo, Colanzi [88] caracteriza a fase do teste de classe, cuja finalidade é
descobrir defeitos de integração entre os métodos dentro do escopo da classe em teste, e a
fase do teste de integração para software orientado a objetos, que tem o objetivo de encontrar
defeitos na integração de classes do sistema. Assim, segundo Colanzi [88], o teste de POO é
organizado em quatro fases:
Considerando-se o método como a menor unidade, o teste de classe, proposto por Co-
lanzi [88], pode ser visto como parte do teste de integração, juntamente com o teste intraclasse
e interclasse.
Alguns autores entendem que a classe é a menor unidade no paradigma de programa-
ção OO [17, 35, 277, 279, 327]. Nessa direção o teste de unidade poderia envolver o teste
intramétodo, intermétodo e intraclasse, e o teste de integração corresponderia ao teste in-
terclasse. Nesse contexto, o teste de classe proposto por Colanzi seria enquadrado como teste
de unidade.
Na Tabela 6.1 são sintetizados os tipos de teste que podem ser aplicados em cada uma
das fases tanto em programas procedimentais quanto em POO, considerando o método ou a
classe a menor unidade.
embora em menor número, algumas iniciativas também são encontradas [34, 35, 67, 69, 169,
198, 213, 257, 364, 370, 420].
A seguir é dada uma descrição mais detalhada a respeito de alguns desses trabalhos, des-
crevendo e exemplificando critérios e estratégias de teste desenvolvidos para o teste de POO.
Inicialmente são descritos brevemente os critérios funcionais e posteriormente são descritos
os critérios estruturais que podem ser utilizados nesse contexto. A estratégia de teste incre-
mental hierárquica também é descrita por enfatizar o reuso de casos de testes que pode ser
obtido por meio do uso de herança.
Como mencionado antes, devido ao caráter dinâmico dos objetos e de sua capacidade de ar-
mazenar estado interno, a modelagem do comportamento dos objetos pode ser feita por meio
de um diagrama de estados. Nesse sentido, as técnicas e os critérios de teste identificados
no Capítulo 3 são diretamente aplicáveis nesse contexto. O mesmo ocorre com os critérios
de teste funcionais, descritos no Capítulo 2, que, por serem independentes do paradigma de
programação, também são diretamente aplicáveis no teste de POO.
No contexto de teste de especificação de POO, o que tem também sido investigado é o
desenvolvimento de critérios de teste que utilizam diferentes tipos de diagramas utilizados
no projeto orientado a objetos para auxiliar na geração de casos de testes. Por exemplo,
Chaim et al. [62] definiram uma série de critérios de teste para realizar teste de cobertura em
especificações UML, mais especificamente em diagramas de casos de uso. Os critérios de
teste propostos por Chaim et al. almejam assegurar que casos de testes garantam a cobertura
dos casos de usos utilizados para modelar sistemas orientados a objetos. Uma das idéias
básicas dos critérios é assegurar que as interações entre casos de usos e entre atores e casos
de usos sejam exercitadas, contribuindo para uma especificação mais precisa do problema
que está sendo modelado. Além do trabalho de Chaim et al. [62], outros trabalhos nessa
mesma linha podem ser citados, como os de Abdurazik e Offutt [1], Beckman e Grupta [29],
Heumann [181], Offutt e Abdurazik [305], entre outros.
6.5. Estratégias, técnicas e critérios de teste OO 133
A técnica de teste estrutural também apresenta uma série de limitações e desvantagens de-
correntes das limitações inerentes às atividades de teste de programa, como estratégia de
validação, tais como a determinação de caminhos e associações não executáveis [141, 191,
261, 302, 337]. Esses aspectos introduzem sérias limitações na automatização do processo
de validação de software [261]. Independente dessas desvantagens, essa técnica é vista como
complementar à técnica funcional [329], e informações obtidas pela aplicação desses critérios
têm sido consideradas relevantes para as atividades de manutenção, depuração e confiabili-
dade de software [172, 166, 320, 329, 402, 403].
A definição desses critérios foi feita originalmente para o teste de programas procedimen-
tais, mas vem sendo estendida ao longo dos anos para se adequar ao teste de POO.
Harrold e Rothermel [169] estenderam o teste de fluxo de dados para o teste de classes.
Os autores comentam que os critérios de fluxo de dados destinados ao teste de programas
procedimentais [144, 170, 337] podem ser utilizados tanto para o teste de métodos indivi-
duais quanto para o teste de métodos que interagem entre si dentro de uma mesma classe.
Entretanto, esses critérios não consideram interações de fluxo de dados quando os usuários
de uma classe invocam seqüência de métodos em uma ordem arbitrária.
Para viabilizar o teste de fluxo de dados nos níveis intramétodo, intermétodo e intraclasse,
Harrold e Rothermel [169] propuseram as seguintes representações de programa: Grafo de
Chamadas de Classe (class call graph), Grafo de Fluxo de Controle de Classe (CCFG - class
control flow graph) e o CCFG encapsulado (framed CCFG). Com base nessas representações,
os três níveis de teste foram considerados:
• teste intermétodo – testa os métodos públicos em conjunto com outros métodos dentro
de uma mesma classe. Esse nível de teste é equivalente ao teste de integração de
programas procedimentais;
• teste intraclasse – testa a interação entre métodos públicos quando eles são chamados
em diferentes seqüências. Como os usuários de uma classe podem invocar seqüências
de métodos em uma ordem indeterminada, o teste intraclasse serve para aumentar a
134 Introdução ao Teste de Software ELSEVIER
Com base nesses níveis de teste, Harrold e Rothermel [169] definiram pares definição e
uso que permitem avaliar relações de fluxo de dados em POO. Seja C uma classe em teste.
Se d representa um comando contendo uma definição e u um comando contendo um uso de
uma variável, seguem-se estas definições:
Programa 6.1
1 class TabelaSimbolo {
2 private:
3 TabelaEntrada *tabela;
4 int numeroentradas, maximoentradas;
5 int *Procurar(char *, int);
6 public:
7 TabelaSimbolo(int n) {
8 maximoentradas = n;
9 numeroentradas = 0;
10 tabela = new TabelaEntrada[maximoentradas]; };
11 TabelaSimbolo() { delete tabela; };
12 int AdicionanaTabela(char *simbolo, char *info);
13 int ObterdaTabela(char *simbolo, char *info);
14 };
*
6.5. Estratégias, técnicas e critérios de teste OO 135
Programa 6.2
1 #include "simbolo.h"
2
3 int TabelaSimbolo::Procurar(char *chave, int indice) {
4 int guardaindice;
5 int Hash(char *);
6 guardaindice = indice = Hash(chave);
7 while (strcmp(ObterSimbolo(indice),chave) != 0) {
8 indice++;
9 if (indice == maximoentradas) /* wrap around */
10 indice = 0;
11 if (ObterSimbolo(indice)==0 || indice==guardaindice)
12 return NAOACHOU;
13 }
14 return ACHOU;
15 }
16
17 int TabelaSimbolo::AdicionanaTabela(char *simbolo, char *info ) {
18 int indice;
19 if (numeroentradas < maximoentradas) {
20 if (Procurar(simbolo,indice) == ACHOU)
21 return NAOOK;
22 AdicionaSimbolo(simbolo,indice);
23 AdicionaInfo(info,indice);
24 numeroentradas++;
25 return OK;
26 }
27 return NAOOK;
28 }
29
30 int TabelaSimbolo::ObterdaTabela(char *simbolo, char **info) {
31 int indice;
32 if (Procurar(simbolo,indice) == NAOACHOU)
33 return NAOOK;
34 *info = GetInfo(indice);
35 return OK;
36 }
37
38 void TabelaSimbolo::AdicionaInfo(info,indice)
39 ...
40 strcpy(tabela[indice].info,info);
41 }
42
43 char *TabelaSimbolo::GetInfo(indice)
44 ...
45 return tabela[indice].info;
46 }
*
Informalmente, o teste intramétodo permite testar pares def-uso dentro de um único mé-
todo. Por exemplo, considerando a classe TabelaSimbolo (Programa 6.2), o método
Procurar contém um par def-uso intramétodo em relação à variável indice, pois a defi-
nição da variável indice na linha 8 tem um uso na linha 9.
Pares def-uso intermétodo ocorrem quando métodos dentro do contexto de uma invocação
interagem e a definição de uma variável dentro dos limites de um método alcança um uso
dentro dos limites de outro método chamado direta ou indiretamente por um método público.
Considerando a classe TabelaSimbolo, o método público AdicionanaTabela invoca
o método Procurar, o qual define a variável indice, que, posteriormente, é utilizada
136 Introdução ao Teste de Software ELSEVIER
para chamar o método AdicionaSimbolo. Logo, existe um par def-uso intermétodo entre
a variável indice definida na linha 10 de Procurar e o uso de indice na linha 22 de
AdicionanaTabela.
Finalmente, pares def-uso intraclasse ocorrem quando seqüências de métodos públicos
são invocadas. Por exemplo, considere a seqüência de chamadas AdicionanaTabela,
AdicionanaTabela . Na primeira chamada a AdicionanaTabela, se um símbolo
é adicionado à tabela, a variável numeroentradas (linha 24) é incrementada (rede-
finida). Na segunda chamada a AdicionanaTabela a variável numeroentradas, de-
finida anteriormente, é utilizada na condição da linha 19. Então, é definido um par def-uso
intraclasse da variável numeroentradas definida na linha 24 e depois utilizada na li-
nha 19.
Assim sendo, o CCFG é o modelo base para representar o fluxo de dados de uma classe,
facilitando a geração de requisitos de teste intramétodo, intermétodo e intraclasse. A des-
crição detalhada do algoritmo utilizado para a construção do Grafo de Fluxo de Controle de
Classe pode ser encontrada no trabalho de Harrold e Rothermel [169]. Entretanto, para a
identificação dos requisitos de teste interclasse, é necessário o desenvolvimento de outra re-
presentação, que expresse o tipo de interação a ser considerada, o que não foi levantado pelo
trabalho desses pesquisadores quando proposto.
Algumas das limitações da técnica proposta por Harrold e Rothermel [169] incluem:
1) não identificar alguns pares definição-uso intramétodo, intermétodo e intraclasse resul-
tantes de apelidos (aliases) específicos; e 2) não manipular características específicas da pro-
gramação OO, tais como polimorfismo e acoplamento dinâmico. Posteriormente, Rothermel
et al. [349] propuseram uma extensão na construção do CCFG incluindo um nó polimór-
fico específico nos pontos de chamada que conecta o conjunto de métodos polimórficos que
poderiam ser invocados daquele ponto, conjunto esse obtido a partir de análise estática da
hierarquia de classes. Como ressalta Clarke e Malloy [86], mesmo essa abordagem ainda
requer algum refinamento, uma vez que o número de métodos polimórficos identificado pode
ser grande, tornando a construção do grafo impraticável.
Sinha e Harrold [364] desenvolveram uma família de seis critérios de teste destinados
especificamente ao teste do comportamento de construções relacionadas ao tratamento de
exceções da linguagem Java. A definição dos critérios foi baseada em uma representação de
programa conhecida como Grafo de Fluxo de Programa Interprocedimental (ICFG) [363], a
partir da qual os requisitos de teste são derivados. Exceções em Java podem ser classificadas
como sendo síncronas ou assíncronas. Uma exceção síncrona ocorre em um ponto particular
do programa e é causada durante a avaliação de uma expressão, da execução de um comando
ou pela execução explícita de um comando throw. Uma exceção assíncrona pode ocorrer
de forma arbitrária, em qualquer ponto do programa. A principal limitação na construção
do ICFG é que ele não representa exceções assíncronas nem aquelas que podem ser geradas
implicitamente, ou seja, o algoritmo para a construção do ICFG realiza uma busca local nas
instruções de cada método para determinar os tipos de exceções que possam ser gerados
explicitamente por meio de um comando throw.
Considerando o teste intramétodo, Vincenzi et al. [420] revisitaram diversos trabalhos na
definição de critérios de teste de fluxo de controle e de dados que permitem a derivação de
requisitos de testes a partir de código objeto Java (Java bytecode). A principal motivação
para trabalhar nesse nível é viabilizar o teste estrutural não somente para programas Java
tradicionais, mas também permitir o teste estrutural de componentes Java para os quais, em
6.5. Estratégias, técnicas e critérios de teste OO 137
geral, nem sempre o código-fonte está disponível. Além da definição dos critérios de teste,
Vincenzi et al. [422] também implementaram uma ferramenta de teste, denominada JaBUTi
(Java Bytecode Understanding and Testing), que automatiza a aplicação dos critérios.
Para ilustrar os critérios de teste definidos por Vincenzi et al. [420], considere o programa
Java apresentado na Figura 6.4(a). A classe Vet contém um método Vet.average() que
calcula e retorna a média dos números armazenados em um vetor de inteiros. As instruções
de bytecode que representam o método Vet.average() são apresentadas na Figura 6.4(b).
Cada instrução de bytecode é precedida de um contador de programa (cp) que identifica uni-
camente cada instrução em um dado método. No exemplo, a primeira instrução de bytecode,
aload_0, é indicada pelo cp 0, e a última, freturn, pelo cp 101.
A Figura 6.4(c) ilustra a tabela de exceções do método Vet.average(). A tabela
indica, para cada segmento de instruções de bytecode (colunas from e to), o início (coluna
target) de cada tratador de exceção válido, bem como o tipo de exceção que é tratada pelo
tratador (coluna type). Por exemplo, do cp 12 ao cp 54 o tratador de exceção válido começa
no cp 60 e é responsável por tratar qualquer exceção da classe java.lang.Exception
ou qualquer uma de suas subclasses. Um tratador de exceção que esteja registrado como
sendo do tipo Class all é responsável por tratar quaisquer exceções que sejam geradas
dentro de seu escopo de atuação.
As instruções de bytecode podem ser relacionadas com linhas do código-fonte, pois o ar-
quivo .class armazena informações que permitem mapear cada instrução de bytecode para
a linha de código-fonte que lhe deu origem. Com isso, se o código-fonte estiver disponível, as
análises realizadas no bytecode podem ser mapeadas de volta para o código-fonte. A tabela
de números de linha (line number table), ilustrada na Figura 6.4(d), fornece tal correspondên-
cia. Por exemplo, o comando localizado na linha número 06 do código-fonte corresponde às
instruções de bytecode do cp 0 ao cp 2. As instruções de bytecode do cp 5 ao 7 correspondem
ao comando da linha 07 no código-fonte, e assim por diante.
O chamado Grafo de Instruções (GI), ilustrado na Figura 6.4(e), representa o fluxo de
controle das instrução de bytecode do método Vet.average(). Cada nó do grafo corres-
ponde a uma única instrução de bytecode. Os nós do GI são numerados de acordo com o
número do contador de programa (cp) das respectivas instruções de bytecode.
Dois tipos diferentes de arestas são utilizados para conectar as instruções de bytecode: 1)
arestas regulares (linhas contínuas), considerando a existência de transferência de controle
entre cada instrução; e 2) arestas de exceção (linhas pontilhadas), considerando a tabela de
exceções. Os desvios condicionais são identificados a partir da semântica das instruções de
bytecode responsáveis por tais desvios. Também por meio da análise semântica das instru-
ções de bytecode é possível identificar quais delas são responsáveis por causar uma definição
e/ou um uso de variável e, conseqüentemente, dar origem aos conjuntos def(i) e uso(i),
correspondentes aos conjuntos de variáveis definidas e usadas em cada nó i do grafo GI, res-
pectivamente. Para mais informações, pode ser consultado o trabalho de Vincenzi et al. [420].
Uma vez que cada instrução de bytecode corresponde a um único nó em GI, o número
de nós e arestas é relativamente grande, mesmo para métodos com poucas linhas de código-
fonte. No caso do GI apresentado na Figura 6.4(e), alguns nós foram omitidos para reduzir
o tamanho do GI e melhorar a sua legibilidade. Reticências (“...”) foram utilizadas para
representar os nós omitidos.
138 Introdução ao Teste de Software ELSEVIER
Com base no GDU, um conjunto de oito critérios de teste foi definido por Vincenzi et
al. [420]. As Tabelas 6.2 e 6.3 descrevem de forma sucinta tais critérios.
140 Introdução ao Teste de Software ELSEVIER
Tabela 6.4 – Conjunto de requisitos de teste estruturais derivados pelos critérios de fluxo de
controle para o método Vet.average()
Critério Requisitos de Teste
Todos-Nósei {0, 15, 34, 43, 54, 54.82, 91, 97}
Todos-Nós
Todos-Nósed {60, 60.82, 74, 74.82, 79 }
{(0,34), (15,34), (34,15), (34,43), (43,54), (54,54.82), (54.82,91),
Todas-Arestasei
(91,97)}
Todas-Arestas {(15,60), (15,74), (34,60), (34,74), (43,60), (43,74), (54,74),
Todas-Arestased (60,60.82), (60,74), (60.82,91), (74,74), (74,74.82), (74.82,79)}
Tabela 6.5 – Conjunto de requisitos de teste estruturais derivados pelo critério Todos-Usos
para o método Vet.average()
Critério Requisitos de Teste
0, 15, L@0 0, (34, 15), L@0 0, (34, 43), L@0
0, 43, L@0 0, 54.82, L@0 0, 91, L@0
0, 97, L@0 0, 43, L@0.out 15, 43, L@0.out
43, 97, L@0.out 0, 15, L@0.v 0, (34, 15), L@0.v
Todos-Usosei
0, (34, 43), L@0.v 0, 15, L@0.v[] 0, 15, L@2
Todos-Usos
15, (34, 15), L@2 0, (34, 15), L@2 15, (34, 43), L@2
0, (34, 43), L@2 0, 43, L@2 15, 43, L@2
0, 91, L@2 15, 91, L@2
0, 60, L@0 0, 60.82, L@0 0, 74.82, L@0
Todos-Usosed
60, 97, L@0.out 60, 91, L@2 74, 79, L@4
Estratégia incremental
Para atingir uma alta cobertura dos requisitos de teste é possível utilizar um conjunto de teste
gerado em diversas fases, considerando uma estratégia incremental de aplicação dos critérios
de teste. Com isso, o testador pode se concentrar em diferentes aspectos do programa que
142 Introdução ao Teste de Software ELSEVIER
Tabela 6.6 – Conjunto de requisitos de teste estruturais derivados pelo critério Todos-
Potenciais-Usos para o método Vet.average()
Critério Requisitos de Teste
0, 15, L@0 0, (34, 15), L@0 0, (34, 43), L@0
0, 43, L@0 0, 54, L@0 0, 54.82, L@0
0, 91, L@0 0, 97, L@0 0, 15, L@0.out
0, (34, 15), L@0.out 15, (34, 15), L@0.out 0, (34, 43), L@0.out
15, (34, 43), L@0.out 15, 43, L@0.out 0, 43, L@0.out
43, 54, L@0.out 43, 54.82, L@0.out 43, 91, L@0.out
43, 97, L@0.out 0, 15, L@0.v 0, (34, 15), L@0.v
0, (34, 43), L@0.v 0, 43, L@0.v 0, 54, L@0.v
Todos-
0, 54.82, L@0.v 54.82, 91, L@0.v 54.82, 97, L@0.v
Pot-
Usosei 0, 15, L@1 0, (34, 15), L@1 0, (34, 43), L@1
0, 43, L@1 0, 54, L@1 0, 54.82, L@1
0, 91, L@1 0, 97, L@1 0, 15, L@2
15, (34, 15), L@2 0, (34, 15), L@2 15, (34, 43), L@2
0, (34, 43), L@2 15, 43, L@2 0, 43, L@2
15, 54, L@2 0, 54, L@2 15, 54.82, L@2
0, 54.82, L@2 15, 91, L@2 0, 91, L@2
15, 97, L@2 0, 97, L@2 54.82, 91, L@5
54.82, 97, L@5
Todos- 0, 60, L@0 0, 60.82, L@0 0, 74, L@0
Pot-Usos 0, 74.82, L@0 0, 79, L@0 15, 60, L@0.out
0, 60, L@0.out 43, 60, L@0.out 60, 60.82, L@0.out
60, 74, L@0.out 15, 74, L@0.out 43, 74, L@0.out
0, 74, L@0.out 15, 74.82, L@0.out 0, 74.82, L@0.out
43, 74.82, L@0.out 60, 74.82, L@0.out 60, 79, L@0.out
15, 79, L@0.out 0, 79, L@0.out 43, 79, L@0.out
60, 91, L@0.out 60, 97, L@0.out 0, 60, L@0.v
0, 60.82, L@0.v 0, 74, L@0.v 0, 74.82, L@0.v
Todos-
74.82, 79, L@0.v 60.82, 91, L@0.v 60.82, 97, L@0.v
Pot-
Usosed 0, 60, L@1 0, 60.82, L@1 0, 74, L@1
0, 74.82, L@1 0, 79, L@1 15, 60, L@2
0, 60, L@2 60, 60.82, L@2 60, 74, L@2
15, 74, L@2 0, 74, L@2 15, 74.82, L@2
0, 74.82, L@2 60, 74.82, L@2 60, 79, L@2
15, 79, L@2 0, 79, L@2 60, 91, L@2
60, 97, L@2 60, 60.82, L@3 60, 74, L@3
60, 74.82, L@3 60, 79, L@3 60, 91, L@3
60, 97, L@3 74, 74.82, L@4 74, 79, L@4
74.82, 79, L@5 60.82, 91, L@5 60.82, 97, L@5
está sendo testado e gerenciar mais facilmente os recursos de tempo e custo em função da
qualidade dos testes desejada.
A estratégia definida a seguir é fundamentada na relação de inclusão entre os critérios
de teste. O objetivo é aplicar um critério de teste menos rigoroso e de menor custo e, à
medida que as restrições de tempo e custo permitirem, considerar a aplicação de critérios
mais rigorosos visando a evoluir o conjunto de teste e aumentar a confiança de que o software
se comporte de acordo com o especificado. Na definição da estratégia, os seguintes conjuntos
de teste são considerados:
Além disso, dependendo do tipo de programa que está sendo testado ou até dos objetivos
de teste desejados, o testador pode escolher aplicar, em um primeiro momento, somente os
critérios independentes de exceção. Posteriormente, conforme as restrições de tempo e custo,
os critérios dependentes de exceção também podem ser aplicados, seguindo os passos da
estratégia.
Observa-se que, em geral, a obtenção de conjuntos de teste adequados requer a identifi-
cação de possíveis requisitos de teste não executáveis, tarefa essa a ser realizada em todas as
etapas que exigem a geração de conjuntos de teste adequados com relação a algum critério
de teste. Formulários específicos devem ser preenchidos durante e ao final de cada etapa para
anotar os dados pertinentes.
Como pode ser observado, os critérios de teste definidos até o momento são destinados
ao teste intramétodo, sendo que, nesse nível de teste, o teste de POO não é essencialmente
diferente do teste de programas procedimentais. Entretanto, os testes intramétodos são de
fundamental importância para assegurar que a lógica de cada método esteja correta. Os de-
mais problemas decorrentes do uso das características de orientação a objetos estão mais
relacionados a defeitos de integração e, a seguir, são descritos sucintamente alguns trabalhos
nesse contexto.
3. apenas aproximadamente metade dos métodos utilizados pelas classes clientes de fato
alteram o estado do objeto utilizado;
4. manipulações de variáveis de tipos primitivos tipicamente utilizadas como base no teste
de fluxo de dados ocorrem poucas vezes em programas OO; e
Esses resultados sugerem que o teste baseado em fluxo de controle pode não ser a maneira
mais efetiva de se revelarem os comportamentos de um programa OO. Além disso, Souter e
Pollock [369] argumentam que as abordagens de fluxo de dados, como, por exemplo, a de
Harrold e Rothermel [169] discutida anteriormente, não atingem o objetivo principal do teste
de fluxo de dados OO de levar em conta as manipulações de objetos. Com isso, as asso-
ciações de fluxo de dados obtidas a partir dos critérios de Harrold e Rothermel [169] seriam
classificadas como sendo livres de contexto (context-free), uma vez que podem ser cobertas
sem considerar um contexto específico.
A partir daí, é proposta uma abordagem de teste baseada em manipulações de objetos
utilizando a análise de ponteiros e escape proposta por Whaley e Rinard [432] cujo objetivo
é: 1) caracterizar como variáveis locais e atributos referenciam outros objetos; e 2) determi-
nar como objetos alocados em uma região do programa podem escapar e ser acessados por
outras regiões. Souter e Pollock [370] utilizam esse tipo de informação, estendendo o grafo
points-to escape para um grafo Annotated Points-to Escape (APE) a fim de caracterizarem
pares Def-Uso contextuais, agregando contexto às definições e usos dos objetos. As defi-
nições e os usos de objetos são baseados nas modificações e nos acessos aos atributos dos
objetos. Para permitir a geração de cdus, informações adicionais sobre a localização e o tipo
de manipulação (definição (store) ou uso (load)) realizada no objeto também são incluídas
no grafo. Segundo as autoras, o teste baseado em associações definição-uso contextuais pode
melhorar a cobertura dos testes, uma vez que múltiplas associações definição-uso contex-
tuais únicas podem ser geradas para uma mesma associação definição-uso livre de contexto.
Ao todo, quatro níveis de contexto foram propostos, identificados por cdu-0, cdu-1, cdu-2
e cdu-3. Quanto maior o nível de contexto, maior o conjunto de associações definição-uso
contextuais requeridas e, conseqüentemente, maiores os requisitos computacionais exigidos
para calculá-las.
Um par Def-Uso contextual é definido como uma tupla (o, def, uso) para um objeto o em
que def e uso são definidos como uma seqüência de chamadas (CSom −CS1 −...−CSm −L)
na qual CSom é o local da primeira chamada a método que leva a modificação (referência)
do (ao) estado do objeto o. L é o local da instrução de atribuição (leitura) em si do dado,
alterando o estado modificado (referenciado) do objeto. Cada CSi na seqüência interna é
o local de chamada em uma seqüência de chamadas que leva do local da chamada original,
CSom , à atribuição (leitura).
Essas seqüências de chamadas é que configuram contextos para cada associação Def-
Uso, enquanto as associações definidas nas abordagens anteriores apenas consideravam a
definição e o uso em qualquer lugar, sem considerar as seqüências de chamadas a métodos
específicas que podem levar à execução da associação. É por esse fato que cada associação
livre de contexto pode dar origem a mais de uma associação contextual. Além disso, essa
abordagem leva em conta o polimorfismo porque as chamadas polimórficas são tratadas na
construção dos grafos APE (em cada ponto de chamada, todos os grafos APE dos métodos
potencialmente chamados são juntados ao APE da região em análise).
Entretanto, Clarke e Malloy [86] ressaltam que a abordagem proposta não considera
associações definição-uso contextuais envolvendo variáveis de tipo primitivo. Além disso,
conforme destacado por Souter e Pollock [369, 370], em programas que contenham longas
cadeias de chamadas e métodos com múltiplas chamadas para o mesmo método é possível
atingir um número de anotações com tamanho exponencial, considerando um APE com ano-
tações precisas. A solução adotada pelas autoras foi, então, limitar o tamanho das anotações
146 Introdução ao Teste de Software ELSEVIER
de modo que a construção do APE tivesse uma complexidade da ordem de O(n4 ), sendo n
o número de nós do APE e, mesmo assim, na condução de alguns experimentos com progra-
mas da ordem de 7.500 a 9.700 linhas de código (LOC), a quantidade de memória requerida
para computar o conjunto de associações definição-uso contextuais cdu-2 e cdu-3 não foi
suficiente.
Independente da fase ou do critério de teste utilizado, a atividade de teste é considerada de
alto custo, e reduzir esses custos é de fundamental importância para promover um incentivo
na adoção de uma estratégia de teste de baixo custo e alta eficácia em revelar defeitos nos pro-
dutos de software. Com a finalidade de reduzir parte dos custos decorrentes da atividade de
testes em POO, Harrold e Rothermel propuseram a chamada Estratégia de Teste Incremental
Hierárquica (HIT). Tal estratégia é descrita a seguir detalhadamente, pois considera-se que
fornece dicas úteis que favorecem a reutilização de casos de testes quando POO fazem uso
de herança.
Harrold et al. [168] definiram uma estratégia para reaproveitar conjuntos de casos de teste
adequados ao teste da superclasse para testar características herdadas nas subclasses, redu-
zindo os custos da atividade de teste.
Como descrito anteriormente, a herança é um mecanismo que permite tanto o compar-
tilhamento da especificação da classe como o do código-fonte para o desenvolvimento de
novas classes baseadas em classes já existentes. A definição de uma subclasse é dada por
um modificador que estabelece as diferenças ou alterações nos elementos3 que compõem
a superclasse. Com isso, um modificador e a superclasse são utilizados na criação de uma
subclasse. A Figura 6.7 ilustra como transformar uma superclasse P com um modificador M
em uma subclasse R. O operador de composição ⊗ une simbolicamente M e P , produzindo
R, sendo R = P ⊗ M .
mente, sofrer alguma alteração na relação de herança, tal como atributos, métodos ou até mesmo a lista de parâmetros
formais de determinado método.
6.5. Estratégias, técnicas e critérios de teste OO 147
A Figura 6.8 ilustra alguns dos tipos de elementos. A classe P tem dois atributos inteiros,
i e j, e métodos A, B e C. B é um método virtual. O modificador para a classe R contém
um atributo do tipo real, i, e três métodos: A, B e C. Combinando-se o modificador com
a classe P, a classe R é obtida. O atributo float i é um elemento novo em R, visto que
ele não aparece em P. O método A, que está definido em M, é um elemento novo em R, uma
vez que a lista de atributos não é igua à lista do A em P. O método A de P é recursivo em
R, pois ele é herdado sem modificações a partir de P. Assim sendo, R contém dois métodos
A. O método B é virtual em P e, por ser redefinido em M, ele é virtual-redefinido em R. O
método C é redefinido em R, visto que sua implementação foi alterada por M, sobrescrevendo
o método C de P. Finalmente, os atributos i e j de P são herdados mas estão ocultos em R,
o que significa que eles não podem ser utilizados pelos métodos definidos pelo modificador.
A proposta consiste em testar inicialmente a superclasse considerando cada método indi-
vidualmente e então testar a interação entre os métodos. Os casos de teste e as informações
da execução dos testes na superclasse são salvos, caracterizando uma história de teste (testing
history). Quando uma subclasse é definida, a história de teste de sua superclasse bem como
o modificador são utilizados para determinar quais elementos devem ser testados (ou retes-
tados) na subclasse e quais casos de testes da superclasse podem ser reaproveitados no teste
da subclasse. Segundo Harrold et al., essa estratégia é hierárquica por ser guiada pela ordem
da relação de hereditariedade, e é incremental porque usa o resultado obtido em um nível de
teste na hierarquia para reduzir o esforço necessário em níveis subseqüentes [168].
148 Introdução ao Teste de Software ELSEVIER
private:
float i; // novo
public:
void A(int a, int b) // recursivo
{i=a;j=a+2*b;}
void A(int A) // novo
{P::A(a,0);}
virtual int B() // virtual redefinido
{return 3*P::B();}
int C() // redefinido
{return 2*P::C();}
Ocultos
int i;
int j;
O teste da classe base inicia-se com o teste dos métodos (intramétodos), utilizando-se técnicas
tradicionais de fluxo de dados. O teste individual de cada método é de fundamental impor-
tância, tendo em vista que a subclasse espera que o método herdado funcione corretamente.
Nesse sentido, Harrold et al. destacam a importância de se utilizarem critérios funcionais e
estruturais para assegurar uma melhor qualidade aos testes intramétodo. A história do teste
consiste em uma tripla (m, (TFi , teste?), (TEi , teste?)) em que m é o método, TFi
é o conjunto de teste baseado em especificação, TEi o conjunto de teste baseado em programa
e teste? indica se o conjunto de teste deve ou não ser (re)executado. O campo teste?
pode ter três respostas diferentes, S indica que todo o conjunto de testes é reusado, P indica
que somente os casos de teste identificados como partes afetadas do teste da subclasse são
reusados e N indica que o conjunto de testes não é reusado.
Além do teste intramétodo também devem ser realizados os testes intraclasse e interclasse.
O teste intraclasse procura testar as interações entre os métodos de uma mesma classe, e o
teste interclasse, a interação entre métodos de classes diferentes.
6.5. Estratégias, técnicas e critérios de teste OO 149
Programa 6.3
1 class Forma {
2 private:
3 Ponto ponto_referência;
4
5 public:
6 void colocar_ponto_referência(Ponto);
7 point obter_ponto_referência();
8 void mover(Ponto);
9 void apagar();
10 virtual void desenhar() = 0;
11 virtual float área();
12 forma(Ponto);
13 forma();
14 }
*
A classe Forma é uma classe abstrata que pode ser utilizada na criação de diversas classes
que representem o comportamento de figuras. Forma inclui duas funções membro virtuais,
área() e desenhar(), contribuindo para o fornecimento de uma interface de acesso
comum entre todas as subclasses na hierarquia de herança. A função desenhar() é uma
função membro virtual definida com um valor inicial 0 e sem implementação. Embora não
presente no Programa 6.3, o método área() tem uma implementação inicial que pode ser
mudada numa subseqüente declaração de subclasse.
Os métodos colocar_ponto_referência() e obter_ponto_referência()
fornecem acesso controlado aos dados; os métodos forma(Ponto) e forma() são cons-
trutores da classe; e o método mover() muda o ponto de referência da figura. Esse método
pode ser implementado em função dos outros métodos já definidos. Finalmente, o método
apagar() pode ser implementado de várias formas. Uma delas seria chamar o método
desenhar() no modo “xor”, ou seja, desenhar novamente a figura com a cor do fundo.
A Tabela 6.8 ilustra a história do teste para a Forma. Por se tratar da classe base, é
necessário testar todos os métodos e todas as interações intraclasse e interclasse. Observa-
150 Introdução ao Teste de Software ELSEVIER
Teste da subclasse
Testada a superclasse e armazenada a história do teste, dá-se início ao teste das subclasses. A
idéia é reaproveitar tanto quanto possível os casos de teste utilizados no teste da superclasse.
Tal reaproveitamento é obtido de acordo com o tipo de elemento herdado pela subclasse.
Dada uma superclasse P , a história do teste da subclasse é construída em função da histó-
ria do teste da classe pai (H(P )), do grafo da classe pai (G(P )) e de um modificador (M ). O
algoritmo TestSubClass (apresentado no Programa 6.4) detalha o processo de construção da
história do teste da subclasse. Dados H(P ), G(P ) e M , o algoritmo produz H(R) (história
do teste da subclasse R) e G(R) (Grafo de Classe da subclasse R).
As ações de TestSubClass dependem do tipo do elemento e das modificações que ele
recebeu no mapeamento de herança. Para cada tipo de elemento (novo, recursivo, redefinido,
virtual-novo, virtual-recursivo e virtual-redefinido) diferentes ações podem ocorrer.
Todo atributo (ou método) NOVO ou VIRTUAL-NOVO deve ser totalmente testado, já
que foi definido em M e ainda não foi testado. Um atributo (ou método) RECURSIVO
ou VIRTUAL-RECURSIVO requer um reteste limitado, pois já foi testado em P e tanto
sua especificação quando sua implementação não foram alteradas. É necessário testar, por
exemplo, o caso em que um método recursivo acessa um método redefinido. Um atributo (ou
método) REDEFINIDO ou VIRTUAL-REDEFINIDO exige reteste, mas os casos de teste
baseados em especificação podem ser reusados, se possível.
Programa 6.4
1 Algoritmo TestSubClass (H(P), G(P), M);
2 entrada: H(P): história de teste da classe P;
3 G(P): grafo da classe P;
4 M: modificador que especifica a subclasse R;
5 saída: H(R): história de teste de R indicando
6 o que deve ser executado novamente;
7 G(R): grafo para a subclasse R;
8 begin
9 H(R):= H(P); /* inicia a história de R a partir da de P*/
10 G(R):= G(P); /* inicia o grafo da classe R a partir do de P*/
11 for each A contido em M do
12 case A seja NOVO ou VIRTUAL-NOVO:
13 Gerar TS, TP para A;
14 Adicionar { A, (TS, Y), (TP, Y)} a H(R);
15 Integrar A em G(R);
16 Gerar TIS e TIP para A ;
17 Adicionar {A, (TIS, Y), (TIP, Y)} a H(R);
18 case A seja RECURSIVO ou VIRTUAL-RECURSIVO:
19 if A acessa dados no escopo de R then
20 Identificar testes de interface reusáveis;
21 Adicionar {(TIS, P) e (TIP,P)} a H(R);
22 case A seja REDEFINIDO ou VIRTUAL-REDEFINIDO:
23 Gerar TP para A;
24 Reusar TS de P se ele existe ou Gerar TS para A;
25 Adicionar {A, (TS, Y), (TP, Y)} a H(R);
26 Integrar A em G(R);
27 Gerar TIP para G(R) em relação a A;
28 Reusar TIS de P;
29 Adicionar {A, (TIS, P), (TIP, P)} a H(R);
30 end TestSubClass.
*
152 Introdução ao Teste de Software ELSEVIER
Programa 6.5
1 class Triângulo: public Forma {
2 private:
3 point Vértice2;
4 point Vértice3;
5
6 public:
7 point obter_vértice1(); // novo
8 point obter_vértice2(); // novo
9 point obter_vértice3(); // novo
10 void colocar_vértice1(); // novo
11 void colocar_vértice2(); // novo
12 void colocar_vértice3(); // novo
13 void desenhar(); // virtual-redefinido
14 float área(); // virtual-redefinido
15 triângulo(); // novo
16 triângulo(Ponto, Ponto, Ponto); // novo
17 }
*
Programa 6.6
1 class TriânguloEquilátero: public Triângulo {
2 public:
3 float área(); // redefinido
4 TriânguloEquilátero(); // novo
5 TriânguloEquilátero(Ponto,Ponto,Ponto); // novo
6 }
*
Para avaliar os resultados obtidos com a utilização da estratégia incremental hierárquica, Har-
rold et al. apresentam um estudo de caso que utiliza a hierarquia de classes de uma biblioteca
de interfaces gráfica (Interviews) implementada em C++. Essa hierarquia é composta
pela superclasse Interactor e suas subclasses Scene, MonoScene e Dialog, sendo
que: Scene é uma subclasse de Interactor, MonoScene é uma subclasse de Scene e
Dialog é uma subclasse de MonoScene.
Com base nessa hierarquia de classes foi avaliada a porcentagem de reaproveitamento de
casos de teste obtida se, em vez de sempre se retestarem todas as classes e subclasses, fosse
utilizada a estratégia proposta. Os resultados são apresentados na Tabela 6.11.
Conforme destacado por Harrold et al.[168], o uso da Estratégia Incremental Hierárquica
reduz significativamente o esforço de teste. Muitos métodos que devem ser testados de novo
reutilizam os casos de teste baseados em especificação que foram desenvolvidos para suas
superclasses. A reutilização de conjuntos de testes da superclasse resulta em uma economia
154 Introdução ao Teste de Software ELSEVIER
tanto no tempo para analisar a classe e determinar quais métodos devem ser testados como
no tempo gasto para executar os casos de testes. Com a estratégia proposta, o testador pode
estimar os custos necessários para a realização dos teste e, com base nessa estimativa, con-
centrar os esforços nos pontos considerados mais problemáticos ou com maior probabilidade
de conter defeitos.
Da mesma forma que os critérios de fluxo de dados, o teste de mutação também foi pro-
posto inicialmente para o teste de unidade de programas procedimentais [115], mas já foram
propostas extensões desse critério para o teste de integração de programas C [110] (vide Ca-
pítulo 5) e também para o teste de especificações em Máquinas de Estados Finitos [132, 133],
Redes de Petri [134, 360], Statecharts [130], Estelle [331, 374], SDL [380] e Especificações
Algébricas [442]. É importante ressaltar: a essência do critério é a mesma, ou seja, realizar
alterações sintáticas no produto que está sendo testado produzindo-se um conjunto de produ-
tos mutantes e desenvolver um conjunto de teste que mostre que o conjunto de mutantes não
6.5. Estratégias, técnicas e critérios de teste OO 155
Teste intramétodo
pelos mesmos operadores, bem como uma descrição detalhada do significado do nome de
cada um deles, podem ser consultados os trabalhos de Agrawal et al. [7] e Vincenzi [413].
Tabela 6.13 – Significado do nome de alguns operadores de mutação de unidade para o teste
intramétodo [413]
Operador Significado Descrição
u-CGCR Constant for Global Constant Replacement Aplica-se a cada constante global do programa,
trocando-a por todas as outras constantes globais que
aparecem na mesma função.
u-CLCR Constant for Local Constant Replacement Aplica-se a cada constante local do programa,
trocando-a por todas as outras constantes locais que
aparecem na mesma função.
u-CGSR Constant for Global Scalar Replacement Aplica-se a cada referência escalar global do pro-
grama, trocando-a por todas as constantes globais
que aparecem na mesma função.
u-CLSR Constant for Local Scalar Replacement Aplica-se a cada referência escalar local do pro-
grama, trocando-a por todas as constantes locais que
aparecem na mesma função.
u-VDTR Domain Traps Aplica-se a cada variável escalar, envolvendo a
mesma por uma “função armadilha” que testa se a
variável assume os valores negativo, zero e positivo.
u-VTWD Twiddle Mutations Aplica-se a cada variável escalar, alterando o seu va-
lor uma unidade a mais e a menos.
u-OLBN Logical Operator by Bitwise Operator Substitui um operador lógico por um operador
bitwise sem atribuição.
u-ORRN Relational Operator Mutation Substitui um operador relacional por outro operador
relacional.
u-SMTC n-trip continue Introduz no início de cada laço uma chamada à fun-
ção que, quando executada 2 vezes, interrompe a exe-
cução do laço.
u-SSDL Statement Deletion Apaga comandos sistematicamente, um a um, mas
mantém o ponto-e-vírgula no final do comando para
manter a validade sintática do programa.
u-SWDD while Replacement by do-while Substitui comandos while por comandos
do-while
O símbolo () é utilizado para indicar quando um operador de mutação é aplicável para
a linguagem em questão, o símbolo (×), quando o operador não é aplicável, e o símbolo
() indica os operadores que podem ser aplicados com alguma modificação semântica. O
símbolo (∗ ) antes do nome do operador indica os operadores novos que foram definidos por
Vincenzi [413]. Tendo em vista que no caso da linguagem C existem operadores de mutação
específicos para realizar mutações em variáveis locais e globais, uma observação é necessária
para definir como esses conceitos de variáveis globais e locais foram mapeados para o teste
intramétodo de linguagens OO.
No caso de C++, por ser um superconjunto de C, o conjunto de variáveis globais é com-
posto por variáveis globais tradicionais (como conhecidas em C) e pelos atributos de uma
classe, uma vez que esses atributos podem ser compartilhados entre os diversos métodos de
uma mesma classe como se fossem variáveis globais da classe. Já o conjunto de variáveis
locais é definido como as variáveis declaradas dentro do método, da mesma forma como as
variáveis locais em C que são aquelas declaradas dentro de uma função.
No caso de Java, não existe o conceito de variável global como na linguagem C, ou seja,
em Java existem somente atributos e variáveis locais. Toda variável em Java deve obrigatoria-
mente ser declarada dentro de uma classe. Assim sendo, o conjunto de variáveis globais para
158 Introdução ao Teste de Software ELSEVIER
a linguagem Java é composto apenas dos atributos de uma classe e o conjunto de variáveis
locais é composto das variáveis declaradas dentro de um método.
Feitas tais considerações, como pode ser observado na Tabela 6.12, todos os 80 operado-
res de unidade de C são aplicáveis no teste intramétodo de programas C++, o que é natural,
uma vez que C++ é, na verdade, um superconjunto de C.
Para o teste de programas Java, observa-se que a maioria dos operadores também é dire-
tamente aplicável (59 dos 80 operadores de mutação). Os demais 21 operadores, analisados
em seqüência, são pertencentes às seguintes classes de mutação:
Como pode ser observado na Tabela 6.12, esses quatro novos operadores são específi-
cos para o teste de programas Java, não sendo utilizados no teste de programas C++.
• Operadores de Mutação de Variáveis
Considerando os operadores de mutação de variáveis, observa-se que cinco (u-VGPR,
u-VGTR, u-VLPR, u-VLTR e u-VSCR) não são diretamente aplicáveis a Java. A jus-
tificativa é que esses operadores são específicos para a realização de mutação de pon-
teiros e registros (struct) existentes em C e C++, mas não presentes em Java.
6.5. Estratégias, técnicas e critérios de teste OO 159
Entretanto, o conceito desses operadores pode ser mapeado para Java. No caso dos
operadores u-VGPR e u-VLPR, embora Java não tenha variáveis tipo ponteiro, ela
apresenta variáveis tipo referência; portanto, operadores de mutação específicos para
realizar mutações em variáveis tipo referência podem ser desenvolvidos. Já no caso dos
operadores u-VGTR, u-VLTR e u-VSCR, os quais se referem a struct, embora Java
não apresente variáveis tipo estrutura, ela apresenta o conceito de classe (a qual pode
ser vista como uma estrutura que contém os dados e funções de acesso a esses dados)
de modo que o conceito desses operadores também pode ser mapeado para Java.
A partir desses operadores, como pode ser observado na Tabela 6.12, três novos foram
definidos:
– u-VGCR – Mutate Global Class References: foi definido a partir dos operadores
u-VGPR e u-VGTR e é aplicado em variáveis de referência globais. Nesse ope-
rador, os tipos das variáveis são preservados de modo que somente variáveis de
referência globais de tipos compatíveis são mutadas.
– u-VLCR – Mutate Local Class References: foi definido a partir dos operadores
u-VLPR e u-VLTR e é aplicado em variáveis de referência locais. Nesse ope-
rador, os tipos da variáveis são preservados de modo que somente variáveis de
referência locais de tipos compatíveis são mutadas.
– u-VCAR – Class Attributes Replacement: foi definido tendo como base o ope-
rador u-VSCR e é responsável pela mutação de referências a atributos de uma
classe que são substituídas por referências aos demais atributos da mesma classe,
respeitando os tipos dos atributos.
Observa-se que tais operadores podem ser também utilizados no contexto de C++, uma
vez que as estruturas sintáticas exigidas por esses operadores também estão presentes
nessa linguagem.
• Operadores de Mutação de Operadores
Observa-se que os 15 operadores de mutação de unidade de C que não são aplicáveis a
Java pertencem à classe de mutação de operadores. A razão para isso é que, ao contrário
de C e C++, toda expressão lógica em Java é do tipo booleana. Essa característica reduz
a ocorrência de enganos, tal como a utilização do símbolo “=” em vez de “==” no teste
de igualdade, limitando o número de operadores de mutação que podem ser utilizados
em expressões lógicas.
Com a definição desses novos operadores, considera-se que os desvios sintáticos mais
comuns cometidos durante a codificação de métodos em Java e C++ estão sendo modelados.
Java passa a ter 59 + 7 = 66 operadores e C++ passa a ter 80 + 3 = 83 operadores destinados
ao teste intramétodo.
Obviamente, quanto maior o número de operadores de mutação, mais mutantes são ge-
rados e, conseqüentemente, maior é o custo para a aplicação do critério no que se refere ao
tempo de execução e análise dos mutantes vivos. Como foi mencionado anteriormente, além
de estudos que buscam a redução do custo de aplicação do teste de mutação por meio da
utilização de critérios alternativos como a Mutação Aleatória (Randomly Selected X% Muta-
tion) [3], a Mutação Restrita (Constrained Mutation) [272] e a Mutação Seletiva (Selective
160 Introdução ao Teste de Software ELSEVIER
Mutation) [313], destacam-se também os estudos que objetivam determinar Conjuntos Es-
senciais de Operadores de Mutação [310, 438, 25] e os estudos que objetivam auxiliar na
determinação de mutantes equivalentes [164, 182, 306, 311, 421].
Um ponto importante a ser observado é que, entre os operadores de mutação considerados
essenciais para o teste de unidade em programas C (u-SWDD, u-SMTC, u-SSDL, u-OLBN,
u-ORRN, u-VTWD, u-VDTR, u-CGCR, u-CLCR, u-CGSR e u-CLSR), todos eles, exceto o
u-OLBN no caso de Java, são também aplicáveis em programas Java e C++, o que motiva
a investigar se a mesma relação de inclusão entre esses operadores se mantém no teste de
programas OO [413].
Teste intermétodo
Ao contrário do teste intramétodo, o teste intermétodo deseja descobrir defeitos nas interfaces
de comunicação entre os métodos. Ele pode ser considerado similar ao teste de integração
em programas procedimentais. Conforme apresentado no Capítulo 5, Delamaro et al. [110]
propuseram o critério Mutação de Interface que pode ser visto como uma extensão da Análise
de Mutantes e preocupa-se em assegurar que as interações entre unidades sejam testadas.
Assim, o objetivo do critério Mutação de Interface é inserir perturbações nas conexões entre
duas unidades.
Observa-se que os conceitos do critério Mutação de Interface podem ser facilmente ma-
peados para o contexto OO, considerando o teste intermétodos. Basicamente, uma vez que,
para realizar as mutações, a grande maioria dos operadores de interface baseia-se nos con-
juntos de parâmetros, variáveis e constantes relacionados à conexão de duas unidades f -g,
conforme apresentado na Seção 5.6 do Capítulo 5, basta redefinir tais conjuntos considerando
as linguagens OO em questão, Java e C++.
Novamente, no caso de C++, tudo o que é válido em C continua sendo válido e resta
apenas adicionar ao conjunto de variáveis globais G(g) os atributos da classe utilizados pela
função (método) g; no caso do conjunto de variáveis externas E(g), devem-se adicionar os
atributos da classe não utilizados pela função (método) g.
Já para a linguagem Java, as variáveis globais podem ser vistas como os atributos de uma
classe ou atributos estáticos e, desse modo, supondo que a mutação será aplicada na conexão
entre dois métodos f e g, a definição dos conjuntos para Java ficaria sendo:
Outro ponto importante a ser observado é que, da mesma forma como foi determinado o
conjunto de operadores essenciais para o teste de unidade, Vincenzi et al. [417, 418] reali-
zaram um experimento aplicando o procedimento Essencial [25] ao conjunto de operadores
de mutação de interface. Para isso, consideraram os dados coletados para cinco programas
C utilitários do UNIX. Desse experimento, Vincenzi et al. [418] chegaram a um subcon-
junto de oito operadores (I-CovAllNod, I-DirVarBitNeg, I-IndVarBitNeg, I-IndVarRepExt,
I-IndVarRepGlo, I-IndVarRepLoc, I-IndVarRepReq e II-ArgAriNeg), os quais compõem o
conjunto essencial de operadores de mutação de interface. Para o experimento em questão,
a utilização desse subconjunto resultou em uma redução de custo do critério Mutação de
Interface superior a 73%, mantendo-se um escore de mutação em relação ao conjunto total
de operadores de interface na ordem de 0,998 [418]. Tais resultados motivam reavaliar se
os mesmos resultados se confirmam no teste de programas OO. A Tabela 6.16 apresenta a
descrição dos oito operadores essenciais. Para mais informações sobre os tipos de mutações
realizadas pelos mesmos operadores, bem como uma descrição detalhada do significado do
nome de cada um deles, pode ser consultado o trabalho de Delamaro [108].
162 Introdução ao Teste de Software ELSEVIER
Tabela 6.16 – Significado do nome de alguns operadores de mutação de unidade para o teste
intermétodo [413]
Operador Significado Descrição
I-CovAllNod Coverage of Nodes Garante cobertura de nós de determinada função.
I-DirVarBitNeg Inserts Bit Negation at Interface Varia- Acrescenta negação de bit em variáveis de interface.
bles
I-IndVarBitNeg Inserts Bit Negation at Interface Varia- Acrescenta negação de bit em variáveis não de inter-
bles face.
I-IndVarRepExt Replaces Non Interface Variables by Troca variáveis não de interface por elementos de E.
Extern Global
I-IndVarRepGlo Replaces Non Interface Variables by Troca variáveis não de interface por elementos de G.
Global Variables
I-IndVarRepLoc Replaces Non Interface Variables by Troca variáveis não de interface por elementos de L.
Local Variables
I-IndVarRepReq Replaces Non Interface Variables by Troca variáveis não de interface por elementos de R.
Required Constants
II-ArgAriNeg Insert Arithmetic Negation on Argu- Acrescenta negação aritmética antes de argumento.
ment
Teste interclasse
Conforme definido por Ma et al. [257], no teste interclasse o objetivo é identificar defei-
tos relacionados a características específicas de programas OO, tais como encapsulamento,
herança, polimorfismo e acoplamento dinâmico.
6.5. Estratégias, técnicas e critérios de teste OO 163
aplicação deles também na linguagem C++. A primeira letra do nome do operador identifica
a qual grupo ele pertence: A – Access Control, I – Inheritance, P – Polymorphism, O –
Overloading, J – Java-Specific Features e E – Common Programming Mistakes.
Como observado por Vincenzi [413], os operadores específicos para a linguagem Java
não são aplicados a C++, mas todos os demais operadores podem ser mapeados para C++.
Um ponto a ser observado é que C++ é muito mais flexível do que Java e um conjunto de
operadores de mutação adequado ao teste de programas C++ deve ser muito mais abran-
gente do que esse. Por exemplo, entre algumas características de C++ não contempladas
por esses operadores estão: herança múltipla, sobrecarga de operadores, funções friends e
templates [94].
Legenda
– operadores aplicáveis
×– operadores não-aplicáveis
– operadores aplicáveis com modificações
∗
– operadores definidos
de classe. Na Tabela 6.18 é apresentada uma breve descrição de alguns dos operadores de
mutação de classe da Tabela 6.17. Para obter mais informações sobre os tipos de mutações
realizadas, bem como uma descrição detalhada do significado do nome de cada um deles,
pode ser consultado o trabalho de Ma et al. [257].
Além dos operadores para o teste interclasse de programas Java apresentados na Ta-
bela 6.17, outros conjuntos de operadores de mutação para o teste de características mais
específicas da linguagem vêm sendo propostos, como, por exemplo, os operadores para o
teste de classes que implementam as interfaces Iterator, Collection e a classe abs-
trata InputStream propostos por Alexander et al. [12].
Perspectiva do cliente
Os clientes são aqueles que desenvolvem sistemas, integrando em suas aplicações compo-
nentes desenvolvidos independentemente. Para auxiliá-los, existem diversas iniciativas de se
adaptarem técnicas de análise e teste destinadas a programas tradicionais para o contexto de
sistemas baseados em componentes. Entretanto, existem algumas questões que dificultam a
adaptação de tais técnicas.
Primeiro, o código-fonte dos componentes em geral não é disponibilizado para os seus
clientes. Técnicas e critérios de teste baseados na implementação, tais como critérios basea-
dos em análise de fluxo de dados e critérios baseados em mutação necessitam do código-fonte
para derivar os requisitos de teste. Quando o código-fonte do componente não está disponível
para o cliente, as técnicas de teste tradicionais não podem ser aplicadas no teste de sistemas
baseados em componentes ou pelo menos será requerido um esquema alternativo estabelecido
entre as partes interessadas.
Segundo, em sistemas baseados em componentes, mesmo que o código-fonte esteja dis-
ponível, os componentes e a aplicação do cliente podem ter sido implementados em diferentes
linguagens de programação. Desse modo, uma ferramenta de análise ou de teste baseada em
uma linguagem de implementação específica não seria adequada para testar um programa que
envolve outra linguagem, além daquela apoiada pela ferramenta.
Terceiro, um componente de software freqüentemente oferece mais funcionalidades do
que a aplicação do cliente necessita. Com isso, sem a identificação da parte da funcionalidade
que é utilizada pela aplicação, uma ferramenta de teste irá fornecer relatórios imprecisos.
Por exemplo, critérios de teste estruturais avaliam o quanto determinado conjunto de teste
é adequado em cobrir os requisitos de teste exigidos pelo critério (elementos estruturais do
programa). Quando se deseja avaliar a adequação de um determinado conjunto de teste em
relação a um sistema baseado em componentes, os elementos estruturais que compreendem
a parte não utilizada do componente devem ser excluídos da avaliação. Caso contrário, uma
ferramenta de teste irá produzir relatórios indicando baixa cobertura para o conjunto de teste,
mesmo se tal conjunto teste exaustivamente a parte do código que está sendo utilizada [348].
Perspectiva do desenvolvedor
A seguir são descritos alguns critérios de teste funcional e algumas estratégias que podem
ser utilizadas no teste de componentes. Em seguida, são discutidas algumas alternativas que
tentam viabilizar o uso de critérios estruturais no teste de componentes.
Como pode ser observado, critérios funcionais [31] são aplicados diretamente no teste de
programas procedimentais, orientados a objetos e componentes de software, visto que deri-
vam os seus requisitos de teste somente com base na especificação do programa/componente.
Entretanto, o maior problema com os critérios da técnica funcional é que, por serem basea-
dos na especificação, eles não são capazes de garantir que partes essenciais ou críticas da
implementação tenham sido cobertas pelo conjunto de teste.
Uma vez que os critérios da técnica estrutural e os critérios baseados em defeitos, em ge-
ral, requerem a disponibilidade do código-fonte para serem aplicados, critérios alternativos,
que não apresentam tal restrição vêm sendo propostos. Tais critérios são baseados em refle-
xão computacional [126], polimorfismo [368], metadados (metadata) [127] e metaconteúdo
(metacontent) [317], teste baseado em estados [32] e autoteste (built-in testing) [126, 424].
Reflexão computacional permite acesso à estrutura interna de um programa e inspeção do
seu comportamento, sendo utilizada na área de teste para automatizar a execução dos testes,
criando instâncias de classes e invocando métodos em diferentes seqüências. No teste de
componentes, a reflexão computacional é usada para “carregar” uma determinada classe em
uma ferramenta de teste que identifica os métodos e os respectivos parâmetros, viabilizando
sua execução. Com isso, a reflexão viabiliza o desenvolvimento de casos de teste, inspecio-
nando o que é necessário para invocar os métodos de cada uma das classes [127]. Rosa e
Martins [346] propõem o uso de uma arquitetura reflexiva para validar o comportamento de
POO.
Outra solução, similar a um empacotador (wrapper), é proposta por Soundarajan e Ty-
ler [368] usando polimorfismo. Dada a especificação formal de um componente, contendo as
pré e pós-condições que devem ser satisfeitas na invocação de cada método, métodos poli-
mórficos são criados de modo que, antes da invocação do método real (o que é implementado
pelo componente), a versão polimórfica do método verifica se a precondição é satisfeita,
coleta informações usadas na invocação do método (valor dos parâmetros, por exemplo), e
constata se as pós-condições foram satisfeitas. A desvantagem dessa abordagem é que ela
requer uma especificação formal do componente e não garante a cobertura de código. Além
disso, para viabilizar a sua utilização, é necessário o desenvolvimento de um gerador auto-
mático de empacotadores, uma vez que alterações na especificação do componente (as pré
ou pós-condições, por exemplo) podem requerer que os métodos polimórficos sejam gera-
dos/avaliados novamente.
Metadados são, também, utilizados pelos modelos de desenvolvimento de componentes
para fornecer informações genéricas sobre o componente, tais como o nome de suas classes,
o nome de seus métodos, bem como informações para o teste [127, 317]. Pelo uso de me-
tadados, aspectos estáticos e dinâmicos do componente podem ser consultados pelo cliente
para a realização de diversas tarefas. O problema com essa abordagem é que ainda não há um
consenso sobre o conjunto de informações que deve ser disponibilizado nem sobre a forma
como disponibilizá-lo. Além do mais, essa abordagem requer um trabalho adicional por parte
do desenvolvedor do componente.
168 Introdução ao Teste de Software ELSEVIER
Outras estratégias propõem uma abordagem integrada para a geração de dados de teste
para o teste de componentes de software. Elas combinam informações funcionais e estrutu-
rais obtidas a partir de uma especificação formal ou semiformal do componente. A idéia é
construir um Grafo Def-Uso [337] do componente com base na sua especificação, a fim de
que critérios de teste de fluxo de dados e de controle possam ser utilizados para a geração de
dados de teste. O problema é que como as informações estruturais são derivadas da especifi-
cação, satisfazer tais critérios não garante a cobertura do código do componente, mas sim de
sua especificação. Além disso, uma especificação formal ou semiformal do componente tem
de estar disponível [32, 125].
O conceito de componentes autotestáveis também vem sendo explorado. A idéia é dis-
ponibilizar componentes de software com capacidades de teste embutidas e que possam ser
habilitadas ou desabilitadas, dependendo se o componente encontra-se em operação normal
ou em manutenção, por exemplo [127, 269, 424]. Edwards [127] discute como diferentes
tipos de informações podem ser embutidas em componentes de software por meio de um em-
pacotador de metadados reflexivo (reflexive metadata wrapper). Ele sugere que informações
como a especificação do componente, sua documentação, histórico de verificação, serviços
de verificação de violação de pré e pós-condições, serviços de autoteste, entre outras, podem
ser embutidas utilizando-se tal mecanismo. O problema com essa abordagem, embora muito
útil, é que ela exige tempo e recursos adicionais por parte do desenvolvedor do componente,
pois é dele que depende a coleta e a inclusão de tais informações. Além disso, quais informa-
ções realmente deveriam ser fornecidas e como deveriam ser fornecidas ainda não dispõem
de um padrão, tornando mais difícil o trabalho de se desenvolverem ferramentas de teste que
necessitam de tais informações.
Como pode ser observado pelos trabalhos descritos, todas as abordagens propostas tentam
minimizar o problema do teste de componentes do lado dos clientes, quando o código-fonte
do componente não se encontra disponível. Isso implica que, exceto se o componente for do-
tado de autoteste ou metadados que permitam a coleta de informações de seu estado interno,
nenhuma informação sobre cobertura de características estruturais do componente pode ser
obtida por parte do cliente.
No caso de componentes Java, tais como JavaBeans, esse problema pode ser superado
realizando os testes diretamente no programa objeto (bytecode) sem depender do código-
fonte Java. O arquivo bytecode é uma representação binária independente de plataforma que
contém informações de alto nível sobre uma classe, tais como seu nome, o nome de sua su-
perclasse, informações sobre métodos, variáveis e constantes utilizadas, além das instruções
de cada um de seus métodos.
Instruções de bytecode lembram instruções em linguagem assembly, mas armazenam in-
formações de alto nível sobre um programa, de modo que é possível extrair informações de
fluxo de controle e de dados a partir delas [414]. Trabalhando diretamente com bytecode
Java, tanto o desenvolvedor do componente quanto seus clientes podem utilizar a mesma re-
presentação e os mesmos critérios para testar componentes Java. Além disso, o cliente será
capaz de avaliar qual porcentagem do componente foi coberta por sua massa de teste, ou seja,
ele será capaz de avaliar a qualidade de seu conjunto de teste funcional em relação a critérios
de teste estruturais.
Chambers et al. [65] e Zhao [457] descrevem duas abordagens diferentes para realizar
análise de dependência em bytecode Java. Os critérios de fluxo de dados apresentados ante-
riormente utilizam uma técnica de análise similar à desenvolvida por Zhao [457]. No trabalho
6.7. Ferramentas 169
O papel do teste é verificar se o componente está sendo utilizado de maneira coerente e que
sua integração respeita a especificação fornecida pelo produtor. Algumas questões, porém,
dificultam a integração do componente. A mais importante delas é que o código-fonte dos
componentes não está disponível, o que impossibilita a utilização de técnicas como a cober-
tura de elementos estruturais.
Beydeda e Gruhn [33] identificam diversos problemas relacionados à utilização de com-
ponentes por parte dos clientes, como: desenvolvimento do componente dependente de um
contexto, documentação insuficiente e dependência do cliente em relação ao produtor. Grande
parte desses problemas está relacionada com a falta de informação, principalmente do cliente,
em relação à maneira como o componente foi desenvolvido. Para lidar com tais problemas
e facilitar a execução de algumas tarefas por parte do cliente, em particular a condução de
testes de maneira sistemática, algumas abordagens foram propostas.
Beydeda e Ghuhn dividem essas abordagens em duas classes: as que procuram lidar com
essa falta de informação e as que procuram facilitar a troca de informação entre produtor e
cliente. Nessa segunda classe destacam-se alguns trabalhos, como os de Orso et al. [318],
Liu e Richardson [246] e Edwards [127], que procuram estabelecer formas de encapsular no
componente informações que possam ser úteis ao cliente na condução de algumas tarefas,
incluindo o teste de integração do componente. Um problema com esses trabalhos é que
eles identificam como a informação deve ser fornecida ao cliente, mas não quais são essas
informações.
6.7 Ferramentas
Na prática, a aplicação de um critério de teste está fortemente condicionada à sua automati-
zação. O desenvolvimento de ferramentas de teste é de fundamental importância, uma vez
que a atividade de teste é muito propensa a erros, além de improdutiva, se aplicada manual-
mente. Além disso, ferramentas de teste facilitam a condução de estudos experimentais que
buscam avaliar e comparar os diversos critérios de teste. Assim sendo, a disponibilidade de
ferramentas de teste propicia maior qualidade e produtividade para a atividade de teste. A
seguir é apresentada a descrição, parcialmente extraída do trabalho de Domingues [120], de
uma série de ferramentas comerciais e não comerciais que se encontram disponíveis para o
teste de POO e algumas também destinadas ao teste de componentes, principalmente para
170 Introdução ao Teste de Software ELSEVIER
o teste de programas escritos em C++ e Java. Essas ferramentas foram obtidas por meio de
pesquisa na World Wide Web e a descrição apresentada é baseada na documentação existente
de cada uma, bem como na execução de tais ferramentas, utilizando versões de demonstração
quando essas encontravam-se disponíveis.
A ferramenta de teste PiSCES (Coverage Tracker for Java) é uma das primeiras ferra-
mentas de teste desenvolvidas para o teste de applets Java. PiSCES é uma ferramenta de
medida de cobertura (comandos e decisões) que identifica quais partes do código-fonte já
foram exercitadas durante os testes e quais ainda precisam ser. Embora tenha sido projetada
para o teste de applets Java, a ferramenta requer o código-fonte para derivar os requisitos de
teste a serem cobertos [36].
Outra iniciativa no desenvolvimento de um conjunto de ferramentas de teste foi iniciado
pela Sun Microsystems em meados dos anos 90 [367]. SunTest (Java Testing Tools from Sun)
incluía uma ferramenta de captura-reprodução (capture-replay), denominada JavaStar, des-
tinada ao teste de programas com interface gráfica; uma ferramenta de medida de cobertura
de código (comandos e decisões), denominada JavaScope; uma ferramenta para a geração de
drivers para o teste de métodos, denominada JavaSpec; uma ferramenta para o teste de carga,
denominada JavaLoad; uma ferramenta destinada à análise estática de threads, denominada
JavaLoom; e uma ferramenta para verificar a portabilidade de programas Java, denominada
JavaPureCheck. Infelizmente, em 1999 a Sun tomou uma decisão estratégica de descontinuar
o desenvolvimento desse conjunto de ferramentas de teste.
JProbe Suite [333] é um conjunto de três ferramentas, composto por: JProbe Profiler
and Memory Debugger que ajuda a eliminar gargalos de execução causados por algoritmos
ineficientes em códigos Java e aponta as causas de perdas de memória nessas aplicações,
rastreando quais objetos seguram referências para outros; JProbe Threadalyzer, que monitora
interações entre threads e avisa o testador quando essa interação representar perigo, bem
como identifica potenciais perigos de concorrências e deadlocks; e JProbe Coverage, que
localiza códigos não testados e mede quanto do código está sendo exercitado, permitindo ao
testador estimar a confiança dos testes executados.
TCAT/Java [342] e JCover [267] são duas ferramentas de teste que implementam os cri-
térios de cobertura de comandos e decisões para o teste de programas e applets Java. Tais
ferramentas também exigem o código-fonte para conduzir a atividade de teste.
Parasoft C++ Test [323] é uma ferramenta de teste de unidade para códigos C/C++ que
executa os seguintes tipos de teste: 1) teste funcional; 2) teste estrutural (cobertura de co-
mandos e decisões); e 3) teste de regressão. Essa ferramenta permite aos desenvolvedores
testarem suas classes imediatamente após elas terem sido escritas e compiladas por meio da
automatização da criação de driver e de quaisquer stubs necessários, no qual o testador pode
personalizar os respectivos valores de retorno ou, ainda, entrar com os próprios stubs. Essa
ferramenta automatiza o teste funcional com a geração automática dos casos de teste e docu-
mentação dos resultados esperados, os quais são comparados com os resultados reais. Além
disso, o testador pode incluir seus casos de teste e obter relatórios personalizados. No teste
estrutural, essa ferramenta gera e executa automaticamente casos de teste projetados para tes-
tar uma classe especificada. Qualquer problema encontrado é assinalado e apresentado em
uma estrutura gráfica. Esses casos de teste são automaticamente salvos, de forma que pos-
sam ser usados facilmente no teste de regressão para se ter certeza de que modificações nas
aplicações não introduziram novos defeitos.
6.7. Ferramentas 171
Tanto Chevalley [75] quanto Ma et al. [257] desenvolveram também ferramentas de teste
baseadas em mutação para apoiar a aplicação dos conjuntos de operadores de mutação que
ambos desenvolveram/estenderam. A característica comum entre essas ferramentas é que
elas utilizam o conceito de reflexão computacional para implementar as mutações. Ambas as
ferramentas utilizam OpenJava [387], que é um sistema reflexivo em tempo de compilação, o
qual utiliza macros (metaprogramas) para manipular metaobjetos que representam entidades
lógicas de um programa, facilitando a realização das mutações.
Recentemente, Bybro [56] desenvolveu uma ferramenta de teste, denominada Mutation
Testing System, que implementa parte do conjunto de operadores de mutação proposto por
Ma et al. [257]. A ferramenta utiliza o framework JUnit para documentar e executar de
forma automática os casos de teste, determinando o número de mutantes mortos e o escore
de mutação obtido.
A Tabela 6.19 apresenta comparações das atividades apoiadas por essas ferramentas de
teste para programas orientados a objetos, tais como: 1) fases do teste; 2) critérios de teste;
3) linguagem suportada; 4) exigência do código-fonte; 5) atividade de depuração; e 6) teste
de regressão.
Quando se analisa a Tabela 6.19 observa-se que o framework JUnit [28] é uma das fer-
ramentas que pode ser utilizada tanto para o programa quanto para o teste de componentes
de software desenvolvidos em Java, mas tal ferramenta apóia apenas a execução automá-
tica de casos de teste e realização de testes funcionais, sem apoio de nenhum critério de
teste específico e não fornecendo informação sobre a cobertura de código obtida por deter-
minado conjunto de teste. Outra ferramenta que também apóia somente o teste funcional é a
CTB [54].
Considerando as ferramentas que permitem a avaliação de cobertura de código por meio
de critérios estruturais, observa-se que das ferramentas analisadas todas apóiam somente o
teste de fluxo de controle (cobertura de comandos e decisão) em POO. Nenhuma delas apóia
a aplicação de algum critério de fluxo de dados seja para o teste de unidade, integração ou
sistema. Além disso, exceto pela ferramenta GlassJAR [124] e pelas que apóiam o teste fun-
cional, todas as demais necessitam do código-fonte para a aplicação dos critérios, dificultando
sua utilização no teste estrutural de componentes de software por parte dos clientes, os quais,
em geral, não têm acesso ao código-fonte.
Ao se levarem em conta as ferramentas que apóiam o teste de mutação, observa-se que
todas fornecem suporte ao teste de integração interclasse. Mesmo que o teste de integração
seja de grande importância no contexto de POO, tendo em vista o grande número de chamadas
de métodos intra e interclasses existentes, considera-se que, ainda assim, os testes de unidade
devam ser realizados, principalmente, para eliminar os defeitos de lógica e de programação
antes de as unidades individuais serem integradas.
Exigência de código-fonte
Atividade de depuração
Linguagem suportada
Critérios funcionais
Teste de integração
Ferramentas de teste de software
Teste de regressão
Teste de unidade
Teste de sistema
PiSCES Java
SunTest Java
xSuds Toolsuite C/C++
JProbe Developer Suite Java
TCAT/Java Java
JCover Java
Parasoft C++ Test C/C++
Parasoft Insure++ C/C++
ProLint C/C++
Rational PureCoverage C++/Java
Rational Purify C/C++
JUnit Java
Cobertura Java
JaBUTi Java
CTB Java/C/C++
Parasoft JTest Java
Glass JAR Toolkit Java
Object Mutation Engine Java
Ferramenta de Chevalley [75] Java
Ferramenta de Ma et al. [257] Java
Mutation Testing System Java
algumas dificuldades adicionais para a atividade de teste. Mesmo com essas dificuldades,
critérios de teste estão sendo desenvolvidos ou adaptados a partir daqueles já existentes para
o teste de programas procedimentais e vêm sendo utilizados no teste de programas orientados
a objetos.
Apesar da popularização trazida pelas vantagens do desenvolvimento baseado em com-
ponentes, nota-se que alguns problemas relacionados a esse paradigma continuam sem uma
solução geral. É o caso da atividade de teste, que introduz alguns problemas importantes
no desenvolvimento baseado em componentes, principalmente considerando a perspectiva
do cliente, que faz uso do componente desenvolvido e, em geral, não possui acesso a todas
as informações que dispõe o desenvolvedor do mesmo. Diversos trabalhos na literatura têm
identificado um ponto em comum, como fonte de tais problemas: a falta de dados a respeito
do desenvolvimento do componente, de modo a facilitar sua utilização como parte de um
sistema baseado em componentes. Esses mesmos trabalhos propõem algumas formas de pro-
ver tal informação ao cliente do componente, tendo por fim minimizar as dificuldades em
algumas tarefas do processo de desenvolvimento, em particular, na condução da atividade
de teste; entretanto, ainda não há consenso sobre o conjunto de informações que deve ser
disponibilizado.
Capítulo 7
Teste de Aspectos
7.1 Introdução
A programação orientada a objetos (POO) revolucionou as técnicas de programação, auxi-
liando no tratamento de complexidades inerentes ao software [58] e possibilitando uma me-
lhor separação de interesses na implementação e no projeto de sistemas. Apesar disso, alguns
problemas não foram completamente resolvidos por suas construções. Especificamente, a
orientação a objetos não permite a clara separação de certos tipos de requisitos – geralmente
não funcionais – que têm sido chamados de interesses transversais (crosscutting concerns),
pois sua implementação tende a se espalhar por diversos módulos do sistema, em vez de ficar
localizada em unidades isoladas.
Como uma proposta para resolver esse problema, no final da década de 1990 [209] surgiu
a programação orientada a aspectos (POA), que oferece mecanismos para a construção de
programas em que os interesses transversais ficam separados dos interesses básicos, em vez
de espalhados pelo sistema. A POA não é um novo paradigma de programação, mas sim uma
técnica para ser utilizada em conjunto com os diversos paradigmas de programação existentes
[128], possibilitando a construção de sistemas com maior qualidade.
A diferença principal apresentada pela POA com relação às outras técnicas de programa-
ção é possibilitar a implementação de módulos isolados – os aspectos –, que têm a capacidade
de afetar vários outros módulos do sistema de forma transversal. Em POA, um único aspecto
pode contribuir para a implementação de diversos outros módulos que implementam as fun-
cionalidades básicas (chamado de código-base). Esse mecanismo que permite a um aspecto
afetar vários pontos do sistema é chamado de quantificação.
A POA é uma abordagem recente. Apesar disso, já existem iniciativas do seu uso para o
apoio ao teste de software, utilizando principalmente a linguagem AspectJ (que será apresen-
tada na próxima seção). A separação entre código-base e código de teste auxilia o manuseio
de ambos, e a facilidade de inserir e remover aspectos na aplicação pode levar à criação de ce-
176 Introdução ao Teste de Software ELSEVIER
nários de teste com maior rapidez. Além disso, os aspectos podem ser facilmente removidos
quando a atividade de teste é finalizada, conservando intacto o programa original.
Por outro lado, a POA apresenta novos desafios em cada fase do ciclo de vida de de-
senvolvimento de software porque apresenta peculiaridades com relação a outras técnicas de
programação. Por exemplo, por causa da existência do aspecto como uma entidade nova, téc-
nicas de modelagem propostas para projetar programas orientados a objetos não podem ser
utilizadas diretamente e, portanto, necessitam de adaptações – ou novas abordagens – para
serem adequadamente utilizadas.
Nesse sentido, também as técnicas de teste de software devem ser revisitadas para serem
aplicadas nesse novo contexto. Por exemplo, para aplicar o teste estrutural a programas orien-
tados a aspectos é necessário adaptar as representações estruturais dos programas – os grafos
de fluxo de controle e de dados – para que os critérios possam ser adequadamente utiliza-
dos. Além disso, pode ser necessário definir novos tipos de critérios, independentemente da
técnica utilizada, que irão auxiliar a descobrir defeitos em programas orientados a aspectos.
Neste capítulo são abordadas duas perspectivas relacionadas a POA e ao teste de soft-
ware: 1) como a POA pode auxiliar o teste de software OO e 2) como é possível testar
programas que utilizam a POA. Na primeira perspectiva são apresentadas alternativas que
utilizam a POA em atividades relacionadas ao teste de software. Na segunda perspectiva são
apresentadas abordagens de teste aplicadas a programas orientados a aspectos.
7.1.1 Definições
7.1.2 AspectJ
• Cliente, que modela clientes e possui atributos para nome, telefone, código de área
do cliente e senha para acessar o sistema da empresa de telefonia (em um sítio na Web
em que ele pode alterar seus dados cadastrais, por exemplo);
• a classe abstrata Conexao com suas duas classes concretas Local e LongaDis-
tancia, que modelam conexões locais e de longa distância;
Programa 7.1
1 public class Chamada {
2 private Cliente chamador, chamado;
3 private Vector conexoes = new Vector();
4
5 public Chamada(Cliente chamador, Cliente chamado, boolean cel) {
6 this.chamador = chamador;
7 this.chamado = chamado;
7.1. Introdução 179
8 Conexao c;
9 c =
10 FabricaConexao.criarConexao(chamador, chamado, cel);
11 conexoes.addElement(c);
12 }
13
14 public void atende() {
15 Conexao conexao = (Conexao)conexoes.lastElement();
16 conexao.completar();
17 }
18 public boolean conectado(){
19 return ((Conexao)conexoes.lastElement()).getEstado()
20 == Conexao.INICIADA;
21 }
22
23 public void desligar(Cliente c) {
24 for(Enumeration e = conexoes.elements(); e.hasMoreElements();) {
25 ((Conexao)e.nextElement()).desconectar();
26 }
27 }
28
29 public boolean inclui(Cliente c){
30 boolean resultado = false;
31 for(Enumeration e = conexoes.elements(); e.hasMoreElements();) {
32 resultado = resultado || ((Conexao)e.nextElement()).conecta(c);
33 }
34 return resultado;
35 }
36
37 public void juntar(Chamada outra){
38 for(Enumeration e=outra.conexoes.elements(); e.hasMoreElements();){
39 Conexao con = (Conexao)e.nextElement();
40 outra.conexoes.removeElement(con);
41 conexoes.addElement(con);
42 }
43 }
44 }
45
46 public aspect Temporizacao {
47
48 public long Cliente.tempoTotalConexao = 0;
49
50 public long getTempoTotalConexao(Cliente cli) {
51 return cli.tempoTotalConexao;
52 }
53
54 private Temporizador Conexao.temporizador = new Temporizador();
55 public Temporizador getTemporizador(Conexao con) {
56 return con.temporizador;
57 }
58
59 after (Conexao c) returning ():
60 target(c) && call(void Conexao.inicia()) {
61 getTemporizador(c).iniciar();
62 }
63
64 pointcut finalizacao(Conexao c): target(c) &&
65 call(void Conexao.desconecta());
66
67 after(Conexao c) returning ():finalizacao(c) {
68 getTemporizador(c).parar();
180 Introdução ao Teste de Software ELSEVIER
69 c.getChamador().tempoTotalConexao+=getTemporizador(c).getTempo();
70 c.getChamado().tempoTotalConexao+=getTemporizador(c).getTempo();
71 }
72 }
*
Além das classes Java comuns, em AspectJ podem-se codificar aspectos por meio da pa-
lavra reservada aspect (como, por exemplo, o aspecto Temporizacao – linhas 46-72
do Programa 7.1. Aspectos são módulos que combinam: especificações de pontos de jun-
ção utilizando conjuntos de junção (pointcut); adendos anteriores (before), posteriores
(after) e de contorno (around); e declarações intertipos, utilizadas para introduzir atri-
butos, métodos e heranças em outras classes ou interfaces, para os propósitos do aspecto.
Versões recentes de AspectJ também apóiam declarações de avisos e erros de compilação
quando certos pontos de junção são identificados [391].
O modelo de pontos de junção de AspectJ é baseado nas construções sintáticas da lingua-
gem: chamada e execução de métodos/construtores (call e execution), leitura e escrita
de atributos (get e set), execução de determinado tratador de exceção (handler), e ou-
tros. Um conjunto de junção é definido com base nos designadores de conjuntos de junção,
que são os call, execution, get, etc., também chamados de designadores de conjuntos
de junção primitivos. Eles podem ser compostos a outros tipos de designadores, utilizando
operadores ‘e’ (&&) e ‘ou’ (||) para formar um único conjunto de junção. Além disso, pode
ser utilizado o operador de negação (!) para evitar que determinados pontos de junção sejam
selecionados. Por questões de simplificação, será usado o termo conjunto de junção para se
referir tanto ao próprio conjunto quanto aos designadores.
Os outros tipos de conjuntos de junção são compostos aos primitivos para filtrar pontos de
junção de acordo com propriedades do tipo: fluxo de controle em um dado conjunto de junção
(cflow e cflowbelow), verificação de alguma condição dinâmica ou estática (if), tipo
do objeto corrente no ponto de junção (this), tipo do objeto do qual o método está sendo
chamado no ponto de junção (target), argumentos passados para o método no qual o ponto
de junção ocorreu (args), etc. Esses três últimos também podem ser utilizados para acessar
essas informações do contexto do ponto de junção (objeto corrente, objeto do qual o método
está sendo chamado e argumentos do método do ponto de junção, respectivamente). Um
exemplo de conjunto de junção no Programa 7.1 é o finalizacao declarado na linha 64,
que identifica todas as chamadas ao método Conexao.desconecta e, além disso, obtém
o objeto do tipo Conexao do qual o método está sendo chamado.
Em AspectJ, existem três tipos de adendos posteriores: after returning, after
throwing e simplesmente after. O primeiro é executado apenas quando o ponto de jun-
ção retorna normalmente; o segundo, apenas quando uma exceção é lançada; e o terceiro
executa sempre. O aspecto Temporizacao listado no Programa 7.1 possui dois adendos
posteriores para iniciar e parar o temporizador de uma conexão, atribuindo a hora atual ao
atributo do tipo Temporizador introduzido na classe Conexao por meio de uma de-
claração intertipos (linha 54). Os adendos definem comportamento nos seguintes conjun-
tos de junção: 1) todas as chamadas ao método Conexao.inicia() – que completa
uma ligação – iniciando o temporizador (linhas 59-62) e 2) todas as chamadas ao método
Conexao.desconecta() – que termina uma conexão – parando o temporizador (linhas
64-71).
7.2. Teste de programas OO apoiado por aspectos 181
Os adendos de contorno (não presentes no exemplo) podem fazer com que o ponto de
junção alcançado prossiga com a sua execução por meio do método proceed(), que tam-
bém pode ser utilizado para obter o valor de retorno do ponto de junção. Dentro dos adendos
também é possível capturar dados do contexto dos pontos de junção, como nos dois adendos
do aspecto Temporizacao listados no Programa 7.1. São duas as maneiras de se obte-
rem os dados do contexto dos pontos de junção: utilizando os conjuntos de junção do tipo
args (para obter os argumentos), this (para obter o objeto executando no ponto de junção)
ou target (para obter o objeto alvo em um ponto de junção, como no exemplo); ou por
meio da palavra reservada thisJoinPoint, que funciona como um objeto que encapsula
o ponto de junção alcançado.
Do ponto de vista da implementação, para ser compatível com qualquer máquina virtual
Java, o compilador de AspectJ gera, a partir do código-fonte do código-base e código-fonte
de aspectos, bytecode Java comum, obtido por meio da combinação de código-base e as-
pectos. Para que isso seja possível, os aspectos são transformados em classes e os adendos
são transformados em métodos comuns. Os parâmetros passados para esses métodos são os
mesmos parâmetros dos adendos, possivelmente acrescentados das variáveis que contêm in-
formações reflexivas (por exemplo, o thisJoinPoint). O corpo do método é o mesmo do
adendo, com exceção do tratamento especial que é dado para os adendos de contorno quando
o método proceed() é explicitamente chamado [183].
A implementação da execução dos adendos é feita da seguinte maneira: primeiramente o
compilador identifica os possíveis pontos de junção no programa e, de maneira geral, insere
uma chamada ao método correspondente ao adendo antes, depois ou em substituição ao ponto
de junção, de acordo com o tipo do adendo, se é anterior, posterior ou de contorno. Além
disso, como alguns pontos de junção só podem ser resolvidos em tempo de execução, para
estes casos são adicionados alguns resíduos no bytecode para fazer as verificações dinâmicas
necessárias. Um exemplo de resíduo pode ser uma instrução condicional if que checa o
tipo de determinado parâmetro, inserida antes da chamada ao método correspondente a um
adendo, quando a execução do adendo depende do tipo do parâmetro [183].
É necessário tomar cuidado para não confundir detalhes de implementação da lingua-
gem AspectJ com conceitos essenciais de POA. Deve ficar claro para o leitor que os méto-
dos em que os adendos definem comportamento adicional são sintaticamente inconscientes
(oblivious) da existência dos aspectos, apesar da estratégia utilizada pelo AspectJ de inse-
rir chamadas nos pontos de junção identificados no bytecode para representar as execuções
implícitas. O conceito de inconsciência (obliviousness) [138] está fortemente ligado à POA
e é considerado uma propriedade interessante do software, pois contribui para um menor
acoplamento dos módulos do sistema.
teste são aspectos de desenvolvimento, conforme definido na Seção 7.1.1. Nesta seção são
apresentados alguns exemplos de uso de aspectos para apoiar o teste de programas orientadas
a objetos. Deve-se notar, entretanto, que as técnicas apresentadas nesta seção podem também
ser aplicadas a programas orientados a aspectos.
Os imitadores virtuais de objetos [285] foram propostos como uma alternativa aos imitadores
de objetos (mock objects) usados principalmente em métodos ágeis de desenvolvimento de
software. Objetos imitadores são utilizados para simular o comportamento de outras unida-
des das quais uma unidade em teste depende e, assim, executar isoladamente seu teste, sem
interferências. As implementações dos imitadores de objetos são simples e devem retornar
valores fixos, como os módulos pseudocontrolados (stubs) usados em POO.
Para ilustrar o uso de objetos imitadores, considere um exemplo simples relacionado à
autenticação de usuários. Na Figura 7.2(a) é mostrado um exemplo de teste de uma unidade
orientada a objetos para a operação validar() [285]. Nesse sistema, a autenticação dos
usuários é iniciada quando os atributos idUsuario e senha são fornecidos pelo usuário e
a operação que autentica esses dois atributos é invocada. Essa operação delega a responsabi-
lidade de autenticar o usuário para o método autenticar(), que retorna um dos códigos
de estado: USUARIO_VALIDO, NENHUMA_SENHA, NENHUM_IDUSUARIO e USUA-
RIO_INVALIDO. O método validar() interpreta o código de estado e atualiza o atributo
situacao com a mensagem adequada para o usuário.
Na Figura 7.2(a) é apresentado o conjunto de classes de autenticação de usuário e tam-
bém uma classe de teste, usada no teste de unidade com apoio de uma ferramenta como o
JUnit. O diagrama de classes possui uma unidade em teste: o método validar() da classe
DialogoAcesso; as unidades das quais ela depende, no caso o método autenticar()
da classe ControladorAcesso; e casos de teste, como os métodos da classe Testar-
DialogoAcesso. O Programa 7.2 ilustra a aplicação de um caso de teste, realizada em
alguns passos: i) instanciação da classe que contém a unidade a ser testada; ii) definição de
valores dos atributos que a unidade precisa para ser testada; iii) invocação do método a ser
testado; e, iv) verificação do resultado da execução do método por meio de uma assertiva.
Programa 7.2
1 public void testarValidarUsuarioValido() {
2 DialogoAcesso dialogo = new DialogoAcesso();
3 dialogo.setIdUsuario("João");
4 dialogo.setSenha("senhaJoão");
5
6 dialogo.validar();
7
8 assertEquals("Usuário autenticado", dialogo.getSituacao());
9 }
*
objetos. A Figura 7.2(b) ilustra o uso dos imitadores de objetos para isolar a unidade em
teste. Os mesmos casos de teste são utilizados para testar o método validar(), porém a
184 Introdução ao Teste de Software ELSEVIER
Programa 7.3
1 public int autenticar(String IdUsuario, String senha) {
2 return USUARIO_VALIDO;
3 }
*
Pode-se notar que o uso de objetos imitadores pode causar dois problemas: i) a classe
DialogoAcesso deve ser alterada para que seu método validar() invoque o método
autenticar() da classe ImitadorControladorDeAcesso, em vez do método da
classe ControladorDeAcesso, o que exige alterações temporárias nas classes envolvi-
das; e, ii) o método autenticar() é alterado para retornar diferentes valores, dependendo
do usuário e da senha usados em cada um dos casos de teste, tornando o método mais com-
plexo.
A idéia dos imitadores virtuais de objetos é aproveitar a característica não invasiva dos
aspectos para simular o comportamento desejado para o teste, em vez de alterar o projeto
de classes do sistema e a implementação de seus métodos [285]. Conforme é mostrado
na Figura 7.2(c), o método da classe TesteBaseadoAspectos usa uma tabela de es-
palhamento (hashing) para armazenar associações entre os métodos, dos quais é desejado um
comportamento simulado, e seus resultados. O aspecto InterceptadorMetodo, Pro-
grama 7.4, é implementado para interceptar chamadas aos métodos invocados pela opera-
ção/método em teste e, com o auxílio da tabela de espalhamento, identificar quando um mé-
todo simulado é invocado para retornar o valor imitador e não permitir que o método original
seja executado. É interessante observar que o conjunto de junção todasAsChamadas es-
pecifica que devem ser selecionadas as execuções de todos os métodos do sistema, exceto
aqueles dentro da classe TesteBaseadoAspectos.
Programa 7.4
1 aspect InterceptadorMetodo{
2 pointcut todasAsChamadas(): execution(* *.*(..)) &&
3 !within(TesteBaseadoAspectos);
4
5 Object around() : todasAsChamadas(){
6 String nomeDaClasse;
7 nomeDaClasse =
8 thisJoinPoint.getSignature().getDeclaringType().getName();
9 Object recebedor = thisJoinPoint.getThis();
10 if (recebedor != null)
11 nomeDaClasse = recebedor.getClass().getName();
12 String nomeDoMetodo = thisJoinPoint.getSignature().getName();
13 Object valorDeRetorno;
14 valorDeRetorno =
15 TesteBaseadoAspectos.getValorRetornoImitador(nomeDaClasse,
16 nomeDoMetodo);
17 if (valorDeRetorno != null){
18 TesteBaseadoAspectos.indicarChamado(nomeDaClasse, nomeDoMetodo,
19 getArgumentos(thisJoinPoint));
7.2. Teste de programas OO apoiado por aspectos 185
20 return valorDeRetorno;
21 }
22 else{
23 return proceed();
24 }
25 }
26 private Hashtable getArgumentos(JoinPoint jp){
27 Hashtable argumentos = new Hashtable();
28 Object[] valoresDeArgumentos = jp.getArgs();
29 String[] nomesDeArgumentos;
30 nomesDeArgumentos =
31 ((CodeSignature)jp.getSignature()).getParameterNames();
32 for (int i = 0; i < valoresDeArgumentos.length; i++){
33 if (valoresDeArgumentos[i] != null)
34 argumentos.put(nomesDeArgumentos[i], valoresDeArgumentos[i]);
35 }
36 return argumentos;
37 }
38 }
*
Programa 7.5
1 public void testarValidarUsuarioValido() {
2 DialogoAcesso dialogo = new DialogoAcesso();
3
4 Integer resultadoImitador =
5 new Integer(ControladorAcesso.USUARIO_VALIDO);
6 setImitador("ControladorAcesso", "autenticar", resultadoImitador);
7
8 dialogo.setIdUsuario("João");
9 dialogo.setSenha("senhaJoão");
10 dialogo.validar();
11
12 assertEquals("Usuário autenticado", dialogo.getSituacao());
13 }
*
livres de alterações durante a atividade de teste, isto é, porque não é necessário introduzir
código de teste no componente.
No teste embutido em componente auxiliado por aspectos [48], os casos de teste são
implementados pelos aspectos e podem ser aplicados depois de combinados com as classes-
base. No exemplo apresentado no Programa 7.6, um aspecto é criado para implementar os
casos de teste do componente ListaUsuarios, presente na Figura 7.2(a). Com o objetivo
de auxiliar o teste, o aspecto introduz um atributo para monitorar o estado do componente.
Os casos de teste são implementados por meio de métodos introduzidos na classe em teste,
os quais complementam a interface do componente com operações genéricas e específicas
de teste, como, no exemplo, testar a inserção de um usuário na lista de usuários ou testar os
três estados possíveis da ListaUsuarios, no Programa 7.6. Por exemplo, o caso de teste
implementado no método inserido pelo aspecto testarInserirUsuario() testa se o
estado de classe ListaUsuarios é valido após a inserção de um usuário no sistema de
controle de acesso.
Programa 7.6
1 public aspect AspectoLista{
2 private String ListaUsuarios.estado = "Vazia";
3
4 public void ListaUsuarios.testarInserirUsuario(){
5 String idUsuario = "João";
6 String senha = "senhaJoão";
7 inserir(idUsuario, senha);
8 if (!apenasUm())
9 System.out.println("Problemas na inserção!");
10
11 }
12
13 public boolean ListaUsuarios.vazia(){
14 return nroElementos == 0;
15 }
16 public boolean ListaUsuarios.apenasUm(){
17 return nroElementos == 1;
18 }
19 public boolean ListaUsuarios.maisDeUm(){
20 return nroElementos > 1;
21 }
22
23 pointcut crescendo(ListaUsuarios lu) :
24 target(lu) && call(public void ListaUsuarios.inserir(..));
25
26 after(ListaUsuarios lu) : crescendo(lu) {
27 if (lu.estado.equals("Vazia"))
28 lu.estado = "Apenas um";
29 else if (lu.estado.equals("Apenas um"))
30 lu.estado = "Mais de um";
31 }
*
Transições de estado podem ser capturadas por meio de conjuntos de junção e tratadas
nas execuções dos adendos. No exemplo, o interesse de teste relacionado à transição de
estados é saber se o número de itens da ListaUsuarios cresce. Isso é feito por meio
do conjunto de junção crescendo(ListaUsuario lu) (linhas 23 e 24) e do adendo
after(ListaUsuario lu):crescendo(lu) (linhas 26-31), que ajustam o estado
da lista. Assim, é possível detectar se a inserção de elementos na lista está alterando de
7.2. Teste de programas OO apoiado por aspectos 187
Além de fornecer mecanismos para auxiliar o teste de software, a POA pode ser usada para
verificar a utilização de regras de codificação, boas práticas de programação e o uso adequado
de padrões de projeto [197, 270]. As verificações no código podem ser feitas tanto em tempo
de compilação quanto em tempo de execução e são muito úteis para a detecção de erros de
programação ou o não-cumprimento de padrões de codificação.
No Programa 7.7 é ilustrado o uso da POA para verificar uma regra de projeto [197].
Qualquer tentativa de verificação de usuário e senha por meio do método verificarSe-
nha(idUsuario, senha) da classe ListaUsuario – Figura 7.2(a) – deve ser feita
apenas pela classe ControladorAcesso ou, eventualmente, por qualquer uma de suas
subclasses. Para garantir essa regra, o aspecto ilustrado no Programa 7.7 exibirá um erro se
esse método for invocado por um método de qualquer outra classe.
Programa 7.7
1 public aspect AspectoRegras{
2 declare error: !withincode(* ControladorAcesso.*(..)) &&
3 call(public int ListaUsuarios.verificarSenha(..)):
4 "Use o método ControladorAcesso.autenticar().";
5 }
*
Outro exemplo de uso de aspectos pode ser observado no Programa 7.8, em que é feita
a verificação de um padrão de codificação tradicional da programação orientada a objetos:
atributos devem possuir métodos de acesso que permitam sua leitura ou escrita para manter
o encapsulamento [197]. Qualquer tentativa de escrita nos atributos sem a utilização de um
método iniciado com ‘set’ – método padrão para a escrita de atributos – resulta em um aviso
emitido pelo AspectJ.
Programa 7.8
1 public aspect AspectoPadroes{
2 declare warning: set(!public * *) &&
3 !withincode(* set*(..)):
4 "Escrevendo atributo fora do método correspondente.";
5 }
*
A técnica de projeto por contrato [281] consiste na definição de invariantes, pré e pós-
condições para definir responsabilidade e pode ser aplicada com o auxílio da programação
orientada a objetos. A utilização da técnica nem sempre é simples, já que, para garantir os in-
variantes, pré e pós-condições, pode ser necessário fazer verificações em tempo de execução.
Além disso, sem a POA, o código para a verificação fica entrelaçado com o código funcional
188 Introdução ao Teste de Software ELSEVIER
de várias classes. O uso de pré e pós-condições também auxilia a atividade de teste, pois
auxilia a revelar alguns tipos de defeitos baseados no estado do sistema antes e depois da
execução.
É importante lembrar: satisfazer uma precondição não implica diretamente a invocação
de um método, apenas indica que o método pode ser invocado a partir de um estado válido
e que vai resultar em um estado que atenda às pós-condições especificadas, permanecendo
em estado válido. Um exemplo de verificação de precondições é mostrado no Programa 7.9.
O método autenticar() da classe ControladorAcesso() – Figura 7.2(a) – tem
como precondição não receber como argumento valores nulos, verificada por meio do uso
do adendo anterior, que lança uma exceção caso ela não seja atendida. Com a precondição
atendida, a pós-condição do método é retornar um valor de autenticação válido: USUA-
RIO_VALIDO, NENHUMA_SENHA, NENHUM_IDUSUARIO e USUARIO_INVALIDO;
caso contrário, o adendo posterior lança uma exceção que indica violação da pós-condição.
Programa 7.9
1 public aspect AspectoChecagemPrePos{
2 pointcut autenticar(String idUsuario, String senha):
3 call(int ControladorAcesso.autenticar(String, String))
4 && args(idUsuario, senha);
5
6 before(String idUsuario, String senha):autenticar(idUsuario, senha){
7 if ((idUsuario==null) || (senha==null))
8 throw new NullPointerException("Parâmetro inválido.");
9 }
10 after (String idUsuario, String senha) returning (int resultado):
11 autenticar(idUsuario, senha){
12 if ((resultado!=ControladorAcesso.USUARIO_VALIDO) &&
13 (resultado!=ControladorAcesso.NENHUMA_SENHA) &&
14 (resultado!=ControladorAcesso.NENHUM_IDUSUARIO) &&
15 (resultado!=ControladorAcesso.USUARIO_INVALIDO))
16 throw new RuntimeException("Resultado de autenticação inválido.");
17 }
*
O invariante de uma classe deve estabelecer uma regra que deve ser verdadeira durante
todo o ciclo de vida das instâncias da classe, e qualquer violação da regra leva a um estado
inválido. No Programa 7.10 é apresentado um exemplo de invariantes, que verifica se o
atributo ‘situacao’ possui um valor permitido.
Programa 7.10
1 public aspect AspectoChecagemInvariante{
2 pointcut checarSituacao(String s):
3 set(private String DialogoAcesso.situacao) && args(s);
4
5 after (String s): checarSituacao(s){
6 if ((s!="Usuário autenticado") &&
7 (s!="Usuário não autenticado") &&
8 (s!="Usuário não fornecido") &&
9 (s!="Usuário não autenticado") &&
10 (s!=null))
11 throw new RuntimeException("Situação inválida.");
12 }
13 }
*
7.3. Teste de programas orientados a aspectos 189
Programa 7.11
1 public aspect AspectoRastrear{
2 pointcut monitorarAutenticar(String idUsuario, String senha):
3 call(int ControladorAcesso.autenticar(String, String)) &&
4 args(idUsuario, senha);
5
6 pointcut monitorarAtributo(String s, DialogoAcesso da):
7 set(private String DialogoAcesso.idUsuario) &&
8 args(s) &&
9 target(da);
10
11 before(String idUsuario, String senha):
12 monitorarAutenticar(idUsuario, senha){
13 System.out.print("O método autenticar foi invocado com: ");
14 System.out.println(idUsuario + " senha:" + senha + ".");
15 }
16
17 before(String s, DialogoAcesso da): monitorarAtributo(s, da){
18 System.out.print("O atributo idUsuario alterado de : ");
19 System.out.print(da.getIdUsuario());
20 System.out.print(" para ");
21 System.out.println(s + ".");
22 }
23 }
*
Programa 7.12
1 O atributo idUsuario alterado de: para João.
2 O método autenticar foi invocado com: João senha:senhaJoão.
*
Neste texto considera-se que em programas OA que são extensões de programas OO (como
acontece nos escritos em AspectJ), as menores unidades a serem testadas são os métodos
(inclusive os introduzidos por aspectos) e também os adendos.
Por definição, uma classe engloba um conjunto de atributos e métodos que manipulam
esses atributos; e um aspecto engloba basicamente conjuntos de atributos, métodos, adendos
e conjuntos de junção. Assim sendo, considerando uma única classe ou um único aspecto, já é
possível pensar em teste de integração. Métodos da mesma classe ou do mesmo aspecto, bem
como adendos e métodos de um mesmo aspecto podem interagir entre si para desempenhar
funções específicas, caracterizando uma integração que deve ser testada.
Se levadas em conta tais considerações, a atividade de teste de programas OA poderia ser
dividida nas seguintes fases:
O teste de programas OA interunidades tanto de módulos (no caso dos aspectos) quanto de
componentes, quando considerado par a par, pode ser classificado em: intermétodo, método-
adendo, adendo-método e interadendo, de acordo com os tipos das unidades que interagem
entre si.
No que diz respeito ao teste de unidade, a representação mais conhecida para o estabele-
cimento de critérios de fluxo de controle é o Grafo de Fluxo de Controle (GFC). Para critérios
de fluxo de dados é utilizado o Grafo Def-Uso, uma extensão do GFC com informação adici-
onal sobre definições e usos de variáveis em cada nó e aresta do grafo.
Vincenzi et al. [415] definiram modelos de fluxo de dados baseados em bytecode Java
para o teste estrutural de unidade de programas orientados a objetos implementados em Java.
Nesses modelos, a informação sobre o fluxo de dados é obtida baseando-se em uma classifi-
cação das instruções de bytecode. A partir desse trabalho, Lemos et al. definiram um modelo
para testar unidades de programas orientados a aspectos a partir do bytecode [235, 237]. O
Grafo Def-Uso orientado a aspectos (AODU) é o modelo de fluxo de dados definido com
o propósito de aplicar critérios de teste estrutural de unidade para programas orientados a
aspectos escritos em AspectJ.
Para o teste de unidade desses programas, um grafo desse tipo deve ser gerado para cada
método, método intertipo declarado e adendo [237], que são as unidades a serem testadas. O
grafo AODU é construído com base em um Grafo de Instrução GI. Informalmente o Grafo
de Instrução é um grafo no qual os nós contêm uma única instrução de bytecode e as arestas
conectam instruções que podem ser executadas uma após a outra (inclusive quando há des-
vios). Informações de fluxo de dados também são obtidas para cada nó de GI [237, 415].
A partir daí, um grafo AODU de uma dada unidade u é definido como um grafo dirigido
AODU (u) = (N, E, s, T, C):
Na Figura 7.3 é mostrado o grafo AODU para representar o fluxo de controle e de da-
dos do adendo de contorno do aspecto de transferência. O código é apresentado no Pro-
grama 7.13. O adendo de contorno tem a função de verificar se o cliente chamado transferiu
para algum outro cliente o recebimento de suas chamadas, e se esse último também o fez,
e assim por diante. O cliente chamado é então mudado para o transferido, se não ocorrer
nenhum ciclo de transferências (ou seja, o cliente chamador ser o chamado). No fim do
adendo é criada uma conexão entre o chamador e o transferido, se este existir, instanciando
uma conexão local ou de longa distância de acordo com o código de área dos clientes. Como
o adendo do aspecto de faturamento define comportamento no momento da criação de uma
conexão, o adendo de contorno é afetado por ele, como pode ser verificado a partir dos nós
transversais 82 e 159.
Programa 7.13
1 public aspect Transferencia {
2
3 ...
4 Conexao around (Cliente chamador, Cliente chamado, boolean cel) :
5 criaConexao(chamador, chamado, cel) {
6
194 Introdução ao Teste de Software ELSEVIER
7 Conexao c;
8
9 if (chamador == chamado)
10 throw new RuntimeException("Chamador não pode ser o chamado!");
11
12 ArrayList a = new ArrayList();
13
14 Cliente tc = chamado;
15
16 while (chamado.temClienteTransferencia()) {
17 if(a.indexOf(chamado) >= 0)
18 throw new RuntimeException("Ciclo de transferências!");
19 else {
20 a.add(chamado);
21 chamado = chamado.getClienteTransferencia();
22 }
23 }
24
25 if(tc != chamado) {
26 if (chamado.local(chamador)) {
27 System.out.println("Transferindo chamada de " + tc + " para "
28 + chamado);
29 c = new Local(chamador, chamado, cel);
30 }
31 else {
32 System.out.println("Transferindo chamada de " + tc + " para "
33 + chamado);
34 c = new LongaDistancia(chamador, chamado, cel);
35 }
36 }
37 else c = proceed(chamador, chamado, cel);
38 return c;
39 }
40 }
*
• Todos-Nós-Transversais (Todos-Nósc )
– Π satisfaz o critério Todos-Nós-Transversais se cada nó ni ∈ C está incluído em
Π. Em outras palavras, esse critério requer que cada nó transversal, e, portanto,
cada execução de adendo que ocorre na unidade afetada seja exercitado pelo me-
nos uma vez por algum caso de teste de T.
No exemplo de grafo mostrado na Figura 7.3, os requisitos de teste referem-se aos nós
transversais 82 e 159. Assim, seriam necessários casos de teste que passassem por cada um
deles, para que o critério fosse satisfeito.
O critério Todas-Arestas pode ser definido no contexto de programas OA da seguinte
maneira:
Da mesma maneira que existem nós especiais no AODU– os nós transversais –, pode-
se considerar também a existência de arestas especiais, que conectam os nós transversais.
Do ponto de vista do teste, é interessante também a informação de quando tais arestas são
exercitadas, seguindo a mesma idéia do critério Todos-Nós-Transversais. Essa informação é
interessante porque um defeito poderia ser revelado somente quando uma aresta transversal
em particular fosse escolhida. Portanto, define-se o seguinte critério:
7.3. Teste de programas orientados a aspectos 197
• Todas-Arestas-Transversais (Todas-Arestasc )
– Π satisfaz o critério Todas-Arestas-Transversais se cada aresta ec ∈ Ec está in-
cluída em Π. Em outras palavras, esse critério requer que cada aresta do grafo
AODU que tem um nó transversal como nó início ou destino seja exercitada por
algum caso de teste de T.
No exemplo de grafo mostrado na Figura 7.3, os requisitos de teste desse critério referem-
se às arestas transversais (74, 82), (74, 159), (82, 153) e (159, 230). Assim, seriam necessá-
rios casos de teste que passassem por cada uma delas, para que o critério fosse satisfeito.
No que diz respeito ao fluxo de dados, o critério Todos-Usos foi revistado para ser apli-
cado nesse contexto:
• Todos-Usos-Transversais (Todos-Usosc )
– Π satisfaz o critério Todos-Usos-Transversais se para cada nó i ∈ def(i), Π inclui
um caminho livre de definição para x de i a cada elemento de dcu(x, i) que é um
nó transversal e a todos elementos de dpu(x, i) nos quais o nó origem da aresta é
um nó transversal. Em outras palavras, esse critério requer que cada par def-c-uso
(i, j, x) em que j ∈ dcu(x, i) e j ∈ C e cada par def-p-uso (i, (j, k), x) em que
(j, k) ∈ dpu(x, i) e j ∈ C seja exercitado pelo menos uma vez por algum caso
de teste de T.
198 Introdução ao Teste de Software ELSEVIER
Na Tabela 7.1 são mostrados os requisitos de teste derivados para o exemplo de grafo
AODU mostrado anteriormente, que corresponde ao adendo de contorno do aspecto de Transferencia
(Programa 7.13).
Tabela 7.1 – Requisitos de teste gerados a partir do grafo AODU do adendo de contorno do
aspecto Transferencia
Critério de teste Conjunto de requisitos
Todos-Nós-Transversais {159, 82}
Todas-Arestas-Transversais {(159, 230), (82, 153), (74, 159), (74, 82)}
Todos-Usos-Transversais {(tc, 15, 159), (chamado, 49, 159), (cel, 0, 82),
(chamado, 0, 82), (chamador, 0, 82), (cel, 0, 159),
(chamado, 0, 159), (chamador, 0, 159), (tc, 15, 82),
(chamado, 49, 82)}
verificar quais deles ainda não foram cobertos por algum caso de teste e então desenvolver um
novo caso de teste que cubra tais requisitos. Nas Figuras 7.4(a), 7.4(b) e 7.4(c) são ilustrados
os requisitos de teste do método LineSegment.distance apresentado no Programa 7.1,
gerados pelos critérios All-Nodes-c, All-Edges-c, e All-Uses-c, respectivamente.
(c) All-Uses-c
A partir dos requisitos dos critérios OA, o testador pode então criar conjuntos de casos de
teste para adequá-los aos critérios. Por exemplo, no caso do adendo mostrado anteriormente,
para cobrir todos os nós transversais deveriam ser criados dois casos de teste. Esses casos
deveriam executar o método da classe que constrói os objetos de conexão FabricaCone-
xao.criarConexao, sendo que o cliente chamado deveria ter escolhido outro cliente
200 Introdução ao Teste de Software ELSEVIER
para receber suas ligações. Um dos casos de teste teria um cliente transferido em uma área
diferente da do chamador – longa distância – e outro na mesma área – local.
A partir daí, o testador poderia continuar verificando cada um dos requisitos restantes
gerados a partir dos critérios oferecidos pela ferramenta. Outra característica interessante da
ferramenta JaBUTi é que os casos de teste podem ser escritos em JUnit e importados para o
projeto de teste.
O teste estrutural deve ser aplicado tanto nos adendos quanto nos métodos, para que
seja analisada a cobertura tanto para o código-base quanto para os aspectos. A ferramenta
ainda compõe sumários dos requisitos de teste de diferentes tipos: por classe/aspecto, por
método/adendo e por critério.
JaBUTi/AJ é uma ferramenta com protótipo operacional implementada em meio acadê-
mico; entretanto, poderia ser utilizada para aumentar a qualidade dos sistemas de software
orientados a aspectos desenvolvidos também no meio industrial.
O teste baseado em estados para programas orientados a aspectos [449] consiste em derivar
casos de teste a partir de modelos de estados. A proposta estende o método de teste de pro-
gramas OO denominado FREE (Flattened Regular Expression) para representar não apenas
classes mas também aspectos, por meio de um modelo aspectual de estados – ASM (Aspec-
tual State Model). Tanto o FREE como o ASM usam a interpretação de Statecharts da UML
– Unified Modeling Language – com extensões específicas para a atividade de teste.
O ASM permite que, primeiramente, as classes-base sejam modeladas usando a notação
do FREE e, posteriormente, os aspectos sejam modelados usando a própria notação. Dessa
forma, a proposta do teste baseado em estados apóia tanto o teste incremental, por meio do
FREE e do ASM, como o teste conjunto de classes e aspectos.
Os objetos são entidades que enviam mensagens para outros objetos e recebem mensagens
de outros objetos. O seu comportamento é determinado pelas interações, dependências e
limitações nas seqüências das mensagens descritas no ASM que, portanto, descreve o estado
e o comportamento dinâmico dos objetos.
A Figura 7.5(a) apresenta um modelo FREE para um objeto que representa uma conta
bancária, cujos estados são: Aberta, Bloqueada, Inativa e Fechada e cujas tran-
sições são: abrir, bloquear, debitar, creditar, obterSaldo, desativar e
fechar. As relações de estado-transição determinam as mudanças de estado possíveis dos
objetos da classe Conta.
A Figura 7.5(b) mostra o ASM para a classe Conta, com alterações decorrentes das
seguintes mudanças nos requisitos, implementadas com o uso de aspectos:
• o proprietário da conta pode efetuar transações com saldo negativo, desde que o saldo
permaneça dentro de um limite preestabelecido. Nesse estado, denominado Devedora,
o cliente deve pagar juros sobre o saldo devedor.
• os débitos na conta devem ser monitorados para verificar se as transações com saldo
devedor estão dentro do limite e, caso contrário, levar ao estado Bloqueada.
7.3. Teste de programas orientados a aspectos 201
• na classe Conta original, o estado Inativa é alcançado quando uma conta per-
manece sem movimentações por um prazo de cinco anos, porém uma nova condição
estabelece que a conta se torna inativa quando permanece sem movimentações durante
cinco anos e quando o saldo é menor que um valor mínimo, o SALDO_MINIMO.
Conforme se pode observar na Figura 7.5, as classes-base são modeladas usando FREE,
e os aspectos são modelados por meio de extensões da notação (ASM) que representam con-
juntos de junção, pontos de junção e adendos. Os elementos gráficos da Figura 7.5(b) consti-
tuídos de linhas tracejadas são estados e transições implementados com construções sintáticas
da POA. Os conjuntos de junção afetam as transições de três diferentes formas: pontos de
junção de entrada, pontos de junção de saída e pontos de junção de contorno. Uma entrada
em um ponto de junção é representada por uma linha tracejada, que inicia com um losango
em uma transição e termina em um estado. Um exemplo de ponto de junção de entrada
pode ser observado na transição debitar(valor), que leva do estado Aberta ao es-
tado Aberta, Figura 7.5(b). Nessa transição existe um ponto de junção de entrada com a
condição [saldo+LIMITE-valor<0] que, caso satisfeita, leva ao estado Bloqueada.
Naquela transição também existe um ponto de junção de saída, que leva ao estado Devedora
caso a condição de guarda [saldo+LIMITE-valor>=0] seja atendida.
Um ponto de junção de contorno é representado por uma linha tracejada, que inicia com
um círculo e leva a um estado. Adicionalmente, um X marca a transição que é interceptada
por esse tipo de conjunto de junção para indicar um fluxo condicional entre a execução da
transição e a execução do adendo relacionado ao conjunto de junção. O ponto de junção
de contorno pode ser observado na transição desativar, que leva do estado Aberta para o
estado Inativa caso a condição de guarda [anoAtual-ultimoAno>5 && saldo <
SALDO_MINIMO] seja atendida.
É apresentada na Figura 7.6(a) a árvore de transição de estados do ASM da Figura 7.5(b),
na qual é possível observar que a raiz da árvore é o estado inicial Aberta. Essa árvore é
gerada percorrendo-se todos os caminhos possíveis que partem do estado inicial do ASM da
202 Introdução ao Teste de Software ELSEVIER
valores adequados sejam atribuídos ao saldo, que é o saldo inicial, e aos parâmetros valor1
e valor2.
Além dos casos de teste que têm por objetivo testar o comportamento esperado dos as-
pectos, com as condições de guarda atendidas, é necessário testar os aspectos com casos de
teste que não atendam as condições associadas às transições. Para isso, é necessário analisar e
identificar casos de teste adicionais para cada condição de transição. A estratégia empregada
nesta tarefa é o critério de cobertura multicondicional, que tem o objetivo de exercitar todas
as possibilidades de disparo de uma transição. Por exemplo, todas as condições possíveis da
transição debitar() do estado Aberta estão presentes na árvore de transições. Esse não
é o caso da transição desativar, como se pode observar na Tabela 7.2. A cobertura mul-
ticondicional para (anoAtual-ultimoAno)<=5 && saldo < SALDO_MINIMO re-
quer que outras transições sejam incluídas na árvore de transições da Figura 7.6(a), resultando
na árvore de transições da Figura 7.6(b).
ORD={A, B, C, D, E, F, G}
1. {A, C, E, F, D, H, B} : aresta de maior peso A→C=9
1.1 {A}
1.2 {B, C, E, F, D, H} : aresta de maior peso H→B=9
1.2.1 {C, E, F, D, H} : aresta de maior peso E→F=4
1.2.1.1 {E}
1.2.1.2 {C, H} : aresta de maior peso C→H=1
1.2.1.2.1 {C}
1.2.1.2.2 {H}
1.2.1.3 {D}
1.2.1.4 {F}
1.2.2 {B}
2. {G}
(b) Aplicação da estratégia de Briand et al.
origem pelas arestas de saída do vértice destino de cada aresta de associação. Esses passos
devem ser seguidos até não existirem mais CFCs não-triviais.
A Figura 7.7(b) mostra um exemplo da aplicação da estratégia em que é possível observar
em um ORD as arestas que foram removidas para a quebra dos ciclos. Nesse exemplo, após
a aplicação inicial do algoritmo de Tarjan, são encontrados o CFC não trivial {A, C, E, F, D,
H, B} e o CFC trivial {G}. O peso das arestas é, então, calculado:
devem ser adicionados e testados um a um para que se possam realizar as atividades do teste
de integração.
Em orientação a aspectos, os tipos de dependência entre aspectos e entre classes e as-
pectos são diferentes dos tipos de dependência existentes em orientação a objetos. O tipo de
dependência gerada por relacionamentos de herança e por relacionamentos de associação é
encontrado em aspectos, ao contrário da agregação, que não existe no contexto da orientação
a aspectos. Da mesma forma, a semântica e os diferentes elementos sintáticos da orienta-
ção a aspectos não possuem correspondente similar em orientação a objetos.
Os tipos de dependência são caracterizados por conjuntos de junção, adendos, declara-
ções intertipo, herança e métodos pertencentes aos aspectos. De forma geral, os aspectos são
dependentes das classes porque elas precisam existir para que o aspecto seja testado. No en-
tanto, alguns autores [60, 210, 211, 338, 391] identificarm casos em que as classes possuem
conhecimento dos aspectos que as entrecortam, o que é denominado inconsciência incom-
pleta [138]. A inconsciência incompleta gera dependência circular [211] entre as classes e
os aspectos, que é tratada nesse trabalho como dependência bidirecional. Um exemplo de
dependência bidirecional pode ser encontrado na Figura 7.8, em que o aspecto Aa depende
da classe D e esta depende do aspecto Aa.
Dos vários elementos sintáticos da orientação a aspectos, os tipos de dependência que
diferem da orientação a objetos podem ser categorizados em dois grupos: i) dependências
geradas por conjuntos de junção; e ii) dependências geradas por declarações intertipo. Os
conjuntos de junção e as declarações intertipo geram tipos de dependência que não são con-
sideradas em um ORD tradicional e são representadas em um ORD estendido por arestas do
tipo ‘P’ e do tipo ‘IT’, respectivamente (Figura 7.8).
Figura 7.10 – Ordem de teste de aspectos e classes sugerida pela estratégia de Briand et al.
ao ORD da Figura 7.8.
tante notar que se houver um framework que implemente o interesse bidirecional, como o
de persistência [338, 401], esta estratégia pode ser eficiente, pois implementam-se as classes
na ordem sugerida pela estratégia de Briand et al. e instancia-se o framework no momento
necessário, pois, em princípio, ele já está testado. Outra estratégia é quebrar a dependência bi-
direcional, transformando o aspecto em dois, cada um dependente da classe que, por sua vez,
não tem consciência dos aspectos, e pode ser desenvolvida e testada primeiramente [211].
Caso se considere uma estratégia em que aspectos e classes são desenvolvidos conjunta-
mente, em determinadas situações algumas classes podem ser testadas depois de um conjunto
de aspectos, o que só acontece quando elas têm consciência dos aspectos. Nesse caso, o al-
goritmo de Briand et al. estabelece a ordem de implementação e teste de classes e aspectos,
minimizando tanto o número de módulos pseudocontrolados de classes assim como o número
de aspectos pseudocontrolados.
8.1 Introdução
Originalmente, aplicações Web possuíam o objetivo de apenas apresentar informações que
consistiam basicamente em documentos no formato de texto [91]. Os Web sites eram com-
postos de arquivos HTML (Hypertext Markup Language) estáticos. A arquitetura utilizada
era a cliente-servidor duas camadas: um cliente Web browser usado para visitar Web sites que
residem em diferentes computadores servidores que enviam arquivos HTML para o cliente.
Entretanto, as aplicações Web tornaram-se aplicações de software para comércio eletrô-
nico, distribuição de informações, entretenimento, trabalho cooperativo e numerosas outras
atividades [304]. Elas estão cada vez mais complexas e são utilizadas em áreas críticas na
grande maioria das empresas. Se essas aplicações falham, os prejuízos são enormes. Por
isso, a garantia da qualidade e da confiabilidade é crucial, e a demanda por metodologias e
ferramentas para realizar o teste de aplicações Web é crescente.
A arquitetura e as tecnologias envolvidas em uma aplicação Web mudaram drasticamente
nos últimos anos. A configuração foi estendida do modelo cliente-servidor duas camadas
para várias camadas e, embora muito semelhante às aplicações cliente-servidor tradicionais,
o teste de aplicações Web é um pouco mais complicado, pois existem outros aspectos a con-
siderar. O principal deles é que aplicações Web são dinâmicas e heterôgeneas.
A palavra heterôgenea é comumente utilizada para designar as diversas maneiras utili-
zadas pelos componentes de software para se comunicarem e as diferentes e recentes tec-
nologias envolvidas [230, 247, 248]. Além disso, deve-se considerar que os componentes
encontram-se, na maioria das vezes, geograficamente distribuídos. Eles incluem software
tradicional, programas em linguagens de scripts, HTML, base de dados, imagens gráficas
e interfaces com o usuário complexas. Entre as principais tecnologias envolvidas, Conal-
len [91] destaca:
Existem inúmeras outras tecnologias utilizadas em aplicações Web. O uso dessas tecnolo-
gias contribui para aumentar a flexibilidade, assim como a complexidade. O aspecto dinâmico
das aplicações Web também gera novos desafios para o desenvolvimento de software e para a
atividade de teste [445], tais como:
• o controle da execução é diretamente afetado pelo usuário. Por exemplo, ele pode, ao
pressionar um botão, modificar totalmente o contexto de execução do programa;
• mudanças no cliente e no servidor ocorrem freqüentemente. Programas e dados no
lado do cliente podem ser gerados e alterados dinamicamente;
• os componentes de hardware e software são heterôgeneos e devem ser integrados di-
namicamente. Isso faz com que atributos de qualidade pouco críticos em aplicações
tradicionais, tais como compatibilidade e interoperabilidade, sejam fundamentais em
aplicações Web;
8.2. Teste estrutural de aplicações Web 211
Embora esses aspectos tornem a aplicação Web muito diferente das aplicações tradicio-
nais, algumas abordagens de teste aplicadas ao software tradicional podem ser adaptadas ou
modificadas. Offutt [304] diz que o fato de as aplicações Web coletarem, processarem e
transmitirem dados pode ser utilizado para que o teste estrutural baseado em fluxo de dados
seja explorado. Vários trabalhos exploram este contexto [230, 247, 248, 343]. A maioria
das aplicações Web é orientada a objetos; portanto, os testes inter e intraclasse podem ser
aplicados [230, 247, 248]. As próximas seções deste capítulo têm como objetivo descrever
trabalhos que buscam estender as técnicas de teste existentes para permitir o teste de aplica-
ções Web.
• página cliente: documento HTML ou XML com scripts embutidos, visualizado por
meio de um navegador Web no lado cliente;
• página servidora: script CGI (Common Gateway Interface), ASP (Active Server Page),
JSP (Java Server Page) ou um servlet executado pelo servidor Web no lado servidor;
• componente: template HTML, Java applet, ActiveX Control, Java Bean ou qualquer
módulo de programa que interaja com uma página cliente, com uma página servidora
ou com outro componente.
• I – relação de herança;
• Ag – relação de agregação;
• As – relação de associação;
• Req – relação de requisição entre uma página cliente e uma página servidora;
O diagrama ORD é um grafo cujos nós representam os objetos e cujos ramos representam
os relacionamentos entre os objetos. A Figura 8.1 apresenta um exemplo de ORD para uma
aplicação Web, que busca informações sobre o acervo de uma biblioteca, na qual o usuário
fornece o autor, o título ou o assunto para verificar se o livro está cadastrado no acervo, via
página-cliente Bib. A página-servidora Bib solicita ao componente BuscaBib a verificação
do acervo, gerando a página-cliente BibCompleta com o resultado da busca, que pode levar
a informações sobre o livro por meio de requisição à página-servidora InfBib. A página-
servidora InfBib pede ao componente BuscaInfBib o carregamento das informações, gerando
a página-cliente InfBibCompleta. As páginas geradas possuem ligação para a página-cliente
Bib.
Para capturar informações sobre os fluxos de controle e de dados da aplicação Web são
utilizados quatro tipos de grafos, muito similares aos utilizados em aplicações tradicionais,
nos testes de unidade e de integração e no teste de software orientado a objetos.
Grafo de Fluxo de Controle (GFC – Control Flow Graph) – contém informações sobre o
fluxo de controle e sobre definição e uso de variáveis. Um grafo de fluxo de controle é
construído para cada função.
8.2. Teste estrutural de aplicações Web 213
O COSD é usado para gerar casos de teste capazes de detectar defeitos associados ao
comportamento dependente do estado da interação entre objetos de uma aplicação Web. A
Figura 8.5 mostra a árvore de teste obtida a partir do COSD da Figura 8.3. Na figura, C é a
página cliente Bib, S é a página servidora Bib e B é o componente BuscaBib. Ao observar
essa árvore de teste pode ser identificado um possível caso de teste.
Exemplo 2: Caso de teste obtido a partir da árvore de teste da Figura 8.5.
• Página Web – contém informações visualizadas pelo usuário, ligações para outras pá-
ginas, formulários e frames.
• Página Estática – página Web com conteúdo fixo.
• Página Dinâmica – página Web com conteúdo dependente do processamento realizado
no servidor e de informações providenciadas pelo usuário.
• Frame – região na página Web, cuja navegação pode ser independente.
8.3. Teste baseado em modelos de especificação 217
• Formulário – conjunto de variáveis de entrada, fornecidas pelo usuário para gerar uma
página dinâmica.
• Ramo Condicional – condição que provoca alterações na página Web quando satisfeita,
por meio de valores de entrada.
• Páginas inalcançáveis – páginas disponíveis no servidor que não são alcançadas a partir
da página inicial.
• Teste de Página (page testing) – cada página na aplicação deve ser visitada pelo menos
uma vez em algum caso de teste.
8.3. Teste baseado em modelos de especificação 219
• Teste de ligação (hiperlink testing) – cada ligação de cada página na aplicação deve ser
percorrida pelo menos uma vez.
• Teste de Definição-Uso (definition-use testing) – todos os caminhos de navegação que
exercitam cada associação definição-uso devem ser exercitados.
• Teste de Todos-Usos (all-uses testing) – pelo menos um caminho de navegação que
exercita cada associação definição-uso deve ser exercitado.
• Teste de Todos-Caminhos (all-paths testing) – cada caminho na aplicação deve ser
percorrido pelo menos uma vez em algum caso de teste.
• gerar um grafo, baseado no modelo de teste, no qual os nós são as páginas Web e os
seus componentes internos, e as ligações são os relacionamentos entre os nós;
A abordagem de teste proposta por Lee e Offutt [233] aplica análise de mutantes [115] para
a validação da interação de dados por meio de mensagens XML entre componentes de apli-
cações Web. Essa abordagem de teste é denominada mutação de interação.
Qualquer processo de software ou combinação de processos executados na Web é definido
como componente Web. Esta definição inclui: Java Server Pages, Java Servlets, JavaScripts,
Active Server Pages, bases de dados e outros.
Uma mensagem XML pode ser vista como uma árvore, na qual os vértices V representam
nomes de elementos e nomes de atributos, e as folhas representam conteúdo. Os nomes dos
elementos e os nomes de atributos são definidos em um esquema específico, nesse caso, em
uma DTD.
A abordagem de teste de mutação de interação usa um modelo de especificação de in-
teração (Interaction Specification Model – ISM) para auxiliar na geração dos mutantes de
mensagens XML. Esse modelo é composto da DTD, que especifica a estrutura da mensagem
XML, de uma interação entre componentes Web (mensagem XML de requisição e mensa-
gem XML de resposta) e do conjunto de restrições XML.
Além de estabelecer um modelo de interação baseado nas mensagens XML trocadas pe-
los componentes da aplicação Web, é necessário criar operadores de mutação. Os operadores
de mutação de interação (Interaction Mutation Operators – IMOs) são regras aplicadas às
mensagens XML para obtenção de mutantes, ou seja, de mensagens XML modificadas se-
gundo as regras. Mensagens XML contêm elementos e atributos, especificados em uma DTD
conforme o domínio da aplicação. Portanto, não é possível criar operadores de mutação que
trabalhem em qualquer aplicação. Dessa forma, as restrições especificadas no modelo de in-
teração são usadas para definir classes genéricas de operadores de mutação que podem ser
empregadas em aplicações baseadas em DTDs diferentes. As classes são instanciadas para
gerar operadores específicos para cada DTD. Inicialmente, duas classes de operadores de
mutação foram desenvolvidas baseadas em restrições XML:
interação por meio das classes genéricas de operadores de mutação, usadas para que sejam
detectados na DTD elementos nos quais as restrições possam ser aplicadas. No teste, os casos
de teste são executados para a interação entre os componentes Web que está sendo testada. O
resultado obtido com a execução das mensagens XML mutantes é analisado, de modo que os
mutantes sejam classificados como vivos, mortos ou equivalentes.
Considere a aplicação Web que busca informações sobre o acervo de uma biblioteca; su-
ponha que para acessar essa aplicação um usuário precise fornecer um nome de usuário e
uma senha, que devem ser validados. A mutação de interação poderia ser aplicada na mensa-
gem de requisição de validação do usuário. A classe genérica do operador de mutação NOT
memberOf poderia ser empregada no elemento usuário associado ao elemento senha, su-
pondo que essa associação tenha sido especificada no modelo de interação. A classe genérica
do operador de mutação NOT lenOf poderia ser empregada no elemento senha, supondo
que esse elemento tenha sido especificado no modelo de interação com lenOf igual a seis.
Exemplo 4: Mensagens XML de requisição original e mutantes, conforme os operadores
de mutação NOT memberOf e NOT lenOf, e mensagens XML de resposta corresponden-
tes. Veja Figura 8.8.
O trabalho de Offutt e Xu [315] explora a perturbação de dados em XML para gerar casos de
teste para aplicações que utilizam serviços Web. Nessa abordagem a interação entre pares de
serviços Web é testada por meio da mutação de mensagens de requisição em formato XML.
O modelo de interação de serviços Web considerado no teste é dois a dois, ou seja, dois
serviços Web se comunicando por meio de chamada de procedimento remota (Remote Proce-
dure Call – RPC) ou documento XML usando SOAP.
8.4. Teste baseado em defeitos 223
Considerando ainda a aplicação Web que busca informações sobre o acervo de uma bi-
blioteca, suponha que a validação do usuário e da senha seja realizada por meio de mensagem
SOAP, na qual os conteúdos dos elementos relacionados a usuário e senha são usados em uma
consulta SQL. A seguir é dado um exemplo de uso do operador Unauthorized(), nesse
contexto.
Exemplo 5: Corpo das mensagens SOAP de requisição original e mutante, conforme o
operador de mutação Unauthorized(), e consulta SQL usando o conteúdo modificado
por esse operador de mutação. Veja Figura 8.9.
A perturbação de comunicação de dados (Data Communication Perturbation – DCP)
está baseada no teste de relacionamento e restrições definidas para os dados em formato
XML por meio do esquema. A manipulação de relacionamento e restrições aos dados é
224 Introdução ao Teste de Software ELSEVIER
feita usando um modelo formal RTG (Regular Tree Grammar), que representa um esquema
e deriva documentos XML. O modelo formal possui um conjunto finito de: elementos, tipos
de dados, atributos, não-terminais e regras de produção que determinam os relacionamentos
e as restrições.
O teste quanto ao relacionamento está focado na integridade referencial entre elementos-
pai e elementos-filho especificada no esquema XML por meio de operadores que indicam a
quantidade máxima de ocorrência de um elemento; esses operadores são: “?” (zero ou um),
“+” (pelo menos um) e “*” (zero ou mais). Com base nesses operadores, são gerados os casos
de teste:
• operador “?” – gera dois casos de teste, um contendo uma instância do elemento as-
sociado ao operador e outro contendo uma instância vazia do elemento associado ao
operador;
• operador “+” – gera dois casos de teste, um contendo uma instância do elemento as-
sociado ao operador e outro contendo um número permitido de instâncias do elemento
associado ao operador;
8.4. Teste baseado em defeitos 225
• operador “*” – gera dois casos de teste, um contendo a duplicação de uma instância
do elemento associado ao operador; e outro contendo a destruição de uma instância do
elemento associado ao operador.
O teste de esquemas deve revelar defeitos que poderiam ocasionar uma falha na aplicação
Web que manipula dados de um documento XML associado ao esquema em teste. Os defeitos
detectados em um esquema estão relacionados à definição incorreta ou à definição ausente
de restrições aos dados armazenados em documento XML, permitindo que dados incorretos
possam ser considerados válidos ou que dados corretos possam ser considerados inválidos.
A abordagem de teste de esquemas proposta por Emer et al. [129] é baseada em defeitos.
Essa abordagem permite a detecção de defeitos em esquemas de documentos XML por meio
de consultas a documentos XML válidos em relação ao esquema em teste.
Os defeitos que podem ser encontrados em um esquema estão associados aos erros mais
comuns cometidos na fase de desenvolvimento ou durante a evolução de um esquema, por
causa de atualizações ocorridas no documento XML associado ao esquema. Esses defeitos
são identificados em três classes, subdivididas em tipos de defeitos baseados em restrições
que podem ser estabelecidas para os dados.
regular (Regular Tree Grammar – RTG), apresentado na abordagem anterior, foi estendido
para representar um esquema, identificando os componentes necessários para a aplicação da
abordagem de teste. A RTG estendida é uma 7-tupla < E, A, D, R, N, P, ns >, na qual:
E é um conjunto finito de elementos.
A é um conjunto finito de atributos.
D é um conjunto finito de tipos de dados.
R é um conjunto finito de restrições.
N é um conjunto finito de não-terminais.
P é um conjunto finito de regras de produção:
Os documentos XML mutantes são gerados por meio de pequenas alterações feitas nos
documentos XML válidos, obtidos juntamente com o esquema em teste. Essas alterações são
feitas em conformidade com as classes de defeito. É importante ressaltar que os mutantes a
ser consultados devem ser bem-formados e válidos com respeito ao esquema em teste.
A próxima etapa é realizar o teste executando as consultas aos documentos XML mutantes
e analisando os resultados obtidos. Um dado de teste é composto de um documento XML
mutante válido e da instância da consulta gerada. O resultado obtido deve ser comparado com
a especificação do resultado esperado, que pode ser conseguido da especificação do esquema
em teste ou da aplicação Web que usa o documento XML relacionado ao esquema em teste.
8.5 Ferramentas
Muitas das extensões para os critérios de teste no contexto de aplicações Web descritas ante-
riormente ainda são objeto de pesquisa e não possuem o suporte de uma ferramenta. Entre as
ferramentas encontradas destacam-se as ferramentas propostas para as abordagens baseadas
em teste de modelos de especificação, mais particularmente no diagrama de classes.
228 Introdução ao Teste de Software ELSEVIER
A ferramenta denominada WAT (Web Application Testing) foi desenvolvida por Di Lucca
et al. [253] para apoiar sua abordagem de teste em aplicações Web, descrita anteriormente.
A ferramenta implementa as funções necessárias à geração de casos de teste, execução do
conjunto de casos de teste gerado e avaliação dos resultados obtidos com a execução dos
casos de teste. A WAT utiliza a análise estática de uma aplicação Web resultante da execução
da ferramenta WARE [252], que realiza engenharia reversa em uma aplicação Web.
A WAT é composta por (Figura 8.12):
– Extrator do modelo de teste (Test Model Abstractor): produz uma instância do mo-
delo de teste para a aplicação Web com base nas informações obtidas da WARE;
– Gerenciador de casos de teste (Test Case Manager): fornece funções de suporte
ao projeto de casos de teste e gerenciamento da documentação do teste;
– Gerador de módulo pseudocontrolado (Driver and Stub Generator): produz o
código das páginas Web implementando os módulos pseudocontrolados;
– Gerenciador de instrumentação de código (Code Instrumentation Manager): exe-
cuta os casos de teste e armazena os resultados obtidos;
– Analisador do resultado do teste (Test Result Anlyzer): analisa e avalia os resul-
tados obtidos com a execução dos casos de teste;
– Gerador de relatório (Report Generator): fornece o relatório com a cobertura dos
componentes Web exercitados no teste.
• Repositório (Repository) – base de dados relacional, na qual são armazenados: o mo-
delo de teste da aplicação Web, os casos de teste, os registros de teste, os arquivos das
páginas Web instrumentadas, os módulos pseudocontrolados e os relatórios de teste.
9.1 Introdução
Nos capítulos anteriores foram discutidos critérios de teste propostos para programas seqüen-
ciais. Programas seqüenciais possuem um comportamento chamado determinístico, ou seja,
toda execução de um programa com a mesma entrada irá produzir sempre a mesma saída.
Isso acontece porque os comandos do programa são executados seqüencialmente, e os des-
vios, selecionados deterministicamente de acordo com o valor de entrada informado.
Diferentemente dos programas seqüenciais, programas concorrentes possuem comporta-
mento não-determinístico, uma característica que torna a atividade de teste ainda mais com-
plexa. O não-determinismo permite que diferentes execuções de um programa concorrente
com o mesmo valor de entrada possam produzir diferentes saídas corretas. Esse comporta-
mento é produzido devido à comunicação, à sincronização e ao paralelismo existentes entre
os processos que fazem parte do programa concorrente. Essas características distinguem os
programas concorrentes dos programas seqüenciais e precisam ser consideradas durante a
atividade de teste de software.
Apesar de existirem novos desafios a serem considerados para testar programas concor-
rentes, observa-se que o conhecimento adquirido no contexto de programas seqüenciais pode
ser aproveitado, com as devidas adaptações, para a definição de técnicas, critérios e ferra-
mentas para o teste de programas concorrentes. Neste capítulo procura-se ilustrar essas con-
tribuições e as limitações impostas para o uso dos critérios de teste propostos para programas
seqüenciais.
Em geral, o teste de software aplicado a programas concorrentes visa a identificar er-
ros relacionados à comunicação, ao paralelismo e à sincronização. Assim, critérios de teste
são definidos para identificar erros dessas categorias. Entretanto, os demais tipos de erros,
classificados para programas seqüenciais [31], também devem ser considerados pelos crité-
rios de teste para programas concorrentes. Krawczyk e Wiszniewski [227] apresentam uma
possível classificação de erros para programas paralelos, registrando a importância de obser-
232 Introdução ao Teste de Software ELSEVIER
var também a classificação de erros de Beizer [31]. A taxonomia de erros para programas
concorrentes é discutida e ilustrada neste capítulo.
Na próxima seção (Seção 9.2) são apresentadas as principais características dos pro-
gramas concorrentes. Um exemplo é utilizado para ilustrar as diferenças entre programas
seqüenciais e concorrentes. Na Seção 9.3 são ilustrados os tipos de erros comuns em progra-
mas concorrentes. Nas Seções 9.4 e 9.5 são apresentados critérios de teste definidos para esse
tipo de programa e na Seção 9.6 são discutidas questões relacionadas ao não-determinismo,
juntamente com algumas soluções para gerenciá-lo. A Seção 9.7 mostra algumas ferramentas
de suporte aos critérios de teste definidos. A Seção 9.8 apresenta as considerações finais do
capítulo.
mento dos valores enviados pelos processos escravos é não determinístico, ou seja, depende
da ordem de chegada no processo mestre. Para isso, dois comandos pvm_recv() não de-
terminísticos (linhas 15 e 17) são definidos no processo mestre. Após o cálculo do mdc, o
resultado é apresentado pelo processo mestre, que finaliza todos os processos criados que
ainda estiverem executando (linha 32 do Programa 9.1).
Programa 9.1
1 int main() { // Programa mestre
2 int x,y,z;
3 int S[3];
4 /*1*/ printf("Entre com x, y e z: ");
5 /*1*/ scanf("%d%d%d",&x,&y,&z);
6 /*1*/ pvm_spawn("gcd",0,0,"",3,S);
7 /*2*/ pvm_initsend(PvmDataDefault);
8 /*2*/ pvm_pkint(&x, 1, 1);
9 /*2*/ pvm_pkint(&y, 1, 1);
10 /*2*/ pvm_send(S[0],1);
11 /*3*/ pvm_initsend(PvmDataDefault);
12 /*3*/ pvm_pkint(&y, 1, 1);
13 /*3*/ pvm_pkint(&z, 1, 1);
14 /*3*/ pvm_send(S[1],1);
15 /*4*/ pvm_recv(-1,2);
16 /*4*/ pvm_upkint(&x, 1, 1);
17 /*5*/ pvm_recv(-1,2);
18 /*5*/ pvm_upkint(&y, 1, 1);
19 /*6*/ if ((x>1)&&(y>1)){
20 /*7*/ pvm_initsend(PvmDataDefault);
21 /*7*/ pvm_pkint(&x, 1, 1);
22 /*7*/ pvm_pkint(&y, 1, 1);
23 /*7*/ pvm_send(S[2],1);
24 /*8*/ pvm_recv(-1,2);
25 /*8*/ pvm_upkint(&z, 1, 1);
26 /*8*/ }
27 /*9*/ else {
28 /*9*/ pvm_kill(S[2]);
29 /*9*/ z = 1;
30 /*9*/ }
31 /*10*/ printf("%d", z);
32 /*10*/ pvm_exit();
33 /*10*/}
*
Programa 9.2
1 int main() { // Programa escravo - gcd.c
2 int tid,x,y;
3 /*1*/ tid = pvm_parent();
4 /*2*/ pvm_recv(tid,-1);
5 /*2*/ pvm_upkint(&x,1,1);
6 /*2*/ pvm_upkint(&y,1,1);
7 /*3*/ while (x != y) {
8 /*4*/ if (x < y)
9 /*5*/ y = y-x;
10 /*6*/ else
11 /*6*/ x = x-y;
12 /*7*/ }
13 /*8*/ pvm_initsend(PvmDataDefault);
14 /*8*/ pvm_pkint(&x,1,1);
15 /*8*/ pvm_send(tid,2);
16 /*9*/ pvm_exit();
17 /*9*/}
*
236 Introdução ao Teste de Software ELSEVIER
Taylor et al. [388] definem um Grafo de Concorrência que representa a transição de estados
do programa concorrente, apresentando as possíveis sincronizações. Entretanto, como esse
método é baseado em estados possíveis no programa, está suscetível ao problema de explosão
de estados.
Yang e Chung [81] definem dois grafos: 1) Grafo de Sincronização, que apresenta o
comportamento em tempo de execução do programa concorrente, modelando as possíveis
sincronizações; e 2) Grafo de Processo, que apresenta a visão estática do programa, mode-
lando o fluxo de controle entre seus comandos. Uma execução do programa irá sensibilizar
uma rota de sincronização (c-rota) no Grafo de Sincronização e um caminho (c-caminho) no
Grafo de Processo, sendo que um c-caminho pode possuir mais de uma c-rota. Os autores
apontam três resultados práticos de sua pesquisa: seleção de caminhos, geração de casos de
teste e execução dos casos de teste.
Ao explorar os conceitos de Grafos de Programas, Yang et al. [451] apresentam uma
extensão a esses grafos capaz de representar os aspectos de comunicação entre processos
em um programa concorrente. Basicamente, o trabalho propõe a construção de um Grafo
de Fluxo de Controle (GFC) para cada processo paralelo, da mesma forma que é feito para
programas seqüenciais. A partir desses grafos, um Grafo de Fluxo de Controle Paralelo é
construído, composto dos GFCs dos processos, adicionando arestas de sincronização. Essas
arestas representam a criação dos processos paralelos e a comunicação entre esses processos.
A contribuição principal desse trabalho está em apresentar que é possível, com as devidas
adaptações, estender critérios de teste seqüenciais para programas paralelos. Os autores con-
sideram somente programas paralelos que utilizam variáveis compartilhadas, implementados
na linguagem Ada.
As idéias apresentadas por Yang et al. [451] foram estendidas por Vergilio et al. [409]
para permitir a aplicação de critérios de teste para programas paralelos em ambientes de
passagem de mensagem. Um modelo de fluxo de dados também foi proposto, adicionando
aos grafos informações sobre definição e uso de variáveis, principalmente aquelas envolvidas
na comunicação entre os processos. A diferença principal desse modelo de teste em relação
ao modelo de Yang et al. [451] é que são considerados processos paralelos executando em
diferentes espaços de memória. Com isso, adaptações foram necessárias para que se tornasse
possível representar, dentre outras coisas, a comunicação entre os processos paralelos. Além
disso, o modelo desenvolvido por Vergilio et al. [409] focaliza a aplicação dos testes para
validar um único processo e também a aplicação paralela como um todo.
238 Introdução ao Teste de Software ELSEVIER
O modelo de teste proposto por Vergilio et al. [409] considera um número n fixo e co-
nhecido de processos paralelos criado durante a inicialização da aplicação paralela. Esses
processos podem executar o mesmo ou diferentes programas, mas cada processo executa seu
código no próprio espaço de memória. A comunicação entre os processos acontece de duas
maneiras. A primeira, chamada de comunicação ponto a ponto, utiliza comandos de envio e
recebimento de mensagens em que é especificado o processo origem e o processo destino da
mensagem. A segunda, chamada de comunicação coletiva entre processos, utiliza comandos
que permitem que uma mensagem seja enviada para todos os processos paralelos da aplicação
(ou para um grupo de processos). Nesse modelo, a comunicação coletiva acontece somente
num único e predefinido domínio (contexto), que envolve todos os processos da aplicação.
O programa paralelo P rog é dado por um conjunto de processos P rog = p0 , p1 , ...pn−1 .
Cada processo p possui seu GFC, construído utilizando-se os mesmos conceitos utilizados
para programas seqüenciais. Um nó n pode ou não estar associado a uma função de comuni-
cação (send() ou receive()). Considera-se que as funções de comunicação possuem os
parâmetros da seguinte forma: send(i,j,t) (ou receive(i,j,t)), significando que
o processo i envia (ou recebe) a mensagem t para o (ou do) processo j. Cada GFC possui um
único nó de entrada e um único nó de saída pelos quais, respectivamente, se têm o início e o
término da execução do processo p.
Um Grafo de Fluxo de Controle Paralelo (GFCP) é construído para P rog, sendo com-
posto pelos GFCp (para p = 0..n−1) e pelas arestas de comunicação dos processos paralelos.
N e E representam, respectivamente, o conjunto de nós e arestas de GFCP. Um nó i em um
processo p é representado pela notação: npi . Dois subconjuntos de N são definidos: Ns e
Nr , compostos por nós que possuem, respectivamente, funções de envio e funções de rece-
bimento de mensagens. Para cada npi ∈ Ns , um conjunto Rip é associado, o qual contém os
possíveis nós que recebem a mensagem enviada pelo nó npi .
As arestas de GFCP podem ser de dois tipos:
• arestas interprocessos (Es ): são arestas que representam a comunicação entre os pro-
cessos, ou seja, são arestas que ocorrem entre os processos paralelos.
Da mesma forma que ocorre com os demais critérios de teste estruturais, um caminho
π = (n1 , n2 , ..., nj , nk ) é livre de definição em relação a uma variável x definida no nó n1 e
usada no nó nk ou aresta (nj , nk ), se x ∈ def (n1 ) e x ∈ def (ni ), para i = 2 . . . j.
A partir dessas informações, é possível caracterizar as associações que devem ser requeri-
das pelos critérios estruturais aplicados a programas concorrentes. Três tipos de associações
podem ser derivados:
• associação c-uso – dada pela tripla (ni p , nj p , x) em que x ∈ def (ni p ), e nj p possui
um c-uso de x e existe um caminho livre de definição com relação a x de ni p para nj p ;
• associação p-uso – dada pela tripla (ni p , (nj p , nk p ), x) em que x ∈ def (ni p ), e
(nj p , nk p ) possui um p-uso de x e existe um caminho livre de definição com relação a
x de ni p para (nj p , nk p );
• associação s-uso – dada pela tripla (ni p1 , (nj p1 , nk p2 ), x) em que x ∈ def (ni p1 ), e
(nj p1 , nk p2 ) possui um s-uso de x e existe um caminho livre de definição com relação
a x de ni p1 para (nj p1 , nk p2 ).
Note que as associações p-uso e c-uso são associações intraprocessos, ou seja, a definição
e o uso de x ocorrem no mesmo processo p1 e são geralmente requeridas no teste de progra-
mas seqüenciais, realizando o teste de cada processo isoladamente. Uma associação s-uso
pressupõe a existência de um segundo processo p2 e pode ser considerada uma associação
interprocessos. Essas associações permitem verificar erros na comunicação entre processos,
ou seja, no envio e no recebimento de mensagens. Considerando associações s-usos e a co-
municação entre processos, outros tipos de associações interprocessos são caracterizadas:
• associação s-c-uso – dada pela quíntupla (npi 1 , (npj 1 , npk2 ), npl 2 , xp1 , xp2 ), tal que existe
uma associação s-uso (npi 1 , (npj 1 , npk2 ), xp1 ) e uma associação c-uso (npk2 , npl 2 , xp2 );
240 Introdução ao Teste de Software ELSEVIER
• associação s-p-uso – dada pela quíntupla (npi 1 , (npj 1 , npk2 ), (npl 2 , npm2 ), xp1 , xp2 ), tal que
existe uma associação s-uso (npi 1 , (npj 1 , npk2 ), xp1 ) e uma associação p-uso (npk2 , (npl 2 ,
npm2 ), xp2 ).
Para ilustrar essas definições, considere o programa PVM (Programas 9.1 e 9.2). O pro-
cesso pm representa o processo mestre que gera três processos escravos (p0 , p1 e p2 ). Cada
processo possui um GFC associado. O GFCP é apresentado na Figura 9.1. Observe que o
grafo está simplificado; para facilitar sua visualização, algumas arestas interprocessos foram
omitidas. Os comandos de sincronização são separados dos demais nós de comandos para
possibilitar a geração das arestas interprocessos (Es ). Assim, o nó 2m (nó 2 do processo pm )
está associado aos comandos compreendidos entre as linhas 7 e 10, os quais são responsáveis
por preparar o buffer e enviar as variáveis x e y para o processo p0 . Assim, é caracterizada
a aresta interprocesso de 2m para 20 . A Tabela 9.1 contém os conjuntos obtidos a partir des-
sas definições, os quais são utilizados para derivação dos requisitos de teste. A Tabela 9.2
apresenta o conjunto de variáveis definidas em cada nó do grafo da Figura 9.1.
Tabela 9.1 – Informações obtidas pelo modelo de teste para o programa gcd
n=4
P rog = pm , p0 , p1 , p2
N = {1m , 2m , 3m , 4m , 5m , 6m , 7m , 8m , 9m , 10m , 10 , 20 , 30 , 40 , 50 , 60 ,
70 , 80 , 90 , 11 , 21 , 31 , 41 , 51 , 61 , 71 , 81 , 91 , 12 , 22 , 32 , 42 ,
52 , 62 , 72 , 82 , 92 }
Ns = {2m , 3m , 7m , 80 , 81 , 82 } (nós com pvm_send())
Nr = {4m , 5m , 8m , 20 , 21 , 22 } (nós com pvm_recv())
R2m = {20 , 21 , 22 }
R3m = {20 , 21 , 22 }
R7m = {20 , 21 , 22 }
R8 0 = {4m , 5m , 8m }
R8 1 = {4m , 5m , 8m }
R8 2 = {4m , 5m , 8m }
E = Eip ∪ Es
Eim = {(1m , 2m ), (2m , 3m ), (3m , 4m ), (4m , 5m ), (5m , 6m ), (6m , 7m ),
(7m , 8m ), (8m , 10m ), (6m , 9m ), (9m , 10m )}
Ei0 = {(10 , 20 ), (20 , 30 ), (30 , 40 ), (40 , 50 ), (40 , 60 ), (50 , 70 ), (60 , 70 ),
(70 , 30 ), (30 , 80 ), (80 , 90 )}
Ei1 = {(11 , 21 ), (21 , 31 ), (31 , 41 ), (41 , 51 ), (41 , 61 ), (51 , 71 ), (61 , 71 ),
(71 , 31 ), (31 , 81 ), (81 , 91 )}
Ei2 = {(12 , 22 ), (22 , 32 ), (32 , 42 ), (42 , 52 ), (42 , 62 ), (52 , 72 ), (62 , 72 ),
(72 , 32 ), (32 , 82 ), (82 , 92 )}
Es = {(2m , 20 ), (2m , 21 ), (2m , 22 ), (3m , 20 ), (3m , 21 ), (3m , 22 ), (7m , 20 ),
(7m , 21 ), (7m , 22 ), (80 , 4m ), (80 , 5m ), (80 , 8m ), (81 , 4m ), (81 , 5m ), (81 , 8m ),
(82 , 4m ), (82 , 5m ), (82 , 8m )}
9.4. Critérios estruturais em programas concorrentes 241
Taylor et al. [388] introduzem alguns critérios e ilustram suas idéias em programas ADA.
Entre esses critérios destacam-se: Todos-Caminhos do Grafo de Concorrência, Todas-Arestas
entre os estados de Sincronização, Todos-Estados, Todos-Possíveis-Rendezvous. Como dito
anteriormente, alguns desses critérios podem ser inaplicáveis, pois o número de estados pode
crescer rapidamente.
Yang et al. [451] estendem o critério de fluxo de dados Todos-Du-Caminhos para progra-
mas paralelos. O Grafo de Fluxo de Programa Paralelo (PPFG) é percorrido para obtenção
dos du-caminhos. Todos os du-caminhos que possuem definição e uso de variáveis relacio-
nadas ao paralelismo das tarefas são requisitos de teste a serem executados, sendo as tarefas
atividades de um programa paralelo que se comunicam entre si mas são executadas indepen-
dentemente (concorrentemente).
Com base nas definições dadas na seção anterior, Vergilio et al. [409] definem duas famí-
lias de critérios de teste estrutural para programas concorrentes em ambientes de passagem
de mensagens: família de critérios baseados em fluxo de controle e no fluxo de comunicação
e família de critérios baseados em fluxo de dados e em passagem de mensagens.
Cada GFC pode ser testado individualmente utilizando-se os critérios Todos-Nós e Todas-
Arestas. Entretanto, o objetivo é testar as comunicações presentes no GFCP para um dado
P rog. Desse modo, os seguintes critérios são definidos:
• Todos-Nós-s: requer que todos os nós do conjunto Ns sejam exercitados pelo menos
uma vez pelo conjunto de casos de teste.
• Todos-Nós-r: requer que todos os nós do conjunto Nr sejam exercitados pelo menos
uma vez pelo conjunto de casos de teste.
• Todos-Nós: requer que todos os nós do conjunto N sejam exercitados pelo menos uma
vez pelo conjunto de casos de teste.
• Todos-Arestas-s: requer que todas as arestas do conjunto Es sejam exercitadas pelo
menos uma vez pelo conjunto de casos de teste.
• Todas-Arestas: requer que todas as arestas do conjunto E sejam exercitadas pelo menos
uma vez pelo conjunto de casos de teste.
• Todas-Defs: requer que para cada nó npi e cada x em def (npi ) uma associação definição-
uso (c-uso ou p-uso) com relação a x seja exercitada pelo conjunto de casos de teste.
9.5. Teste de mutação em programas concorrentes 243
• Todas-Defs/s: requer que para cada nó npi e cada x em def (npi ) uma associação s-c-uso
ou s-p-uso com relação a x seja exercitada pelo conjunto de casos de teste. Caso não
exista associação s-c-uso ou s-p-uso com relação a x, é requerido qualquer outro tipo
de associação interprocessos em relação a x.
• Todos-c-Usos: requer que todas as associações c-usos sejam exercitadas pelo conjunto
de casos de teste.
• Todos-p-Usos: requer que todas as associações p-usos sejam exercitadas pelo conjunto
de casos de teste.
• Todos-s-Usos: requer que todas as associações s-usos sejam exercitadas pelo conjunto
de casos de teste.
• Todos-s-c-Usos: requer que todas as associações s-c-usos sejam exercitadas pelo con-
junto de casos de teste.
• Todos-s-p-Usos: requer que todas as associações s-p-usos sejam exercitadas pelo con-
junto de casos de teste.
A Tabela 9.3 apresenta exemplos de elementos requeridos por esses critérios, para o
programa da Figura 9.1. Da mesma forma que ocorre para programas seqüenciais, a não-
executabilidade é inerente a esses critérios de teste. Para ilustrar, considere o critério Todos-
s-Usos. A associação (1m , (2m , 21 ), x, y) é não-executável, pois o comando send em 2m
envia a mensagem explicitamente para o processo p0 e, portanto, não sincroniza com o pro-
cesso p1 .
Tabela 9.3 – Elementos requeridos pelos critérios de teste em relação ao programa gcd
Critério Elementos requeridos
Todos-Nós-s 2m , 3m , 7m , 80 , 81 , 82
Todos-Nós-r 4m , 5m , 8m , 20 , 21 , 22
Todos-Nós 1m , 2m , 3m , 4m , 5m , 6m , 7m , 8m , 9m , ..., 10 , 20 , 30 , .., 11 , 21 , 31 , ...
Todas-Arestas-s (2m , 20 ), (2m , 21 ), (2m , 22 ), (3m , 20 ), (3m , 21 ), (3m , 22 ), (7m , 22 ),
(80 , 4m ), (80 , 5m ), (80 , 8m ), (81 , 4m ), (81 , 5m ), (81 , 8m ),
(82 , 4m ), (82 , 5m ), (82 , 8m )
Todas-Arestas (1m , 2m ), (2m , 3m ), (3m , 4m ), ..., (10 , 20 ),
(20 , 30 ), ..., (11 , 21 ), (21 , 31 ), ...(2m , 20 ), (2m , 21 )...
Todas-Defs (8m , 10m , z), (20 , 50 , x), (20 , 60 , x), (20 , (30 , 40 ), x), (20 , 60 , y)...
Todas-Defs/s (1m , (2m , 20 ), 50 , x, x), (1m , (2m , 20 ), 60 , y, y),
(1m , (2m , 20 ), (40 , 50 ), y, y), (1m , (3m , 20 ), 50 , z, y), ...
Todos-c-Usos (1m , 10m , z), (8m , 10m , z), (20 , 80 , x)...
Todos-p-Usos (4m , (6m , 7m ), x), (4m , (6m , 9m ), x), (5m , (6m , 7m ), y),
(5m , (6m , 9m ), y), (20 , (30 , 40 ), x), (20 , (30 , 80 ), y)...
Todos-s-Usos (1m , (2m , 20 ), x, y), (1m , (2m , 21 ), x, y), (1m , (3m , 20 ), y, z),
(4m , (7m , 22 ), x), (5m , (7m , 20 ), y), (5m , (7m , 21 ), y), ...
Todos-s-c-Usos (1m , (2m , 20 ), 50 , x, x), (1m , (2m , 20 ), 60 , x, x), (1m , (2m , 20 ), 50 , y, y),
(1m , (2m , 20 ), 60 , y, y), (1m , (2m , 21 ), 61 , x, x),
(1m , (3m , 21 ), 61 , x, x), (20 , (80 , 8m ), 10m , x, z), ...
Todos-s-p-Usos (1m , (2m , 20 ), (30 , 40 ), x, x), (1m , (2m , 20 ), (30 , 80 ), x, x),
(1m , (2m , 20 ), (40 , 50 ), x, x), (1m , (3m , 20 ), (30 , 40 ), z, y),
(5m , (7m , 20 ), (30 , 40 ), y, x), (20 , (80 , 4m ), (6m , 7m ), x, y), ...
que eles cobrem a maioria dos aspectos relacionados à concorrência e sincronização e são
também de baixo custo (geram um número pequeno de mutantes). Para tratar o problema de
não-determinismo os autores consideram a proposta de Silva-Barradas [359].
Giacometti et al. [151] apresentam uma proposta inicial para a aplicação do teste de mu-
tação para programas em ambientes de passagem de mensagens, considerando o ambiente
PVM. O conjunto de operadores de mutação definido procura modelar erros no fluxo de
dados entre os processos paralelos; erros no empacotamento de mensagens; erros na sin-
cronização e no paralelismo dos processos. Os autores indicam a necessidade de tratar o
não-determinismo no contexto de PVM, a exemplo do trabalho de Silva-Barradas [359].
1. selecionar um conjunto de teste da forma (X, S), em que X representa uma entrada
para o programa P e S, uma seqüência de sincronização de P ;
2. para cada teste (X, S) selecionado:
(a) determinar se S é ou não executável para P com X, tentando sensibilizar a exe-
cução de P com X conforme S;
(b) se a seqüência S é possível, examinar o resultado dessa execução.
mensagens recebidas (quando um receive de um processo pode receber mais de uma men-
sagem e depende da ordem de chegada para que tais mensagens sejam recebidas) e detectar
erros de mensagens enviadas/recebidas.
Yang [452] descreve técnicas que podem ser utilizadas para testar programas concorren-
tes por meio de execução não determinística. O objetivo dessas técnicas é mudar o tempo de
execução de eventos de sincronização (por exemplo, eventos de envio e de recebimento de
mensagens), sem introduzir mudanças no ambiente em que o programa está sendo executado.
Esse tipo de teste é conhecido como teste temporal. Se um programa não tem erros de sin-
cronização, mudar o tempo desses eventos não deveria causar deadlocks ou outro comporta-
mento incorreto no programa. Duas técnicas podem ser empregadas para esse fim: múltiplas
execuções, em que o programa concorrente é executado várias vezes com a mesma entrada
de teste e inserção de atrasos, em que são inseridos comandos de atraso (delays) no programa
concorrente de modo a atrasar alguns segmentos do programa. O autor destaca estudos que
indicam que a inserção de atrasos pode ser um mecanismo eficiente para identificar erros em
programas concorrentes.
Essas técnicas apresentam vantagens e desvantagens. Por exemplo, a execução não de-
terminística, inserindo atrasos ou executando várias vezes o programa, tem a vantagem de
ser simples de implementar. Entretanto, um dos problemas é que a inserção de atrasos pode
aumentar significativamente o tempo de execução do programa concorrente, que ficará atra-
sado até a sincronização desejada ser atingida. Sem dúvida, o maior problema é a quantidade
de informações que precisam ser testadas, pois é preciso inserir atrasos de modo que todas
as combinações possíveis de sincronizações sejam testadas. Por outro lado, a execução con-
trolada pode ser mais barata se comparada com teste temporal. Porém, os mecanismos de
controle inseridos para forçar a execução de certas sincronizações podem modificar sensivel-
mente o comportamento do programa. O efeito disso ainda precisa ser estudado.
De uma maneira geral, o testador precisa ter em mente quais são seus objetivos quando
estiver tratando a execução não determinística de programas concorrentes. De modo sim-
plista, dois objetivos principais podem ser almejados: 1) reproduzir um teste já realizado,
possivelmente buscando verificar se os erros corrigidos não produziram outros erros no pro-
grama; e 2) executar todas as seqüências de sincronização, buscando um mecanismo que
permita combinar todas as possibilidades de sincronização e garantir que todas funcionem
adequadamente, sem se esquecer de gerenciar a explosão de combinações possíveis.
9.7 Ferramentas
A maioria das ferramentas existentes auxilia a análise, a visualização, o monitoramento e a
depuração de um programa concorrente. Exemplos dessas ferramentas são: TDCAda [383]
que apóia a depuração de programas na linguagem ADA, utilizando execução determinística;
ConAn (ConcurrencyAnalyser) [249], que gera drivers para o teste de unidade de classes de
programas concorrentes Java; e Xab [30], MDB [103] e Paragraph [174] para programas es-
critos em ambiente de passagem de mensagens. Xab e Paragraph objetivam o monitoramento
de programas PVM e MPI, respectivamente. MDB permite uma execução controlada e a de-
puração de programas PVM. Outras ferramentas que analisam o desempenho de programas
MPI são avaliadas em por Moore et al. [287].
9.7. Ferramentas 247
A ferramenta Della Pasta (Delaware Parallel Software Testing Aid) [451] foi uma das
primeiras ferramentas desenvolvidas para a análise de caminhos em programas concorrentes.
Ela permite o teste de programas concorrentes com memória compartilhada. Possui um ana-
lisador estático, que recebe como entrada o nome do arquivo a ser testado e gera possíveis
caminhos para cobrir associações com relação a variáveis envolvidas na sincronização entre
tarefas.
Em se tratando de suporte a critérios de teste em ambientes de passagem de mensagens,
destacam-se três ferramentas: STEPS [251], Astral [351] e ValiPar [376].
STEPS e Astral são ferramentas que apóiam a visualização e a depuração de programas
PVM. Ambas as ferramentas geram caminhos para cobrir elementos requeridos por critérios
estruturais baseados em fluxo de controle.
ValiPar [376] apóia a aplicação dos critérios apresentados na Seção 9.4.2. Essa ferramenta
permite que programas em diferentes ambientes de passagem de mensagens sejam testados.
Atualmente, a ferramenta encontra-se configurada para PVM e MPI. A ValiPar é composta
por quatro módulos principais (Figura 9.2), discutidos a seguir.
da IDeL para programas em C foi usada, a qual foi estendida para tratar comandos
específicos do PVM e MPI, relacionados à comunicação e sincronização de processos.
Se forem utilizados os GFCs e o GFCP gerados pelo módulo IDeL, é possível obter
informações sobre o fluxo de controle, fluxo de dados e fluxo de comunicação dos
processos paralelos. Esse módulo é o único dependente da linguagem utilizada.
• O módulo ValiElem gera os elementos requeridos, tendo como entrada as informações
produzidas pelo módulo IDeL. Similarmente à ferramenta POKE-TOOL [61], apre-
sentada no Capítulo 4, ValiElem utiliza dois tipos de grafos: um Grafo Reduzido de
Herdeiros e vários grafos(i). O primeiro grafo contém somente arestas primitivas, ou
seja, arestas que garantem a execução das demais e, por questões de minimização, são
efetivamente requeridas. O segundo tipo, o grafo(i), é um grafo construído para todo
nó i que contém uma definição de variável. Um determinado nó k pertence ao grafo(i)
se existe pelo menos um caminho do nó i para k que não redefine a variável x, definida
em i. Esse grafo é utilizado para estabelecer as associações requeridas. ValiElem gera,
para cada elemento requerido, um descritor representado por meio de uma expressão
regular, que descreve os possíveis caminhos que cobrem o elemento requerido.
• O módulo ValiExec executa o programa instrumentado mediante o comando e entra-
das fornecidas pelo usuário. Para cada entrada o programa instrumentado produzirá
os caminhos percorridos em cada processo, bem como a seqüência de sincronização
realizada. O módulo ainda armazena as entradas fornecidas, sejam elas pela linha de
comando ou via teclado, e as saídas produzidas a serem verificadas com o resultado
esperado pelo testador.
• O módulo ValiEval calcula a cobertura do conjunto de dados de teste fornecido, uti-
lizando os caminhos e seqüências produzidos pelo módulo ValiExec. ValiEval utiliza
os descritores produzidos pelos critérios de teste. Se um dado caminho é reconhecido
pelo autômato correspondente a um elemento requerido, significa que esse elemento
foi coberto pelo conjunto de teste.
A Tabela 9.4 sintetiza as principais características das ferramentas existentes para apoiar
a atividade de teste de programas concorrentes.
Em linhas gerais, as pesquisas nessa área já apresentam algumas soluções para esses
desafios. É claro que algumas soluções são mais dispendiosas, como a execução não deter-
minística, mas os resultados obtidos são muito promissores.
Os critérios de teste apresentados indicam que é factível considerar os critérios definidos
para programas seqüenciais a fim de testar programas concorrentes. Ainda são necessários
estudos para avaliar a complexidade e os aspectos complementares dos critérios propostos.
Com relação às ferramentas de apoio, observa-se que o esforço inicial concentrou-se em
dispor ferramentas de apoio ao monitoramento e à depuração de programas concorrentes.
Isso é natural, pois há um esforço muito maior para localizar erros em programas concorren-
tes, e as ferramentas de suporte à depuração de programas seqüenciais não são adequadas.
Algumas ferramentas de apoio a critérios de teste foram apresentadas, indicando um esforço
na direção de fornecer suporte ao teste de programas concorrentes.
Capítulo 10
10.1 Introdução
Devido à diversidade de critérios de teste existentes, decidir qual deles deva ser utilizado ou
como utilizá-los de maneira complementar para obter melhores resultados com baixo custo
não é tarefa fácil. A realização de estudos teóricos e experimentais permite comparar os di-
versos critérios de teste existentes, procurando fornecer uma estratégia viável para realização
dos testes. A estratégia de teste que se deseja alcançar deve ser eficaz para revelar erros, ao
mesmo tempo que apresenta baixo custo de aplicação.
Na abordagem teórica procuram-se estabelecer propriedades e características dos crité-
rios de teste, como, por exemplo, a eficácia de uma estratégia de teste ou uma relação de
inclusão entre os critérios. Na abordagem experimental são coletados dados e estatísticas que
registram, por exemplo, a freqüência na qual diferentes estratégias de teste revelam a pre-
sença de erros em determinada coleção de programas, fornecendo diretrizes para a escolha
entre os diversos critérios disponíveis [189].
Do ponto de vista de estudos teóricos, os critérios de teste são avaliados segundo dois
aspectos: a relação de inclusão e a complexidade dos critérios. A relação de inclusão esta-
belece uma ordem parcial entre os critérios, caracterizando uma hierarquia entre eles. Nessa
hierarquia, um critério de teste C1 inclui um critério C2 se, para qualquer programa P e
qualquer conjunto de casos de teste T1 C1 -adequado (ou seja, T 1 cobre todos os elementos
requeridos executáveis de C1 ), T1 for também C2 -adequado, e para algum conjunto de ca-
sos de teste T2 C2 -adequado, T2 não for C1 -adequado. A complexidade de um critério C1
é definida como o número máximo de casos de testes ou de elementos requeridos, no pior
caso [337, 302, 429].
252 Introdução ao Teste de Software ELSEVIER
Do ponto de vista de estudos experimentais, os seguintes fatores são utilizados para ava-
liar os critérios de teste: custo, eficácia e dificuldade de satisfação (strength). Entende-se por
custo o esforço necessário para que o critério seja usado, o qual pode ser medido pelo número
de casos de teste necessários para satisfazer o critério ou por outras métricas dependentes do
critério, tais como: o tempo necessário para executar todos os mutantes gerados, ou o tempo
gasto para identificar mutantes equivalentes, caminhos e associações não executáveis, cons-
truir manualmente os casos de teste e aprender a utilizar as ferramentas de teste. Eficácia
refere-se à capacidade que um critério possui em detectar um maior número de erros em re-
lação a outro. Dificuldade de satisfação refere-se à probabilidade de satisfazer-se um critério
tendo sido satisfeito outro critério [435].
Este capítulo sintetiza os principais estudos teóricos e experimentais, relacionados aos
critérios de teste vistos nos Capítulos 2, 4 e 5. Na Seção 10.2 são descritos os resultados
de estudos teóricos relacionados aos critérios de teste estruturais e ao teste de mutação. Na
Subseção 10.3.1 são apresentados os resultados de estudos experimentais relacionados aos
critérios de teste estruturais e ao teste de mutação. Na Subseção 10.3.2 são apresentados
os resultados de estudos experimentais relacionados ao teste funcional. Finalmente, na Se-
ção 10.4 são apresentadas as considerações finais deste capítulo.
Esses fatores influenciam também na escolha entre os diversos critérios de teste exis-
tentes.
Segundo Frankl e Weyuker [143], uma das propriedades que deve ser satisfeita por um
bom critério de teste é a aplicabilidade: diz-se que um critério C satisfaz essa propriedade se
para todo programa P existe um conjunto de casos de teste T que seja C-adequado para P ,
ou seja, o conjunto de caminhos executados por T inclui cada elemento exigido pelo critério
C. Os critérios de fluxo de dados não satisfazem essa propriedade devido à existência de
caminhos não-executáveis. Desse modo, é indecidível saber se existe um conjunto de casos
de teste T que exercite todos os elementos requeridos por um dado critério de teste estrutural.
Rapps e Weyuker [337] apresentam a relação de inclusão entre os critérios de fluxo de
dados (Figura 10.1). Pode-se observar, por exemplo, que o critério Todas-Arestas inclui o
critério Todos-Nós, ou seja, se um conjunto de casos de teste satisfaz o critério Todas-Arestas,
então esse conjunto também satisfaz o critério Todos-Nós. Quando não é possível estabelecer
10.2. Estudos teóricos de critérios de teste 253
essa ordem de inclusão para dois critérios, diz-se que esses critérios são incomparáveis, como
é o caso dos critérios Todas-Defs e Todos-p-Usos.
Maldonado [261] estendeu essa hierarquia inserindo os critérios Potenciais-Usos (Fi-
gura 10.2). Os estudos teóricos revelam que os critérios Potenciais-Usos satisfazem as pro-
priedades básicas exigidas para um bom critério de teste (listadas anteriormente), mostrando
que, mesmo na presença de caminhos não executáveis, eles estabelecem uma hierarquia de
critérios entre os critérios Todas-Arestas e Todos-Caminhos. Além disso, esses critérios in-
cluem o critério Todas-Defs e requerem um número finito de casos de teste. Outra caracte-
rística é que nenhum outro critério baseado em fluxo de dados inclui os critérios Potenciais-
Usos.
Segundo Maldonado [261], o primeiro passo na determinação da complexidade de um
critério consiste em identificar qual estrutura de fluxo de controle, considerando um fluxo
de dados qualquer, maximiza o número de elementos requeridos pelo critério. Desse modo,
para determinar a complexidade dos critérios Potenciais-Usos, Maldonado [261] identificou o
grafo de controle ilustrado na Figura 10.3 como sendo o que maximiza o número de elemen-
tos requeridos pelos critérios Potenciais-Usos, ou seja, potenciais du-caminhos e potenciais
associações. Com base nesse grafo, Maldonado [261] mostra que ((11/2)t + 9)2t − 10t − 9
254 Introdução ao Teste de Software ELSEVIER
Figura 10.2 – Relação de inclusão dos critérios de fluxo de dados estendida com os critérios
Potenciais-Usos.
potenciais du-caminhos seriam requeridos, o que iria exigir, no pior caso, 2t casos de teste
para serem cobertos, sendo t o número de comandos de decisão, ou seja, a complexidade
do critério Todos-Potenciais-Du-Caminhos é da ordem de 2t . Sendo Todos-Potenciais-Du-
Caminhos o critério mais forte da família de critérios Potenciais-Usos, como pode ser visto
na relação de inclusão da Figura 10.2, isso significa que, no pior caso, 2t é o limitante supe-
rior de todos os critérios incluídos pelo critério Todos-Potenciais-Du-Caminhos, como, por
exemplo, os critérios Todos-Potenciais-Usos e Todos-Usos. Resultados similares foram ob-
tidos por Vincenzi [413] na análise de complexidade dos critérios estruturais orientados a
objetos.
10.2. Estudos teóricos de critérios de teste 255
cidade de revelar erros dos critérios [145] ou indicando que a relação de inclusão proposta
por Rapps e Weyuker [337] tem uma estreita relação com a capacidade de revelar erros [459].
Frankl e Weyuker [145] definiram cinco outras relações entre critérios e avaliaram essas re-
lações usando três medidas probabilísticas que procuram agregar à hierarquia estabelecida
por essas relações a capacidade de revelar a presença de erros, ou seja, refletir a eficácia do
critério.
Mathur [275] ilustra a seguinte situação para mostrar a importância de se avaliarem crité-
rios de teste: considere a necessidade de testar um programa P , o qual é parte de um sistema
crítico. O funcionamento correto desse sistema depende, obviamente, de P . O testador irá
testar P tanto quanto for possível e, para isso, decide usar vários critérios de teste com o
objetivo de verificar a adequação dos casos de teste desenvolvidos. Inicialmente, os casos de
teste são gerados de modo a satisfazer um critério C1 . De posse disso, uma questão que surge
é: Tendo obtido um conjunto de casos de teste adequado ao critério C1 , e utilizando agora
um critério C2 , é possível melhorar esse conjunto de casos de teste? Essa é uma questão
prática que surge diante da dificuldade em decidir se um programa foi suficientemente tes-
tado. O desenvolvimento de estudos experimentais procura, dessa forma, auxiliar a responder
questões desse gênero.
No contexto de teste de software, a condução adequada de experimentos requer o desen-
volvimento das seguintes atividades:
ficativos, em torno de 0,98, indicando que a mutação seletiva é uma abordagem promissora
para a aplicação do teste de mutação.
O teste funcional tem sido explorado em alguns experimentos relatados na literatura, com o
objetivo de compará-lo com outras técnicas de teste, como também com técnicas de inspeção
de código. Em seguida descrevem-se alguns desses experimentos.
Técnicas: Leitura de código disciplinada, teste de especificação (teste funcional) e teste misto
(combinação de teste funcional e teste estrutural).
Artefatos de software: Três programas escritos em PL/I, de 64, 164 e 170 instruções que
continham defeitos que causavam respectivamente 9, 15 e 25 diferentes tipos de falhas.
Principais objetivos: Comparar as três técnicas no que diz respeito à efetividade em
revelar falhas.
Caracterização dos participantes: Foram selecionados 39 participantes de acordo com
sua experiência em programação em PL/I que, em média, era de três anos. O tipo dos parti-
cipantes variava de alunos de graduação a programadores inexperientes.
Projeto experimental: O experimento foi dividido em duas fases: a fase de treinamento
e a fase de execução do experimento. O treinamento consistiu em uma introdução teórica
às técnicas, uma apresentação das especificações e uma visão geral dos três programas. O
experimento foi constituído de três sessões, e os participantes foram divididos em três grupos.
Como a ordem em que os participantes utilizaram os programas e aplicaram as técnicas foi
randomizada, foi possível, por exemplo, que um indivíduo de determinado grupo aplicasse a
técnica leitura de código disciplinada no programa 1 durante a primeira sessão, a técnica teste
misto no programa 3 durante a segunda sessão e a técnica teste de especificação no programa
2 durante a terceira sessão. Assim, nem a técnica nem o programa eram fixos em determinada
sessão.
Forma de utilização das técnicas
Leitura de código disciplinada: Durante a aplicação dessa técnica os participantes tive-
ram de, a partir da especificação, caracterizar as estruturas de código de mais baixo nível
(trechos) e, pela combinação dessas estruturas, obter a especificação do programa como um
todo. A especificação final foi comparada com a especificação original do programa para que
as inconsistências fossem identificadas.
Teste de especificação: Durante a aplicação dessa técnica, que consiste em um teste fun-
cional, os participantes utilizaram somente a especificação do programa para derivar os casos
de teste. A detecção das falhas foi realizada por meio da comparação das saídas produzidas
pelos casos de teste com a especificação original. Nenhum critério foi especificado para a
criação dos casos de teste.
Teste misto: Durante a aplicação dessa técnica os participantes tiveram de combinar as
técnicas de teste funcional e teste estrutural. Assim, os casos de teste foram gerados com
10.3. Estudos experimentais 261
base na especificação, mas o objetivo foi verificar se com eles se alcançava 100% de cober-
tura de todas as instruções do programa. As falhas foram detectadas comparando as saídas
produzidas pelos casos de teste com a especificação original.
Principais resultados obtidos: As duas técnicas de teste não apresentaram nenhuma
diferença significativa com relação à efetividade; porém os participantes que aplicaram a
leitura de código disciplinada tiveram uma efetividade menor do que quem aplicou as técnicas
de teste. Os participantes observaram somente 50% das falhas existentes.
de teste funcional obteve um melhor resultado com relação à eficiência em isolar defeitos do
que as outras técnicas. Na média, os participantes detectaram 50% das falhas.
Técnicas: Foram utilizadas as mesmas técnicas aplicadas por Kamsties e Lott [207] e Basili
e Selby [26], ou seja, as técnicas: leitura de código, teste funcional e teste estrutural, com
uma exceção – para a técnica teste estrutural foi aplicado somente o critério que cobre 100%
das instruções do programa.
Artefatos de software: Foram utilizados os programas que estavam disponíveis no pa-
cote de replicação do experimento de Kamsties e Lott [207], tanto para a fase de treinamento
quanto para a fase de execução do experimento.
Principais objetivos: Comparar as três técnicas no que diz respeito à efetividade e à
eficiência em revelar falhas. Foram selecionados alguns tipos de defeitos do total existente
nos programas do pacote original.
Caracterização dos participantes: 47 alunos de graduação, sendo que todos eles já
haviam completado dois anos fazendo disciplinas de programação.
Projeto experimental: Realizou-se um treinamento teórico em cada uma das técnicas
e, em seguida, foram realizadas três sessões de treinamento prático em cada uma delas. A
execução do experimento propriamente foi feita em três sessões, em semanas consecutivas.
Organizaram-se seis grupos, balanceados de acordo com a habilidade individual de cada
participante em relação à programação. A ordem de utilização dos programas foi fixada e
alternou-se a técnica utilizada por cada grupo.
Principais resultados obtidos: Ao fazer uma análise particular de cada técnica, observou-
se que todas as técnicas apresentaram uma taxa de efetividade similar quanto à observação
de falhas e ao isolamento de defeitos. Quando combinadas, as técnicas apresentaram uma
taxa de efetividade maior. Mesmo nos piores casos de combinações das técnicas, observou-
se que a taxa de efetividade foi 13% maior do que a média de efetividade de cada uma das
técnicas individualmente. Na média, todas as técnicas combinadas apresentaram uma taxa de
efetividade 25% maior do que a média das técnicas individuais.
falhas. Para finalizar, foi utilizado o código-fonte para isolar defeitos que causaram as falhas
observadas. Nenhuma técnica especial para isolar defeitos foi especificada. Também nesse
caso os participantes receberam a lista contendo os mutantes equivalentes, devido ao fato de
esses mutantes não permitirem a obtenção de um escore igual a um.
Principais resultados obtidos: A técnica teste funcional obteve um melhor resultado
com relação à eficiência em revelar falhas e isolar defeitos do que as outras técnicas. Na
média, os participantes detectaram 26,96% das falhas. Se os resultados forem analisados
em conjunto, explorando o aspecto complementar das técnicas, o percentual de falhas obser-
vadas e dos defeitos isolados pelos participantes é maior. Os piores resultados obtidos por
qualquer combinação de técnicas foram sempre melhores que os piores resultados obtidos
individualmente por determinada técnica.
ção para programas C foram aplicados no programa cal e os mutantes equivalentes foram
determinados manualmente. Ao todo, 4.624 mutantes foram gerados, dos quais 335 foram
identificados como equivalentes. Assim, todos os mutantes restantes podem ser mortos por
algum caso de teste.
Principais resultados obtidos: A Tabela 10.1 ilustra para cada caso de teste a quantidade
de mutantes vivos, a porcentagem de mutantes vivos em relação ao total de mutantes gerados,
o escore de mutação obtido e o número de mutantes vivos agrupados por classe de operador
de mutação.
Tabela 10.1 – Cobertura dos conjuntos de casos de testes funcionais em relação ao critério
Análise de Mutantes
Conjunto Mutantes Porcentagem Mutantes Vivos por Classe de Operador
de Teste Vivos de Vivos Escore Constante Operador Statement Variável
TSSF T 0 0 1,000000 0 0 0 0
TSP B1 371 8,02 0,913500 193 78 27 73
TSP B2 74 1,60 0,982747 33 22 0 19
TSP B3 124 2,68 0,971089 58 31 13 22
TSP B4 293 6,34 0,931686 116 84 16 77
TSRA1 1875 40,55 0,563242 944 539 103 289
TSRA2 558 12,07 0,870021 287 161 21 89
TSRA3 419 9,06 0,902399 216 113 15 75
TSRA4 348 7,53 0,918938 181 87 12 68
TSRA5 311 6,73 0,927557 159 77 11 64
TSRA6 296 6,40 0,931051 149 73 11 63
TSRA7 69 1,49 0,983927 21 30 0 18
Como pode ser evidenciado pelo experimento, o único conjunto de teste que foi capaz de
matar todos os mutantes, evidenciando todos os defeitos modelados por esses mutantes, foi o
conjunto CTFS, obtido a partir do critério Funcional Sistemático.
Somente dois outros conjuntos de testes atingiram escore de mutação acima de 0,98 mas
inferior a 1,00: CTPA2 e CTAl7. Na média, considerando os conjuntos de testes aleatórios
CTAl1, CTAl2 e CTAl3, observou-se que apresentaram escores de mutação de 0,56, 0,87, e
0,90, respectivamente, todos inferiores aos escores de mutação acima de 0,91 obtidos pelos
conjuntos funcionais CTPA1, CTPA2, CTPA3 e CTPA4.
Como o conjunto de teste obtido pelo critério Funcional Sistemático possui 76 casos de
testes e somente 21 destes foram efetivos, ou seja, mataram ao menos um mutante, compa-
rando os resultados obtidos por CTAl2 e CTAl7, os escores de mutação obtidos foram meno-
res, 0,870 e 0,984, respectivamente. Tais valores são 13 e 1,6% abaixo do escore determinado
por CTFS.
Embora estudos de casos adicionais sejam necessários para que os resultados possam
ter validade estatística, os dados obtidos com o estudo de caso descrito evidenciam que, se
aplicados corretamente, os critérios de testes funcionais podem determinar um alto grau de
cobertura do código-fonte do produto em teste, revelando um grande número de defeitos com
baixo custo de aplicação.
268 Introdução ao Teste de Software ELSEVIER
11.1 Introdução
Testar um programa com todos os seus possíveis valores de entrada ou executar todos os
seus caminhos é idealmente desejável, mas impraticável. A maioria dos critérios de teste,
descritos nos capítulos anteriores, relaciona valores do domínio de entrada de um programa,
agrupando-os em partições não necessariamente disjuntas. Em geral, os critérios de teste
dividem o domínio de entrada do programa em subdomínios e requerem que pelo menos
um ponto de cada subdomínio seja executado. Por exemplo, o critério estrutural Todos-Nós
agrupa em um subdomínio todas as entradas que executam um deteminado nó.
Uma vez particionado o domínio, a questão é : “Que pontos de cada subdomínio devem
ser escolhidos?” Isso diz respeito à tarefa de geração de dados de teste para satisfazer um
determinado critério, ou seja, dados para executar um caminho para cobrir o elemento reque-
rido. Muito embora a automatização dessa tarefa seja desejável, não existe um algoritmo de
propósito geral para determinar um conjunto de teste que satisfaça um critério. Nem mesmo
é possível determinar automaticamente se esse conjunto existe [141]. O problema de geração
de dados de teste é indecidível, sendo que existem restrições inerentes às atividades de teste
que impossibilitam automatizar completamente a etapa de geração de dados de teste. Entre
elas, destacam-se:
• correção coincidente: ocorre quando o programa em teste possui um defeito que é al-
cançado por um dado de teste, um estado de erro é produzido, mas coincidentemente
um resultado correto é obtido. Alguns autores afirmam que correção coincidente ocorre
muito raramente e ao definir suas estratégias supõem que ela não está presente. Con-
sidere o Programa 11.1. Um caminho incorreto do programa poderá ser tomado, mas
uma saída correta poderá ser produzida. Suponha que na linha 4 o predicado correto
seja y < 0. O caso de teste (x = −1, y = 0) deveria executar a parte else (linhas
6 e 7), mas devido ao erro executa a parte then (linha 5). Entretanto, coincidente-
mente, o resultado produzido é 4, igual ao esperado. Um outro dado de teste tal como
(x = 2, y = 0) revelaria o erro;
270 Introdução ao Teste de Software ELSEVIER
Programa 11.1
1 void calculo (int x, int y)
2 {
3 if (y <= 0)
4 printf ("%d",x+x+6-y*y);
5 else
6 printf ("%d",x*x+3-y*y);
7 }
*
37 class = 2; /* isoceles */
38 if (a=b)
39 area = c*sqrt(4*a*b-c*c)/4;
40 else
41 area = a*sqrt(4*b*c-a*c)/4;
42 }
43 printf("%d", class); printf("%f", area);
44 }
*
técnica é defendida por vários autores [37, 122, 161] por ser prática e mais fácil de automa-
tizar, além de menos custosa. No entanto, gerar dados de teste aleatoriamente não garante
a satisfação do critério e também não auxilia a determinação de elementos não-executáveis
ou mutantes equivalentes, pouco se aprendendo sobre o programa. Outra questão é que ge-
rar aleatoriamente não garante a seleção dos melhores pontos. Os melhores pontos são os
que têm maior probabilidade de revelar defeitos. A estratégia utilizada para selecionar esses
pontos é fundamental, pois dela depende a eficácia dos dados de teste gerados. Este capítulo
descreve as principais técnicas de geração de dados de teste que podem ser utilizadas para
satisfazer os diversos critérios de teste existentes e para selecionar os “melhores” pontos. As
técnicas são descritas, e uma discussão das principais vantagens e desvantagens da aplicação
de cada uma delas é apresentada.
elemento desse conjunto. Nem sempre é possível determinar uma solução que satisfaça a
condição do caminho, ou seja, determinar se o caminho é ou não executável.
Para exemplificar os conceitos apresentados, considere o Programa 11.4 e seu grafo apre-
sentados na Figura 11.1. Sejam K e J os valores simbólicos para as variáveis de entrada
k e j. Após a execução do caminho (1, 2, 4), o valor da variável j será o valor simbólico
J + 1 − K. A restrição criada para o caminho será J + 1 > K. Para o caminho completo
(1, 2, 4, 6) tem-se a computação do caminho dada pelo valor de j que é J + 1 − K e a con-
dição do caminho é (J + 1 > K)&&(J − K > 0), formada por restrições associadas aos
predicados dos nós 1 e 4. O dado de teste K = 1 e J = 3 satisfaz essa restrição e executa o
caminho (1, 2, 4, 6).
Programa 11.4
1 void f(double j,double k)
2 {
3 /* no’ 1 */ j = j + 1;
4 /* no’ 1 */ if (j > k)
5 /* no’ 2 */ j = j - k;
6 /* no’ 3 */ else
7 /* no’ 3 */ j = k - j;
8 /* no’ 4 */ if (j <= 1)
9 /* no’ 5 */ j = j - 1;
10 /* no’ 6 */ printf("%f",j);
11 }
*
• Laços: a execução simbólica pode tratar laços quando o número de iterações é conhe-
cido. Quando isso não ocorre, Howden [188] propõe executar o laço k vezes, sendo
que k pode ser escolhido pelo usuário ou pelo sistema. Isso pode gerar algumas in-
consistências e gerar restrições sem solução, mas essa técnica é, em geral, adotada por
questões de simplicidade.
• Referências a variáveis compostas, tais como vetores e matrizes, e apontadores: o
problema é identificar a qual variável se faz referência. O Programa 11.5 ilustra o pro-
blema com um vetor. Sendo i e j variáveis de entrada com valores simbólicos, a quais
elementos do vetor a condição se refere? Boyer et al. [44] propõem que todas as possi-
bilidades sejam analisadas, mas esse número pode crescer rapidamente. Ramamoorthy
et al. [335] sugerem que uma instância do vetor seja criada sempre que não for possível
identificar qual elemento está sendo definido. A cada instância do vetor os elementos
têm o mesmo valor simbólico, exceto aquele que acabou de receber um valor.
Por exemplo, seja Ak a k-ésima instância do vetor A. Se o comando A[m] = P é
executado, uma nova instância k + 1 de A é criada, tal que:
Ak+1 [i] = Ak , se i = m.
Ak+1 [m] = P , caso contrário.
Quando uma referência é feita a um vetor, sua última instância é utilizada. As ins-
tâncias permitirão que ambigüidades sejam posteriormente resolvidas. O número de
274 Introdução ao Teste de Software ELSEVIER
instâncias pode ser reduzido se heurísticas forem implementadas para determinar equi-
valência entre índices de vetores.
Programa 11.5
1 void f()
2 { ....
3 scanf("%d %d", &i,&j);
4 A[0] = 0;
5 A[1] = 1;
6 ....
7 A[10] = 10;
8 if (A[i] < A[j])
9 .....
10 }
*
Apesar das restrições apresentadas, a execução simbólica é uma técnica bastante difun-
dida. Ela vem sendo utilizada há vários anos. Na década de 1970, surgiram muitos sistemas
para geração automática de dados de teste baseados em execução simbólica. Entre esses: o
SELECT [44], o CASEGEN [335] e o DISSECT [188]. A maior dificuldade em tais sistemas
é resolver a condição do caminho criada.
A execução simbólica é mais custosa do que a geração aleatória, mas, em geral, é mais
eficaz. Howden [188] realizou um experimento com um conjunto de programas incorretos.
Os dados gerados com execução simbólica revelaram aproximadamente 50% dos erros. A
maioria dos erros encontrados foi de computação. Erros de domínio e/ou anomalias de fluxo
de dados raramente foram determinados pela execução simbólica.
Programa 11.6
1 void max (int low, int step, int high)
2 {
3 int A[100], i, max, min;
4 /* no’ 1 */ leitura (A);
5 /* no’ 1 */ min = A [low];
6 /* no’ 1 */ max = A [low];
7 /* no’ 1 */ i = low + step;
8 /* no’ 2 */ while (i < high) {
9 /* no’ 3 */ if (max < A[i])
10 /* no’ 4 */ max = A [i];
11 /* no’ 5 */ if (min > A[i])
12 /* no’ 6 */ min = A[i];
13 /* no’ 7 */ i = i + step; }
14 /* no’ 8 */ printf ("%d, %d",min,max);
15 }
*
Toda vez que uma variável de entrada receber um valor, um novo conjunto de variáveis que
influenciam cada subobjetivo e, conseqüentemente, novos fatores de risco deve ser derivado.
A técnica dinâmica também permite tratar estruturas de dados dinâmicas, tais como re-
gistros e apontadores. Cada campo do registro tem um nome distinto e pode ser tratado como
uma variável separada. Entretanto, apontadores representam duas variáveis: o apontador em
si e a variável para a qual ele aponta.
Toda estrutura dinâmica criada durante a execução do programa é tratada como uma va-
riável separada. É mantida uma lista dessas variáveis criadas, que serão identificadas por
nomes únicos. O tipo de estrutura a ser criada está na declaração do comando.
Dois tipos de subobjetivos são utilizados: o aritmético e o de apontador. O aritmético
é resolvido como descrito anteriormente; o de apontador será resolvido utilizando-se um
método de pesquisa baseado em análise de fluxo de dados e backtracking.
A análise de fluxo de dados dinâmica é utilizada para determinar as variáveis que são
apontadores e influenciam esse subobjetivo. Não existe em um subobjetivo do tipo aponta-
dor uma função a ser minimizada. Atribuições sistemáticas são feitas às variáveis de entrada
até que o subobjetivo seja resolvido. Passa-se então para os próximos subobjetivos. Se al-
gum deles não puder ser satisfeito, um backtracking é realizado e novas soluções para os
subobjetivos anteriores são derivadas.
Na maioria dos trabalhos dessa categoria [137, 201, 345] os indivíduos na população codi-
ficam valores possíveis para as variáveis de entrada do programa em teste. No trabalho de
Ferreira e Vergilio [137], os indivíduos são representados por uma concatenação de blocos;
cada bloco está associado a uma variável de entrada do programa. O formato do bloco va-
ria de acordo com o tipo da variável, e diferentes tipos de valores podem ser gerados. Uma
população inicial costuma ser gerada aleatoriamente, mas também pode ser fornecida pelo
testador, que poderá utilizar um conjunto de teste existente. A função de avaliação utilizada é
dada pela cobertura do critério a ser satisfeito e pelo conjunto de elementos cobertos por in-
divíduo na população, sendo que cada indivíduo representa um dado de teste. Esse conjunto
pode ser representado por uma matriz tal como a da Figura 11.4, na qual o valor X representa
que o indivíduo i cobre o elemento requerido j. Operadores genéticos são aplicados até que
a cobertura desejada seja obtida ou que um número estipulado de gerações seja alcançado.
Para se alcançar um melhor resultado com esse tipo de função, Ferreira e Vergilio [137]
introduziram técnicas de hibridização, tais como o uso de listas tabu e elitismo, para garantir
que alguns indivíduos que cobrem poucos elementos requeridos sejam mantidos na popula-
ção; para isso, basta contribuírem para um aumento da cobertura global do critério.
11.5. Geração de dados sensíveis a defeitos 279
A técnica chamada teste de domínios foi originalmente desenvolvida por White e Cohen [433],
com o propósito de selecionar dados de teste para um conjunto de caminhos de programas,
mas o teste de domínios não especifica como os caminhos são selecionados. Nesse sentido,
ela deve ser usada conjuntamente com critérios baseados em caminhos (estruturais).
O objetivo é detectar erros de domínio, ou seja, erros que atribuem um domínio incorreto a
um dado caminho, selecionando dados de teste no limite do domínio do caminho ou próximos
dele. Esse é o fundamento da técnica de teste funcional conhecida como Análise do Valor
Limite (Capítulo 2), que diz que um grande número de erros tende a se concentrar em limites
dados pelas condições existentes no programa.
Clarke, Hassel e Richardson [84], bem como Chou e Du [78], descreveram alguns pro-
blemas encontrados ao se aplicar a técnica proposta por White e Cohen e propuseram alter-
nativas. Zeil et al. [453] propuseram uma extensão para detectar erros lineares em funções de
predicados não lineares.
Primeiramente será dada uma descrição da terminologia e das suposições adotadas por
White e Cohen [433] para definir a técnica. A técnica original é então apresentada e possíveis
extensões são mencionadas.
280 Introdução ao Teste de Software ELSEVIER
Terminologia básica
A técnica analisa os limites do domínio de um caminho para gerar os dados de teste que o
executam. O domínio do caminho é definido como o conjunto de dados de entrada que satisfa-
zem a “condição do caminho”. Os limites do domínio do caminho são dados pelas condições
associadas às arestas que compõem o caminho. A cada condição corresponde um predicado,
que é uma combinação lógica de expressões relacionais.
White e Cohen assumem que os predicados encontrados são simples. Interpretações de
predicados em condições de caminhos determinam os limites do domínio do caminho dados
por uma borda. Cada borda poderá ser aberta ou fechada, dependendo do operador relacional
do predicado associado (aberta: >, <, = e fechada: ≤, ≥, =).
O número de predicados no caminho é um limitante superior do número de bordas do
domínio, uma vez que alguns predicados não correspondem necessariamente a uma borda. É
o caso de interpretações de predicados independentes das variáveis de entrada e predicados
redundantes.
A forma geral de uma interpretação de um predicado linear simples é: a1 x1 +a2 x2 +...+
an xn rop k, em que rop é um operador relacional, cada xi é uma variável de entrada e cada ai
e k são constantes. A borda do domínio é dada pela igualdade a1 x1 + a2 x2 + ... + an xn = k.
Se as interpretações de predicados forem lineares, os limites são bordas lineares e podem
ser de três tipos básicos: igualdade, inequações e desigualdades.
Para uma condição de caminho composta de interpretações lineares que são igualdades,
ou inequações, o domínio do caminho é dado por um poliedro convexo. Se uma ou mais
desigualdades estiverem presentes, o domínio do caminho consiste na união de um conjunto
de poliedros convexos disjuntos.
A técnica de teste de domínios está baseada na análise geométrica dos limites do domínio.
Tem como objetivo detectar erros nas bordas, dados por pequenas diferenças entre a borda
correta e a borda do programa em teste (borda dada), embora o teste de domínios não exija
que se tenha conhecimento do programa correto.
White e Cohen [433] definiram a técnica para programas que não referenciam apontado-
res, vetores ou matrizes e que não possuem chamadas de procedimento e/ou funções. Além
disso, para simplificar, as seguintes suposições foram feitas:
A técnica de teste de domínios seleciona dois tipos de pontos de teste. Pontos on perten-
cem à borda dada e pontos off ficam a uma distância muito pequena e devem pertencer ao
domínio que não contém a borda. Se a borda é fechada, os pontos on pertencem ao domínio
do caminho que está sendo testado e os pontos off pertencem a algum domínio adjacente. Se
a borda é aberta, os pontos on pertencem a algum domínio adjacente, enquanto os pontos off
pertencem ao domínio em teste.
A técnica N × 1
Programa 11.7
1 void ex (double x, double y)
2 {
3 double d, z;
4 /* no’ 1 */ if (x > 0) {
5 /* no’ 2 */ z = y -x;
6 /* no’ 2 */ d = 0;
7 /* no’ 3 */ while (z < d)
8 /* no’ 4 */ d = d + 1;
9 /* no’ 5 */ if (x <= d)
10 /* no’ 6 */ if (y <= 1.001 * d)
11 ....
12 }
*
A Figura 11.6 ilustra a aplicação da técnica para um domínio com três dimensões. Uma
recomendação feita por Chou e Du [78] e Clarke et al. [84] é que em duas dimensões os
pontos on devem ser escolhidos tão próximos quanto possível do final da borda, e os pontos
off próximos ao seu centro para que um número maior de defeitos seja revelado. Em três di-
mensões, combinação formada pelos pontos on deve conter o centro da borda dada, e o ponto
off deve pertencer ao centro do hiperplano paralelo à borda dada, que fica a uma distância ε
do lado aberto da borda.
282 Introdução ao Teste de Software ELSEVIER
As técnicas N × N e V × V
disso, quando o número de vértices da borda é muito maior que a dimensão do domínio,
alguns vértices consecutivos não serão testados; isso pode permitir que a borda correta não
intercepte alguma extensão lateral da borda dada.
Esse problema é resolvido pela técnica V × V , em que V é o número de vértices da
borda dada. Se existem V vértices, V pontos on e V pontos off, próximos aos vértices ou nos
vértices do hiperplano L paralelo à borda dada, são selecionados.
A técnica V × V é influenciada pelo domínio; qualquer mudança no domínio implica
mudanças nos pontos off e on escolhidos.
A complexidade das três técnicas é dada pelo número de pontos requeridos. A técnica
N × 1 requer (N + 1) ∗ B pontos, a técnica N × N requer (2 ∗ N ) ∗ B pontos, em que B é o
número de bordasfechadas do domínio do caminho. A técnica V × V requer para o domínio
B
de um caminho, i=1 2 ∗ Vi , em que Vi é o número de vértices da i-ésima borda. Esse total
pode crescer rapidamente.
Outro fator importante a ser considerado é a facilidade para determinar os pontos. A
técnica V ×V exige que todos os vértices sejam determinados e pode parecer que demandaria
mais tempo. No entanto, para se garantir uma maior eficácia das técnicas N × 1 e N × N , os
vértices também precisam ser determinados para que pontos centrais possam ser escolhidos.
Além disso, todas as combinações de vértices precisam ser consideradas para que a melhor
possa ser escolhida; isso, em geral, requer tempo não polinomial. A técnica V × V , segundo
Clarke et al. [84], se mostra mais barata.
Zeil et al. [453] analisaram a capacidade das técnicas em detectar defeitos e sua relação
com a confiabilidade. Os autores concluem que esse problema está na formulação das técni-
cas de teste de domínios e propõem uma nova abordagem que leva em consideração o com-
partilhamento de comandos por um número potencialmente infinito de caminhos, propondo o
teste de domínios de subcaminhos que levam até uma dada interpretação de predicados. Essa
nova visão do teste de domínios reduz em muito os custos computacionais. O número de
pontos de teste é significativamente reduzido. Cada interpretação é testada só uma vez. Por
exemplo, o predicado x ≥ 0 da Figura 11.5 pertence a todos os caminhos do programa e será
testado só uma vez. Zeil et al. dizem que essa visão resolve os problemas de dependências
das bordas adjacentes e não provoca perda da habilidade em detectar erros de bordas do teste
de domínios.
Zeil et al. [453] afirmam que as três técnicas apresentadas podem ser consideradas efetivas
para detectar erros em bordas. A técnica N × N leva alguma vantagem sobre a N × 1, por
284 Introdução ao Teste de Software ELSEVIER
ser melhor na maioria dos casos. No entanto, a técnica V × V raramente seria melhor que
a N × N , visto que os pontos por ela escolhidos formam um superconjunto dos escolhidos
pela N × N . Entretanto, a técnica N × 1 é mais prática e fácil de aplicar.
As suposições feitas para se definirem as técnicas de teste de domínios podem ser su-
primidas ou relaxadas, desde que mudanças apropriadas nas técnicas sejam efetuadas. Isso
permite que o teste de domínio seja aplicado a uma classe maior de programas. No entanto,
para ele se tornar efetivo, é necessário que ele seja aplicado com outros métodos de teste.
A técnica de teste baseado em restrições foi proposta por DeMillo e Offutt em 1991 [116].
A técnica tem como objetivo auxiliar a geração de dados de teste adequados ao teste de
mutação. Ela usa restrições algébricas projetadas para detectar um tipo particular de defeito
no programa.
O teste baseado em restrições usa os conceitos de análise de mutantes para gerar dados
de testes; os dados são projetados para matar os mutantes, ou seja, para detectar defeitos
descritos pelos operadores de mutação. Como apresentado no Capítulo 5, um mutante difere
do programa original por uma pequena diferença sintática em um comando S. Para matar um
mutante é necessário que: 1) o comando S seja executado; 2) imediatamente após a execução
de S, o estado do programa original seja diferente do estado do programa mutante; 3) essa
diferença seja propagada até que comandos de saída sejam executados e resultados diferentes
sejam produzidos, possibilitando assim a revelação do defeito.
Essas três condições são referenciadas pelos termos: alcançabilidade, necessidade e sufi-
ciência, que serão discutidos a seguir.
Alcançabilidade
O problema de gerar dados de teste que garantam alcançar um dado comando S é em geral
indecidível. Expressões de caminhos (execução simbólica) descrevem restrições que preci-
sam ser satisfeitas para que um comando seja alcançado. Cada expressão corresponde a um
caminho diferente até S. Desde que o interesse seja apenas alcançar o comando, o conjunto
de todos os caminhos que alcançam S é dado por uma seqüência de restrições conectadas
pelo operador “ou”.
Um problema para determinar essas restrições são os laços. Eles podem gerar um número
desconhecido ou ilimitado de caminhos. Se a execução do laço for relevante para alcançar o
comando e se isso puder ser identificado, deve-se gerar uma restrição que garanta a execução
do laço pelo menos uma vez. Caso a execução do laço seja irrelevante para o comando, o
laço poderá ser executado zero ou mais vezes. O ideal é que não se exclua qualquer caminho
que possa alcançar o comando.
Necessidade
Suficiência
Programa 11.8
1 void maximo(double m, double n)
2 {
3 max = m; /* mutante 1: modificado para max = abs(m) */
4 if (n > max)
5 max = n; /* mutante 2: modificado para max = 6 */
6 return max;
7 }
*
Muitos pesquisadores têm considerado o problema de suficiência [50, 187, 290], relacio-
nando-o ao termo correção coincidente. Determinar condições de suficiência é então uma
questão indecídivel. DeMillo [116] considera um mutante morto quando a saída obtida di-
verge da saída do programa original. Ele considera que se um dado de teste satisfez as
condições de alcançabilidade e de necessidade, um estado incorreto foi produzido, e saídas
iguais foram obtidas, três situações poderiam ter ocorrido:
Em experimentos conduzidos por DeMillo e Offutt [116], notou-se que a terceira situação
raramente acontece na prática. A probabilidade de se satisfazer a condição de suficiência,
dado que as condições de necessidade e alcançabilidade foram satisfeitas, é muito grande.
Em 90% dos mutantes analisados, quando predicados não estavam envolvidos, a produção de
um estado intermediário incorreto garantiu um estado final incorreto e, conseqüentemente,
a morte do mutante. A posição adotada por DeMillo e Offutt [116] foi então assumir que a
satisfação das condições de necessidade e alcançabilidade implica a satisfação da condição
de suficiência.
Por outro lado, apenas 60% dos mutantes que envolviam mutações em predicados fo-
ram mortos. Em muitos casos, embora um efeito no estado do mutante fosse registrado,
o resultado da avaliação de um predicado continuava o mesmo que o do programa origi-
nal. Um exemplo desse problema é apresentado na Tabela 11.1. A condição de necessi-
dade gerada para provocar a diferença de estado é I = 3. Observe que o dado de teste
I = 7, J = 9, K = 7 satisfaz essa restrição, mas o predicado continua sendo avaliado como
verdadeiro, como era esperado.
Estratégias de teste referenciadas como teste baseado em predicados requerem, em geral, que
cada predicado, ou condição, do programa seja testado.
Os predicados no programa dividem o domínio de entrada do programa em partições e
definem os caminhos do programa. Predicados são classificados em predicados simples e
compostos.
A motivação para que técnicas de teste de predicados fossem introduzidas é a determina-
ção de defeitos em predicados.
As técnicas baseadas em predicados foram classificadas por Tai [382] e se dividem em
dois grupos: as que testam predicados simples e as que testam predicados compostos. Tai
também discute aspectos de eficácia de tais técnicas em determinar defeitos de predica-
dos. Técnicas que consideram apenas predicados simples podem deixar de detectar erros
de predicados em expressões booleanas. As técnicas que consideram predicados compos-
tos propõem testes exaustivos para todas as combinações, o que pode torná-las viáveis so-
mente se o predicado contiver um número pequeno de operadores. O problema com pre-
dicados compostos é que, se o teste exaustivo não for realizado, poderá acontecer de, em-
bora os predicados individuais sejam avaliados incorretamente, o resultado da avaliação de
toda a expressão ser coincidentemente correto. Por exemplo, para o predicado composto
P = (E1 = E2 )&&(E3 < E4 ), os seguintes testes são requeridos:
t1 : E1 = E2 e E3 = E4
t2 : E1 > E2 e E3 > E4
t3 : E1 < E2 e E3 < E4
Note que, para esse conjunto de testes, todas as possíveis situações para os dois predi-
cados simples foram consideradas. Entretanto, o conjunto não distingue o predicado P do
predicado R = (E1 <> E2 )&&(E3 = E4 )
Essa situação é semelhante à que acontece no teste baseado em restrições, descrita na
subseção anterior. Um estado incorreto é produzido mas não é suficiente para que o defeito
seja revelado. DeMillo [116] sugeriu o uso de uma restrição adicional chamada restrição de
predicado, que é um tipo de condição de suficiência. Tai [382] propõe duas técnicas de teste
para predicados compostos. As técnicas são baseadas em erros, pois requerem que dados de
teste sejam executados para satisfazer um conjunto de restrições. O conjunto de restrições
é projetado para garantir que defeitos em operadores booleanos e defeitos em operadores
relacionais sejam detectados, com a suposição de que não existam defeitos de outros tipos.
A grande vantagem é que um conjunto de restrições que descrevem vários tipos de erros
é gerado de uma só vez. Um número grande de mutantes é morto se as restrições forem
288 Introdução ao Teste de Software ELSEVIER
satisfeitas; no caso a suposição é satisfeita porque cada mutante difere do original por uma
mutação simples.
Tai [382] propôs duas técnicas para detectar defeitos em predicados compostos. A primeira,
chamada teste do operador booleano, ou técnica BOR (Boolean Operator Testing), garante a
detecção de defeitos em operadores booleanos. Tai afirma que se uma técnica é efetiva em
determinar defeitos em operadores booleanos, ela também o será em determinar defeitos em
expressões booleanas.
A segunda técnica, chamada teste dos operadores booleano e relacional ou técnica BRO
(Boolean and Relational Operator Testing), garante a detecção de defeitos em operadores
booleanos e relacionais.
Tai utiliza a notação a seguir para definir as restrições que serão requeridas pelas técnicas
BRO e BOR.
Para uma variável booleana B:
Dado o predicado C = (E1 ≥ E2 ) || !(E3 > E4 ), uma restrição para C é dada por uma
lista de elementos l = (v1 , v2 , .., vn ) que denota os valores para as variáveis booleanas ou
expressões relacionais. A restrição dada pela lista X = {(=, <)} requer um teste, fazendo
E1 = E2 e E3 < E4 . Note que o operador “! ” não afeta o requisito <. O valor produzido por
C quando satisfeita a restrição X é dado por C(X). O conjunto de todas as restrições para
um predicado C é dado por S. S é dividido em dois conjuntos. O primeiro conjunto St (C)
11.5. Geração de dados sensíveis a defeitos 289
composto por todas as restrições X em que C(X) = true, e o segundo conjunto Sf (C) com-
posto por todas as restrições X em que C(X) = f alse. A concatenação de duas restrições
(listas) l1 e l2 é denotada por (l1 , l2 ).
Sejam A e B dois conjuntos de restrições. A % B denota o conjunto mínimo de elementos
(u, v), tais que u(v) ∈ A(B) e cada elemento de A(B) aparece como u(v) pelo menos uma
vez. Se A = {(=), (>)} e B = {(<), (>)}, A % B tem dois valores possíveis:
• C1 || C2 .
F (C) = S1f % S2f
T (C) = {S1t ∗ {f2 }} $ {{f1 } ∗ S2t }
em que f1 ∈ S1f e f2 ∈ S2f e (f1 , f2 ) ∈ F (C)
• C1 && C2 .
T (C) = S1t % S2t
F (C) = {S1f ∗ {t2 }} $ {{t1 } ∗ S2f }
em que t1 ∈ S1t e t2 ∈ S2t e (t1 , t2 ) ∈ T (C)
290 Introdução ao Teste de Software ELSEVIER
Para o predicado P = ((E1 < E2 ) && ((E3 ≥ E4 ) || (E5 = E6 ))), a construção das
restrições é feita de uma forma botom-up, visitando-se a árvore sintática da Figura 11.9.
Ao aplicar as técnicas BOR e BRO geram-se conjuntos de restrições S1 , S2 e S3 corres-
pondentes aos predicados C1 , C2 e C3 . Gera-se um conjunto S4 para a expressão C1 || C3
utilizando-se S2 e S3 . Gera-se, para a expressão C1 && (C2 || C3 ), um conjunto S5 a partir
de S1 e S4 . A Figura 11.10 mostra o total de restrições requeridas.
As ferramentas BGG [382] e PredTool [358] geram conjuntos BOR e BRO para predi-
cados de programas escritos, respectivamente em Pascal e C. Elas fornecem a cobertura das
restrições para um dado conjunto de testes.
11.6. Considerações finais 291
As restrições geradas pelas técnicas BRO e BOR podem ser utilizadas para gerar dados
de teste adequados à análise de mutantes, como dito anteriormente. Além disso, as restrições
poderão ser utilizadas tanto com técnicas de teste funcional quanto com técnicas estruturais.
Tai indica duas abordagens para utilizá-las com a técnica de teste baseada em caminhos. A
primeira é selecionar um conjunto de caminhos de tal maneira que cada restrição exigida seja
coberta pelo menos uma vez. A outra, gerar um dado de teste para executar cada restrição de
um caminho dado. Na prática, uma restrição também poderá ser não executável, caso no qual
nem todas as restrições derivadas poderão ser satisfeitas e, conseqüentemente, não se poderá
garantir que todos os erros de predicados foram detectados.
Com o objetivo de aumentar a eficácia das técnicas, Tai [382] propôs uma extensão cha-
mada teste de expressões booleanas e relacionais com parâmetro , ou técnica BRE() (Boo-
lean and Relational Expression Testing with parameter ). A idéia é substituir ocorrências de
> e < por + e −, respectivamente, sendo ε um número muito pequeno, maior que 0. Essa
técnica usa fundamentos da técnica teste de domínios e é mais eficaz para determinar defeitos
em expressões relacionais que envolvem outros tipos de erros além dos detectados pelas téc-
nicas BOR/BRO. BOR e BRO possuem maior capacidade de revelar defeitos de predicados
(defeitos em expressões e operadores booleanas e relacionais); a técnica BRE revela defeitos
semelhantes aos revelados pela técnica teste de domínios.
A técnica teste de domínios deve ser utilizada com um critério de seleção de caminhos.
Sugere-se um critério mais exigente porque assim será maior a probabilidade de o caminho
que revela o defeito ser selecionado. Essa técnica também é a mais difícil de ser aplicada e
automatizada; existem problemas com predicados compostos e não lineares, domínios dis-
cretos, etc.
A técnica de teste baseado em restrições tem sua eficácia dependente dos operadores de
mutação utilizados, visto que não é possível descrever todo tipo de defeito e que as restrições
de predicado são muito importantes por serem aproximações das restrições de suficiência.
As técnicas BOR/BRO são mais práticas e mais fáceis de ser automatizadas, mas são
menos poderosas em termos de eficácia. BRE pode ser mais eficaz, mas, por outro lado,
existe a dificuldade em se determinar o valor de .
Vergilio et al. [406, 408] propõem critérios de teste, denominados Critérios Restritos, que
têm o objetivo de combinar restrições de predicado, tais como as utilizadas no Teste Baseado
em Restrições e pelas técnicas BRO/BOR e BRE, para gerar dados de teste adicionais para o
teste estrutural. Os dados de teste devem executar o caminho dado para cobrir um determi-
nado elemento requerido por um critério estrutural e satisfazer a restrição que descreve um
defeito. Dessa maneira, aumenta-se a capacidade de revelar outros defeitos, geralmente não
revelados por critérios estruturais.
Um problema inerente a todas as técnicas de geração de dados sensíveis a erros é que,
no final, um conjunto de restrições é derivado. Programas que resolvam eficientemente essas
restrições, utilizando execução simbólica ou dinâmica e técnicas de computação evolutiva,
são necessários. Entretanto, nem sempre é possível encontrar essa solução; a indecidibilidade
permanece como obstáculo à atividade de geração de dados de teste, não sendo possível sua
completa automatização.
Capítulo 12
Depuração
12.1 Introdução
A depuração de software é comumente definida como a tarefa de localização e remoção
de defeitos [16]. Normalmente, ela é entendida como o corolário do teste bem-sucedido
[10], ou seja, ela ocorre sempre que um defeito é revelado. No entanto, defeitos podem ser
revelados em diferentes fases do ciclo de vida do software e a depuração possui características
diferentes, dependendo da fase em que se encontra o software. Por isso, os processos de
depuração que ocorrem durante a codificação, depois do teste e durante a manutenção, podem
ser diferenciados.
A depuração durante a codificação é uma atividade complementar à de codificação. Já
a depuração depois do teste é ativada pelo teste bem-sucedido, isto é, aquele que revela a
presença de defeitos, podendo beneficiar-se da informação coletada nessa fase. A depuração
durante a manutenção, por sua vez, ocorre por uma necessidade de manutenção no software,
que pode ter sido causada, por exemplo, por um defeito revelado depois de liberado o soft-
ware ou pela necessidade de acrescentar novas características a ele.
A relevância da atividade de depuração tem dirigido esforços de pesquisa em duas di-
reções: no entendimento do processo de depuração e no desenvolvimento de técnicas que
aumentem a sua produtividade. Vários experimentos foram desenvolvidos com o objetivo de
entender o processo de depuração de software e estabelecer um modelo de depuração.
Araki et al. [16], utilizando resultados de experimentos anteriores [410], definiram o mo-
delo de depuração chamado de Hipótese-Validação descrito na Figura 12.1. O modelo defi-
nido caracteriza a atividade de depuração como um processo interativo de síntese, verificação
e refinamento de hipóteses [423]. De acordo com esse modelo, o responsável pela depuração
do software (chamado a partir de agora simplesmente de programador) estabelece hipóteses
com relação à localização do defeito e à modificação necessária para corrigir o programa.
O processo de depuração é guiado pela verificação e pela refutação das hipóteses levanta-
das, bem como pela geração de novas hipóteses e refinamento das já existentes [16]. Note-se
294 Introdução ao Teste de Software ELSEVIER
que o modelo Hipótese-Verificação é genérico, não sendo vinculado a nenhum tipo particular
de depuração. Outros autores [66, 117] também desenvolveram modelos de depuração que
são variantes do modelo de Araki et al. [16], vinculados, porém, a técnicas específicas de
depuração.
Para investigar a atividade de depuração que ocorre depois do teste, foi definido o modelo
de Depuração Depois do Teste (DDT). Esse modelo foi desenvolvido a partir de modelos de
depuração propostos anteriormente na literatura [5, 16, 66, 117, 410] e tem como objetivo
12.2. Modelo de depuração depois do teste 297
indicar que tipo de informação deve ser utilizada e quais tarefas devem ser realizadas para
apoiar especificamente a depuração depois do teste. Os passos que compõem o modelo são
descritos na Figura 12.2.
citados pelos casos de teste, os programas mutantes “mortos” pelos casos de teste na análise
de mutantes (Capítulo 5), etc.
próximos. Eles estão próximos porque em geral o número de comandos executados e o tempo
entre o alcance do defeito e a observação dos efeitos são pequenos.
Essa técnica de depuração foi desenvolvida no final dos anos 60. Atualmente, algumas
linguagens de programação já incluem construções que permitem seu uso como a macro pre-
definida assert do padrão ANSI da linguagem C. Mais recentemente, algumas ferramentas
como ASAP [101] e APP [347] aumentaram ainda mais o poder expressivo das asserções. A
seguir, é descrito um exemplo que utiliza asserções e a ferramenta APP.
O Programa 12.1 anotado com asserções realiza a troca de valores de duas variáveis in-
teiras sem a utilização de uma variável intermediária por meio da operação de ou-exclusivo.
As asserções estão incluídas nos indicadores de comentários especiais /*@ ... @*/.
A cláusula assume define uma precondição (válida antes da execução do procedimento);
a cláusula promise especifica uma pós-condição (válida depois de executado o procedi-
mento); e a cláusula assert indica uma condição que deve ser verdadeira no corpo. O
operador in retorna o valor de uma variável antes da execução do procedimento. O Pro-
grama 12.2 contém a rotina que é ativada quando a cláusula promise é violada.
Programa 12.1
1 void swap(x,y)
2 int * x;
3 int * y;
4
5 /*@
6 assume x && y && x != y;
7 promise *x == in *y;
8 promise *y == in *x;
9 @*/
10
11 {
12
13 *x = *x ^ *y;
14 *y = *x ^ *y;
15
16 /*@
17
18 assert *y == in *x;
19
20 @*/
21 }
*
Programa 12.2
1 promise * x == in * y {
2 printf("%s invalid: file %s, ",__ANNONAME__,__FILE__);
3 printf("line %d, function %s:\n",__ANNOLINE__,__FUNCTION__);
4 printf("out *x == %d, out *y == %d\n",*x,*y);
5 }
*
A depuração com asserções permite o mapeamento de erros para sintomas internos, pois
elas indicam pontos do programa nos quais ocorrem discrepâncias em relação à especificação.
Portanto, essa técnica de depuração apóia o Passo 2 do modelo DDT, utilizando informação
básica de teste. Segundo Rosenblum [347], o custo em termos de espaço e tempo de execução
dos programas anotados com asserções é insignificante, o que viabiliza sua utilização em sis-
temas reais. Entretanto, essa técnica requer a codificação adicional de parte da especificação,
que, por sua vez, poderá, também, conter defeitos.
12.3. Técnicas de depuração e o modelo DDT 301
Nesta subseção são apresentadas diferentes técnicas para “fatiar” programas, isto é, para
identificar trechos de código do programa com grande probabilidade de conter defeitos.
Fatiamento1 (slicing) de programas é uma técnica cujo objetivo é selecionar fatias (slices)
do programa. Uma fatia é um conjunto de comandos que afetam os valores de uma ou mais
variáveis em determinado ponto do programa. As variáveis e o ponto do programa definem o
critério de fatiamento. A fatia pode ser determinada estática ou dinamicamente. No primeiro
caso, os comandos selecionados podem afetar as variáveis no ponto especificado para alguma
possível entrada do programa. No segundo caso, os comandos selecionados efetivamente
afetam os valores das variáveis no ponto especificado para determinada entrada.
Tanto as fatias estáticas quanto as dinâmicas podem ser executáveis ou não. Se executá-
vel, a fatia é um subconjunto das instruções do programa original que também é um programa.
Esse novo programa fornece os mesmos valores para as variáveis selecionadas no ponto es-
pecificado do programa original. Fatias executáveis em geral são maiores porque precisam
incluir declarações de variáveis e outros comandos que não afetam o critério de fatiamento,
mas são necessários para a execução.
A Figura 12.3 contém um programa que calcula a soma das áreas de N triângulos, bem
como diferentes tipos de fatias deste programa. O programa e as fatias estática e dinâmica
foram obtidos da tese de doutorado de Agrawal [5]. O critério de fatiamento utilizado para a
fatia estática foi a variável sum localizada no comando 42. A fatia dinâmica foi determinada
para o caso de teste cujos valores de entrada são: N = 2 e os triângulos (3, 3, 3) e (6, 5, 4); os
valores de saída foram: N = 2 e sum = 13,90. O critério de fatiamento utilizado foi o valor da
variável sum quando da execução do último comando, no caso o comando 42. Esse caso de
teste falha e revela o defeito localizado no comando 18. O valor correto para sum é 13,82.
A técnica de fatiamento de programas apóia dois aspectos do modelo DDT: mapeamento
de falhas para possíveis sintomas internos (Passo 2) e seleção de novos possíveis sintomas a
partir daqueles inicialmente identificados (Passo 4). Nas duas tarefas é utilizada informação
básica de teste.
Essa técnica, porém, possui alguns problemas. O primeiro é o tamanho das fatias, tanto
estáticas (especialmente) como dinâmicas. O exemplo da Figura 12.3 ilustra esse problema.
O número de comandos selecionados pelas fatias estática (SS) e dinâmica (DS) não é muito
diferente do número total de comandos do programa. Para programas de grande porte, o
número de comandos que afetam um critério de fatiamento pode também ser muito grande,
o que pode tornar a técnica pouco atrativa.
Para superar parte desse problema, Korel e Rilling [223] desenvolveram, internamente
aos procedimentos, fatias dinâmicas parciais (por exemplo, fatia do código de um laço) de
forma a restringir o tamanho do “pedaço” de código que o programador terá de investigar.
1 Existem muitos artigos que discutem fatiamento de programas além das referências básicas [9, 221, 427]. Em
especial, Tip [395] e Kamkar [205] possuem excelentes revisões bibliográficas que discutem vários aspectos dessa
técnica (por exemplo, fatiamento estático e dinâmico, algoritmos, estrutura de dados, fatiamento intra e interproce-
dimental, tratamento de apontadores e de programas não-estruturados, custo, etc.).
302 Introdução ao Teste de Software ELSEVIER
SS DS SD DD ED Cmd Código
1 #define MAX 100
2 typedef enum { equilateral, isosceles, right, scalene}
class_type;
3
4 main()
5 {
6 int a, b, c;
7 class_type class;
8 int a_sqr, b_sqr, c_sqr, N, i;
9 double area, sum, s;
10 printf("Enter number of triangles:");
11 scanf("%d", &N);
12 sum = 0;
13 i=0;
14 while (i < N) {
15 printf("Enter three sides of triangle
in descending order:", i+1);
16 scanf("%d %d %d", &a, &b, &c);
17 a_sqr = a * a;
18 b_sqr = b * c; /* <- correct: b_sqr = b * b; */
19 c_sqr = c * c;
20 if((a == b) && (b == c))
21 class = equilateral;
22 else
23 if((a == b) || (b == c))
24 class = isosceles;
25 else
26 if(a_sqr == b_sqr + c_sqr)
27 class = right;
28 else
29 class = scalene;
30 if(class == right)
31 area = b * c/2.0;
32 else
33 if(class == equilateral)
34 area = a * a * sqrt(3.0)/4.0;
35 else {
36 s = (a + b + c)/2.0;
37 area = sqrt(s*(s-a)*(s-b)*(s-c));
38 }
39 sum=sum+area;
40 i=i+1;
41 }
42 printf("Sum of areas of the %d triangles is %.2f.",
N, sum);
43 }
SS – static slice; DS – dynamic slice; DD – dynamic dice; ED – execution dice
dos dados coletados durante a execução (comandos executados e posições de memória aces-
sadas) que pode demorar dezenas de minutos. Uma vez realizado esse pré-processamento,
diferentes fatias de uma mesma execução podem ser obtidas em segundos.
Pan [117, 321] propõe a identificação de um novo tipo de fatia, chamada fatia crítica, durante
o teste baseado em defeitos (análise de mutantes). Esse novo tipo de fatia é definido da
maneira a seguir. Suponha-se que um programa P tenha produzido um valor incorreto para
uma variável de saída v. Seja M uma versão alterada de P (mutante), em que apenas um
comando S tenha sido eliminado. Se o valor de v é diferente quando M é executado com um
caso de teste, então ele é um comando crítico. A fatia crítica é composta pelos comandos
críticos do programa P .
O custo de determinação da fatia crítica isoladamente é muito alto, visto que, para um
programa com n comandos, seria necessário executar n programas mutantes M com cada
caso de teste [117]. Entretanto, esse custo pode ser amortizado durante o teste se a fatia
crítica for obtida durante a análise de mutantes utilizando o operador eliminação de comando.
DeMillo et al. [117] descrevem um experimento com programas pequenos em que as fatias
críticas selecionaram 25% menos comandos que as fatias dinâmicas.
Agrawal et al. [10, 6] propõem a determinação de uma fatia do programa a partir dos
resultados obtidos do teste estrutural do programa. O conjunto de comandos associados aos
requisitos de teste estruturais (por exemplo, nós, ramos, c-usos e p-usos) executados por um
caso de teste particular é chamado de fatia de execução.
A vantagem das fatias baseadas em informação de teste é que a maior parte do custo para
sua obtenção já foi amortizada durante o teste do programa. Entretanto, de modo semelhante
às outras fatias, há uma grande probabilidade de essas fatias incluírem um número elevado de
comandos. As fatias críticas, apesar da possível redução de 25% trazida em relação às fatias
dinâmicas, muito provavelmente incluem uma grande fatia de código quando utilizadas em
programas complexos e críticos. Já as fatias de execução são sempre iguais ou maiores que
as fatias dinâmicas.
Tanto as fatias críticas quanto as de execução são úteis para mapear informação detalhada
de teste para possíveis sintomas internos (comandos suspeitos). Portanto, essas duas técnicas
apóiam o Passo 2 do modelo DDT. Contudo, elas não apóiam a seleção de novos sintomas
internos (Passo 4). Isso porque as fatias crítica e de execução são determinadas a partir de
casos de teste, e não a partir de um critério de fatiamento que pode ser variado. Logo, não é
possível refiná-las.
Heurísticas utilizando fatias têm sido propostas para reduzir o espaço de busca para localiza-
ção do defeito [5, 10, 89, 74, 254, 322, 321]. A idéia é realizar operações com as fatias para
determinar um conjunto menor de comandos com grande probabilidade de conter o defeito.
As heurísticas mais simples realizam operações de subtração, intersecção e união de fatias
[322, 321].
304 Introdução ao Teste de Software ELSEVIER
Lyle and Weiser [254] introduziram o recorte2 de fatias estáticas. Os autores propõem
a subtração das fatias obtidas de variáveis de saída corretas, isto é, variáveis cujos valores
estão corretos para todos os casos de teste, das fatias de variáveis de saída incorretas, isto é,
que produziram um valor incorreto em pelo menos um caso de teste. A intuição subjacente
é que a eliminação dos comandos comuns a ambas as fatias pode levar à identificação de um
conjunto menor de comandos com maior chance de conter o defeito.
De maneira análoga, as fatias dinâmicas podem ser utilizadas para a obtenção de fragmen-
tos de recorte [5, 74, 322, 321]. Nesse caso, os fragmentos de recorte podem ser calculados
usando fatias das mesmas variáveis, não necessariamente de saída, que tenham produzido
valores corretos e incorretos em dois casos de teste diferentes. Fragmentos de recorte podem
ser igualmente obtidos a partir de fatias baseadas em informação de teste. A técnica de re-
corte pode ainda envolver a subtração entre o resultado da união ou intersecção de fatias de
variáveis obtidas de casos de teste que falham e o resultado da união ou intersecção de fatias
de casos de teste que passam.
Na Figura 12.3, são apresentados exemplos de fragmentos de recorte. Pela utilização
do caso de teste que falha apresentado anteriormente, o fragmento de recorte estático (SD)
foi determinado fazendo a subtração da fatia estática de N (variável correta) da fatia estática
de sum (variável incorreta), ambas as fatias com relação ao comando 42. O fragmento de
recorte dinâmico foi igualmente determinado utilizando um caso de teste que passa. Para
N = 2 e triângulos (4, 4, 4) e (5, 3, 3), o programa produz o valor correto de sum igual a
11,07. As fatias dinâmicas foram determinadas com respeito a sum no comando 42 para
ambos os casos de teste – que falha e que passa. O fragmento de recorte dinâmico (DD)
foi obtido da subtração das fatias dinâmicas por Agrawal [5]. O fragmento de recorte de
execução (ED) é calculado subtraindo os nós exercitados pelo caso de teste que passa dos nós
exercitados pelo caso de teste que falha.
A Figura 12.3 mostra que as heurísticas baseadas em fatias reduzem o espaço de busca dos
defeitos. O fragmento de recorte dinâmico, o qual inclui apenas os comandos que realmente
afetam o critério de fatiamento, contém apenas seis comandos e inclui o comando que contém
o defeito. No entanto, a Figura 12.3 também ilustra alguns dos problemas relacionados com
as heurísticas, em especial com os fragmentos de recorte. Por exemplo, o fragmento de
recorte estático é quase igual à fatia estática de sum. Isso ocorre porque a intersecção entre
as fatias estáticas da variável incorreta e da variável correta contém apenas um comando (11).
O fragmento de recorte de execução, por sua vez, não inclui o comando com o defeito, pois
o nó no qual o comando “defeitoso” está contido é exercitado pelo caso de teste que falha
e pelo que passa. É importante observar que esse problema também pode ocorrer com os
fragmentos de recorte dinâmicos.
Outra maneira de identificar, heuristicamente, comandos do programa com grande pro-
babilidade de conter o defeito é estabelecendo um ranking entre eles. Nesse ranking, os
comandos ocorridos mais freqüentemente em fatias de casos de teste que manifestam falhas
são mais bem classificados do que os não tão freqüentemente ocorridos [322, 321]. Essa idéia
foi inicialmente proposta por Collofello e Cousins [89] para evitar que um defeito localizado
em um trecho do código executado tanto por casos de teste que falham quanto que não falham
fosse eliminado na operação de subtração da técnica de recorte.
2 Os termos dicing (fatiamento em cubos) e dice (cubo, dado) foram traduzidos para os termos recorte e fragmento
de recorte, respectivamente.
12.3. Técnicas de depuração e o modelo DDT 305
Esses autores definem dez heurísticas que realizam operações de recorte e de ranking
de nós (obtidos durante o teste com o critério todos nós) para identificar trechos de código
suspeitos [89]. Agrawal et al. [10] revisitaram essa abordagem para a definição das fatias
de execução e também de heurísticas baseadas em recorte de outros requisitos de teste (por
exemplo, ramos, p-usos, c-usos) executados pelos casos de teste.
O uso de heurísticas impõe alguns riscos. Apesar de a intuição ser de haver grande pro-
babilidade do trecho de código selecionado conter o defeito, também há a chance de ele ser
excluído durante as operações envolvendo conjuntos (por exemplo, intersecção, subtração)
ou durante a criação do ranking. Nesse último caso, o trecho selecionado pode incluir os
efeitos do defeito, mas excluir o próprio defeito. Além disso, as heurísticas que operam sobre
fatias possuem as mesmas restrições inerentes à técnica utilizada para obtê-las.
Do ponto de vista do modelo DDT, as heurísticas que utilizam fatias estáticas e dinâmicas,
da mesma maneira que a técnica de fatiamento de programas, apóiam as tarefas definidas nos
Passos 2 e 4, utilizando informação básica de teste. Já as heurísticas que utilizam fatias
baseadas em informação de teste apóiam somente a tarefa de mapeamento para sintomas
internos (Passo 2), utilizando informação detalhada de teste.
As heurísticas baseadas em requisitos de teste estrutural [10, 89, 322] apóiam apenas o Passo
2 do modelo DDT porque se baseiam em uma informação estática – o trecho de código obtido
do mapeamento dos requisitos de teste selecionados. Com o objetivo de tratar esse problema,
Chaim et al. [63, 64] propuseram o uso de informação dinâmica de teste para a localização
de defeitos.
O pressuposto é que o conjunto de requisitos selecionados pelas heurísticas ainda forne-
cem indicações úteis para a depuração mesmo quando falham em incluir o defeito no trecho
de código selecionado. No entanto, essas informações estão disponíveis em tempo de execu-
ção. Os autores desenvolveram estudos empíricos que mostram que esse pressuposto é válido.
A partir dele, foi desenvolvida uma estratégia de depuração que combina o uso de heurísti-
cas para seleção inicial de requisitos de teste e mecanismos para o refinamento sucessivo das
informações obtidas em tempo de execução até a localização do defeito.
A estratégia de depuração baseada em informação dinâmica de teste parece promissora
porque apóia tanto a geração (por meios de heurísticas) como a seleção (utilizando os me-
canismos de refinamento) de hipóteses, ou seja, os Passos 2 e 4 do modelo DDT. Adicional-
mente, os mecanismos desenvolvidos podem ser implementados em depuradores simbólicos
comuns com overhead limitado no seu desempenho e utilizam algoritmos de ordem linear
em função do número de ramos do programa.
A técnica de depuração algorítmica foi originalmente desenvolvida para programas sem efei-
tos colaterais escritos em Prolog [353]. Essa técnica é baseada em um processo interativo
durante o qual o programador fornece conhecimento a respeito do comportamento esperado
do programa ao sistema de depuração e este, por sua vez, guia o programador durante o
processo de localização do defeito [146].
A técnica funciona como a seguir. Para a localização do defeito, o caso de teste que
manifestou uma falha deve ser executado sob a supervisão do sistema baseado em depuração
algorítmica. O sistema cria, então, uma árvore de execução na qual os nós representam
invocações dos procedimentos do programa. Esses nós contêm o nome do procedimento e
os valores de entrada e saída utilizados na invocação em particular. O sistema, então, visita,
partindo do procedimento de mais alto nível, os nós da árvore de execução, perguntando ao
programador se os valores dos parâmetros de entrada e saída estão corretos. Se a resposta
for positiva, o processo continua visitando os próximos nós de mesmo nível. Caso contrário,
o processo visita os nós dos procedimentos de nível inferior invocados pelo procedimento
de nível superior. Esse processo termina quando é identificado um procedimento no qual
os parâmetros de entrada estão corretos e os de saída incorretos ou que não faz chamada a
nenhum outro procedimento ou, se o faz, os valores dos parâmetros de entrada e saída dos
procedimentos invocados estão corretos.
Um exemplo de depuração algorítmica, apresentado por Fritzson et al.[146], para pro-
gramas escritos em Pascal é descrito utilizando o Programa 12.3. O procedimento P possui
dois parâmetros de entrada a e c e calcula os valores de dois parâmetros de saída b e d. O
valor de b é calculado chamando-se o procedimento Q, e o valor de d é obtido da chamada
ao procedimento R.
Programa 12.3
1 procedure P(a,c:integer; var b, d:integer);
2 procedure Q(a:integer; var b: integer);
3 ...
4 end;
5 procedure R(c:integer; var d: integer);
6 ...
7 end;
8 begin
9 Q(a,b);
10 R(c,d);
11 end;
*
A técnica de depuração algorítmica como proposta por Shapiro [353] visa à identificação do
procedimento no qual se encontra o defeito; entretanto, ela não apóia a localização interna-
mente ao procedimento. Korel [219, 223, 224] utiliza as técnicas de depuração algorítmica e
fatiamento de programas para estabelecer uma estratégia para encontrar o defeito dentro do
procedimento. A ferramenta PELAS (Program Error-Locating Assistant System) implementa
essa estratégia. Outras ferramentas como Spyder [5, 423], FIND [354, 355] e ALICE [218]
adotam abordagens semelhantes variando o mecanismo de interação com o usuário e a téc-
nica de fatiamento utilizada. A seguir, é mostrado como a ferramenta PELAS é utilizada
para descrever a localização de defeitos baseada em depuração algorítmica e fatiamento de
programas.
308 Introdução ao Teste de Software ELSEVIER
Considere o exemplo contido no Programa 12.4 (desenvolvido por Shimomura et al. [354])
para ilustrar o algoritmo de localização implementado na ferramenta PELAS. As entradas n
= 2 e a = (6, 2) produzem a saída incorreta s = 4 (a saída correta é s = 8). A Figura 12.5
contém os pontos de execução do programa para esse caso de teste, e a Figura 12.6 contém a
rede de dependência.
Programa 12.4
1 get(n,a);
2 t:=1; { correto: t:=10 }
3 s:=a[1];
4 i:=2;
5 while i =< n loop
6 s:=s-a[i]; { correto: s:=s+a[i]; }
7 i:=i+1;
8 end loop;
9 if s > t then
10 if s mod 2 != 0 then
11 s:=s+1;
12 end if;
13 end if;
14 put(s);
*
Figura 12.5 – Instância dos comandos executados pelo caso de teste n = 2 e a = (6, 2).
Figura 12.6 – Rede de dependência gerada pela ferramenta PELAS durante a execução
do programa.
dos do procedimento são executados. Essa questão é importante para programas com longas
execuções, pois a memória disponível pode ser totalmente consumida [225].
Os grafos construídos pelas ferramentas PELAS, Spyder e FIND tinham inicialmente o
objetivo de determinar fatias dinâmicas do programa. Novos algoritmos para a determinação
de fatias dinâmicas que não dependem da construção do grafo foram desenvolvidos [225,
158]. Entretanto, essas soluções, por evitar a construção do grafo, perdem as instâncias dos
comandos que influenciam determinado ponto da execução, o que impede a seleção de novos
sintomas internos (pontos de execução) a serem investigados.
Some-se a isso o fato de que essas ferramentas baseiam-se em execução em reverso, o que
compromete sua escalabilidade. Os novos algoritmos de fatiamento dinâmico e de execução
em reverso, se confirmados como escaláveis para programas reais, poderão tornar a depuração
algorítmica intraprocedimental viável.
A estratégia de depuração implementada nas ferramentas PELAS, Spyder, FIND e ALICE
expandem a idéia de depuração algorítmica (originalmente utilizada para a determinação do
procedimento defeituoso) para localização de defeitos internamente aos procedimentos. Por-
tanto, ela apóia o Passo 4 do modelo DDT utilizando informação básica de teste.
A depuração delta foi criada inicialmente por Zeller [454] com o objetivo de reduzir o tama-
nho da entrada de casos de teste que provocam a ocorrência de falhas. A idéia é determinar
o dado de entrada diretamente responsável pela falha do programa. Um problema típico de
utilização da depuração delta é determinar qual dos comandos HTML provoca a falha de car-
regamento de uma página HTML. A depuração delta soluciona essa questão fazendo o teste
de carregamento de páginas que são resultantes da diferença entre a página que provocou a
falha e uma página que não provocou (no caso, uma página vazia).
Considere uma página que contenha metade do texto da página que provoca a falha. Se
a falha continua a ocorrer, então o trecho que induz à falha está na metade incluída. Se,
por outro lado, a falha não ocorre mais, então o trecho omitido é o que contém a falha. O
caminho inverso pode ser também percorrido partindo da página que não provoca a falha e
incluindo trechos da página errônea. Ao repetir esse processo inúmeras vezes, pode-se chegar
ao comando HTML que provoca a ocorrência da falha.
O algoritmo da depuração delta é inspirado na busca binária, porém, tratando situações
em que a página carregada produz um resultado indeterminado. A depuração delta pode ser
utilizada para identificar outras circunstâncias causadoras de falhas. Por exemplo, considere
que um programa teve 10.000 linhas alteradas e uma falha ocorre quando executado por um
determinado caso de teste. Qual dessas 10.000 linhas causa a falha? Fazendo a execução
repetida de versões do programa com diferentes conjuntos de linhas modificadas e utilizando
o algoritmo de depuração delta, podem-se identificar as linhas que dão origem à falha.
Zeller et al. [87, 454] propuseram a utilização da depuração delta para identificação de
trechos do programa que causam a ocorrência da falha. Isso é realizado comparando-se o
estado do programa em execuções que manifestam a falha e execuções que não manifestam.
São feitas alterações no estado do programa utilizando o algoritmo de depuração delta para
identificar os comandos que causam a manifestação da falha. Os resultados obtidos são pro-
12.4. Comparação das técnicas e o modelo DDT 311
missores, mas o custo para utilizar essa técnica em programas com longas execuções ainda é
uma questão em aberto.
Do ponto de vista do modelo DDT, a depuração delta auxilia o Passo 2, pois ajuda no
mapeamento das entradas em sintomas internos, seja por meio de simplificação da entrada,
seja pela identificação de trechos do programa candidatos a conter o defeito. O tipo de infor-
mação utilizada é simples, daí a necessidade de repetidas execuções para obter informação
útil para a depuração.
Confiabilidade
13.1 Introdução
Com o constante desenvolvimento da tecnologia, os sistemas computacionais têm sido re-
quisitados em quase todas as áreas da atividade humana. Especificamente nos últimos anos,
softwares específicos foram desenvolvidos para aplicações críticas, como sistemas de con-
trole de usinas nucleares, sistemas de controle aeroespacial, controle de processos na área
médica e muitas outras áreas de risco no campo industrial. A natureza de sistemas compu-
tacionais em termos de precisão de tempo e em termos do comportamento repetitivo faz do
software a solução ideal para áreas nas quais um simples engano pode causar efeitos extre-
mamente danosos.
Essa crescente dependência em relação ao software tem conscientizado tanto os usuários,
que cada vez mais exigem softwares confiáveis, como também a indústria de software, no
sentido de desenvolver produtos de alta qualidade. No entanto, além de freqüentemente o
software constituir a parte mais dispendiosa para a solução de um problema que envolve o
computador, desenvolver software com qualidade tem exigido um enorme esforço na ativi-
dade de teste e também tem sido uma tarefa extremamente difícil. A principal razão dessa
dificuldade é que o software é uma entidade lógica, diferentemente de muitos outros siste-
mas em que os componentes têm alguma forma física para os quais um valor concreto de
qualidade pode ser alcançado. Uma outra evidência dessa dificuldade é que o software é um
artefato extremamente complexo que não pode ser analisado por formalismos matemáticos
bem estruturados [258].
Nesse sentido, as evidências apontam a necessidade de pesquisas na área, tendo por fim
também um melhor entendimento do que vem a ser a qualidade de software.
Em um contexto mais amplo, qualidade de software é uma propriedade multidimensional
ainda difícil de ser medida e, conforme a norma ISO9126, é constituida pelas características:
funcionalidade, confiabilidade, eficiência, portabilidade, usabilidade e manutenibilidade. To-
davia, a confiabilidade, ao contrário de outras características, é comumente aceita como um
316 Introdução ao Teste de Software ELSEVIER
fator-chave da qualidade, uma vez que pode ser medida e estimada usando dados históricos,
qualificando, assim, as falhas do software.
A confiabilidade é uma característica que tem sido extensivamente considerada na análise
da qualidade do software, pois se um software não é confiável, pouco importa se outras
características da qualidade são aceitáveis. Por outro lado, medir a confiabilidade de um
software tem-se mostrado uma tarefa desafiadora.
A preocupação com a confiabilidade de software teve início por volta de 1967 com Hud-
son [193]. A partir dos anos 70, fundamentados na teoria sobre confiabilidade de hardware,
surgiram os primeiros estudos e os primeiros modelos de confiabilidade de software [199,
356]. Na década de 1980 ampliaram-se os estudos e surgiram vários outros modelos.
Ao atribuir-se um grau de confiabilidade ao software, o objetivo é quantificar alguns
aspectos desse software que, devido à presença inevitável de defeitos, está sujeito a falhas
durante seu período de utilização. Assim, o estudo da confiabilidade de software caracteriza-
se como uma abordagem analítica que está baseada nos conceitos de métricas, medidas e
modelos.
A confiabilidade representa a qualidade do ponto de vista do usuário. Sabe-se que uma
confiabilidade de 100%, mesmo para programas de baixo nível de complexidade, é prati-
camente impossível de ser obtida. Do ponto de vista das organizações que desenvolvem
software, a confiabilidade é uma referência para a avaliação do software. Nesse contexto, a
indústria analisa o software para garantir que o produto atingiu um certo nível de confiabi-
lidade como um critério para sua liberação. Para isso, o teste é extensivamente conduzido
tanto para remover defeitos quanto para determinar o nível de confiabilidade.
De uma maneira geral, um sistema de software é dito confiável se desempenha correta-
mente suas funções especificadas por um longo período de execução e em uma variedade de
ambientes operacionais [155, 292]. Essencialmente, existem três maneiras de se alcançar um
alto nível de confiabilidade:
Para tanto, torna-se necessária a coleta de dados de falhas. Essa coleta compreende:
1) contagem de falhas para o rastreamento da quantidade de falhas observadas por unidade
de tempo; 2) tempo médio entre falhas que faz o rastreamento dos intervalos entre falhas
consecutivas. De posse desses dados, a engenharia de confiabilidade de software pode desen-
volver atividades de estimação e previsão.
Para melhor entendimento do significado de confiabilidade consideremos, como exem-
plo, um automóvel. Por melhor que seja o processo de fabricação, o carro deve passar por
períodos de manutenção para reparos de funcionalidades ou substituição de peças desgasta-
das. Dessa maneira, o carro é considerado confiável se, por longos períodos, apresentar um
comportamento desejável e consistente entre os períodos de manutenção.
Em relação ao software, a confiabilidade é definida como a probabilidade de que o soft-
ware não falhe em um dado intervalo de tempo, em um dado ambiente [292]. Logo, é uma
medida importante para decidir sobre a liberação do software. A probabilidade de ocorrência
de falha serve também como um preditor útil da confiabilidade corrente para o software em
operação. Um software é considerado altamente confiável quando pode ser utilizado sem re-
ceio em aplicações críticas. O contínuo crescimento de tamanho e complexidade do software
projetado atualmente torna a confiabilidade o aspecto indiscutivelmente mais importante de
qualquer sistema de software.
Apesar de se utilizar o carro como exemplo para o entendimento da confiabilidade, a
confiabilidade de software é completamente diferente da confiabilidade de hardware. Ao
contrário do hardware, o software não envelhece e não sofre desgaste por ação do uso ou do
tempo. No hardware os defeitos são causados por peças de baixa qualidade ou por desgas-
tes naturais das peças. No software os defeitos podem ser introduzidos nas várias fases do
desenvolvimento e são causados principalmente por problemas no projeto. A confiabilidade
do hardware pode ser aumentada por substituição de material de melhor qualidade e práticas
de projeto mais aperfeiçoadas. O crescimento da confiabilidade do software resulta da des-
coberta e da eliminação de defeitos no software obtidos pela aplicação de um teste intensivo
e de qualidade.
Devido a essas diferenças, a confiabilidade de hardware é tratada de maneira diferente
da confiabilidade de software. Quando um hardware é reparado, ele retorna ao seu nível de
confiabilidade anterior, ou seja, a confiabilidade do hardware é mantida. Entretanto, quando
um software é reparado, a sua confiabilidade pode tanto aumentar como diminuir, caso novos
defeitos sejam inseridos durante o reparo. Assim, o objetivo da engenharia de confiabilidade
de hardware é manter a estabilidade, enquanto o objetivo da engenharia de confiabilidade de
software é melhorar a confiabilidade.
13.2.1 Definições
A teoria sobre confiabilidade de software lida com métodos probabilísticos aplicados para
analisar a ocorrência aleatória de falhas em um dado sistema de software. Nesta seção são
apresentados alguns conceitos que formalizam a teoria da confiabilidade, tais como: função
Confiabilidade, função Taxa de Falhas, função de Falhas Acumuladas, função Intensidade de
Falhas, Tempo Médio para Falhas (MTTF) e Tempo Médio entre Falhas (MTBF).
No estudo sobre confiabilidade de software, a variável aleatória de interesse pode ser o
tempo T decorrido para a ocorrência de uma falha do software. Por conseqüência, a base
dos resultados sobre confiabilidade estará relacionada ao estudo da variável aleatória T , mais
especificamente, ao estudo das funções associadas à variável aleatória T , tais como função
densidade de probabilidade f (t) e função distribuição de probabilidades acumuladas F (t).
A probabilidade de que haja uma falha no software em decorrência do tempo t é definida
como:
t
F (t) = P [0 ≤ T ≤ t] = f (x)dx (13.1)
0
1 − F (t) = 1 − P [0 ≤ T ≤ t] = P [T > t]
13.2. Fundamentos de confiabilidade de software 319
Função Confiabilidade
+∞
R(t) = P [T > t] = 1 − F (t) = f (x)dx (13.2)
t
Quando uma base de tempo é determinada, as falhas no software podem ser expressas
por várias funções, como: função Taxa de Talhas, função de Falhas Acumuladas, função
Intensidade de Falhas, tempo médio para falhas (MTTF) e tempo médio entre falhas (MTBF).
Para determinar o comportamento de falhas no software, basta observar o comportamento
de uma dessas funções. Ou seja, para determinar a forma funcional do modelo que representa
o comportamento das falhas no software, é só determinar a forma funcional de apenas uma
dessas funções. O comportamento das demais funções pode ser explicitamente determinado.
Alguns modelos de confiabilidade de software são determinados por suposições no compor-
tamento da taxa de falhas, outros são determinados por suposições no comportamento da
função de Falhas Acumuladas.
Para descrever o ritmo de ocorrência das falhas em um sistema, a taxa de falhas é um conceito
bastante utilizado. Teoricamente, a taxa de falhas é definida como a probabilidade de que
uma falha por unidade de tempo ocorra num intervalo [t, t + Δt], dado que o sistema não
falhou até o tempo t. Na prática, a taxa de falhas é a razão entre o incremento do número de
falhas e o incremento de tempo correspondente. Assim, usando a probabilidade condicional,
tem-se que:
F (t + Δt) − F (t)
=
ΔtR(t)
A taxa de falhas instantânea, Z(t), também conhecida como taxa de risco associada à
variável aleatória T , é definida como:
Ou seja, a taxa de risco é definida como o limite da taxa de falhas quando o intervalo Δt
tende a zero (Δt → 0).
320 Introdução ao Teste de Software ELSEVIER
A taxa de risco é uma taxa de falhas instantânea no tempo t, dado que o sistema não
falhou até o tempo t. Embora haja uma pequena diferença entre a taxa de falhas e a taxa de
risco, normalmente usa-se Z(t) como taxa de falhas [255].
As funções f (t), F (t), R(t), e Z(t) matematicamente fornecem especificações equiva-
lentes sobre a distribuição da variável aleatória T e podem ser derivadas uma da outra por
simples operações algébricas.
Observa-se que:
f (t) dF (t) 1
Z(t) = = (13.3)
R(t) dt R(t)
dF (t) dR(t)
=− (13.4)
dt dt
Substituindo a Equação(13.4) na Equação (13.3), tem-se:
dR(t) 1
−Z(t) = (13.5)
dt R(t)
Rt
R(t) = e− 0
Z(x)dx
(13.6)
Utilizando a Equação (13.3) tem-se que a relação entre a função densidade de probabili-
dade f (t) e a taxa de falhas Z(t) é dada por:
Rt
f (t) = Z(t)e− 0
Z(x)dx
(13.7)
A taxa de falhas se altera ao longo do tempo de vida do sistema. A Figura 13.1 ilustra o
comportamento da taxa de falhas de alguns sistemas.
A Região I, conhecida como fase de depuração, representa as falhas iniciais do sistema.
Nessa região a taxa de falhas tende a decrescer com o tempo.
A Região II, conhecida como período de vida útil do sistema ou fase de operação, repre-
senta as falhas causadas por eventos aleatórios ou por condições de stress do sistema. Nessa
região a taxa de falhas permanece constante com o tempo.
A Região III representa a fase de desgaste do sistema, caracterizada pelo crescimento na
taxa de falhas em função do tempo.
13.2. Fundamentos de confiabilidade de software 321
para o teste do software até que se atinja o objetivo especificado – a taxa de falhas desejada.
Pode-se também estimar a confiabilidade ao término do teste.
Quando se considera crescimento de confiabilidade, uma medida usual é a confiabilidade
condicional ([154, 293]). Dado que o sistema teve n−1 falhas, a confiabilidade condicional é
a função de sobrevivência associada à n-ésima falha do sistema. A confiabilidade condicional
é de interesse quando o sistema está em fase de desenvolvimento, período em que se observa o
tempo para a próxima falha. Quando o sistema está liberado e em fase operacional, o interesse
passa a ser o intervalo de tempo livre de falhas e, nesse caso, os instantes de falha não são
necessariamente condicionados às falhas anteriores. O interesse é a confiabilidade em um
dado intervalo de tempo, independentemente do número de falhas ocorridas anteriormente.
A Tabela 13.1 ilustra as relações existentes entre a função densidade de probabilidade
f (t), a função de distribuição de probabilidades acumulada F (t), a função confiabilidade
R(t) e a função taxa de falhas Z(t).
Seja Ti (i = 1, 2, 3, ...) a variável aleatória que representa o i-ésimo intervalo de tempo
entre falhas e seja Ti (i = 1, 2, 3, ...) a variável aleatória que representa o i-ésimo tempo de
falha.A Figura 13.3 ilustra o comportamento dessas variáveis aleatórias, mostrando que
Ti = j=1 Tj = Ti−1 + Ti para i = 1, 2, 3, ..., e T0 = 0.
i
As relações anteriores mostradas na Tabela 13.1 são também válidas para a confiabilidade
condicional.
13.2. Fundamentos de confiabilidade de software 323
Rt
dF (t)
f (t) - dt − dR(t)
dt Z(t)e− 0
Z(x)dx
t Rt
F (t) 0
f (x)dx - 1 − R(t) 1−e 0
Z(x)dx
+∞ Rt
R(t) 0
f (x)dx 1 − F (t) - e− 0
Z(x)dx
f (t) dF (t) 1 1
Z(t) R +∞
f (x)dx dt 1−F (t) − dR(t)
dt R(t) -
t
Figura 13.3 – Variáveis aleatórias Tempo de Falha (Ti ) e Tempo entre Falhas (Ti ).
A função de Falhas Acumuladas, também denominada função Valor Médio, é uma maneira
alternativa de caracterizar a ocorrência aleatória das falhas em um software. A função de
Falhas Acumuladas descreve o crescimento da curva de Falhas Acumuladas e, assim, consi-
dera o número de falhas ocorridas no software até um tempo t. A função Valor Médio é uma
maneira alternativa de representar o processo de falhas no software, e, assim, o comporta-
mento futuro pode ser estimado por uma análise estatística do comportamento dessa curva de
crescimento.
Teoricamente, o número de falhas acumuladas até o tempo t pode ser representado por
uma variável aleatória M (t) com um valor médio μ(t). Isto é, μ(t) representa a função valor
médio do processo aleatório.
Dessa forma, tem-se que:
μ(t) = E[M (t)]
em que E representa a média da variável aleatória M (t).
A Figura 13.4 representa um comportamento típico da função Valor Médio.
O processo aleatório pode ser completamente especificado, assumindo-se uma distribui-
ção de probabilidades para a variável aleatória M (t) para algum t.
324 Introdução ao Teste de Software ELSEVIER
[μ(t)k ] −μ(t)
P [M (t) = k] = e
k!
μ(t)k −μ(t) N
P [M (t) = k] k! e k p(t)k [1 − p(t)]N −k
A função Intensidade de Falhas λ(t) representa a taxa de variação instantânea da função Valor
Médio. Isto é, representa o número de falhas ocorridas por unidade de tempo. Por exemplo,
pode se dizer que houve 0,01 falha/hora ou então que houve uma falha a cada 100 horas.
A função Intensidade de Falhas representa a derivada da função Valor Médio e é um valor
instantâneo. A Figura 13.5 ilustra o comportamento da função Intensidade de Falhas.
Observa-se que no início do teste do software a ocorrência de falhas é maior em uma
unidade de tempo e, conseqüentemente, a função de Falhas Acumuladas ou função Valor
Médio cresce rapidamente. À medida que o teste prossegue, o número de falhas por unidade
de tempo tende a diminuir, ou seja, a função Valor Médio cresce mais lentamente. A taxa
de variação da função Valor Médio tende a decrescer rapidamente conforme o teste procede.
Com isso, a função Intensidade de Falhas tem um comportamento decrescente no tempo.
A função Intensidade de Falhas λ(t) é, então, obtida da função Valor Médio como:
d E[M (t)]
dμ(t)
λ(t) = = (13.8)
dt dt
dλ(t)
Para que haja crescimento da confiabilidade, é necessário que dt < 0 para qualquer
t ≥ t0 para algum t0 .
A função Tempo Médio para Falhas, MTTF (Mean Time to Failure), representa o tempo
esperado para a ocorrência da próxima falha, ou seja, é o tempo durante o qual o software
funciona sem falhas. O MTTF é uma medida que pode ser utilizada para caracterizar o
modelo de falhas de um sistema de software.
Suponha que um software esteja em teste e que tenham sido encontradas i − 1 falhas.
Registrando-se os tempos entre as falhas como t1 , t2 , t3 , . . . ti−1 , a média desses valores é o
tempo médio antes de ocorrer a próxima falha. Um MTTF de 500 significa que uma falha
pode ser esperada a cada 500 unidades de tempo.
Considere que cada defeito identificado tenha sido corrigido e que o sistema esteja no-
vamente em execução. Pode-se utilizar Ti para denotar o tempo antes da próxima falha, que
ainda será observada. Ti é uma variável aleatória e, quando se fazem declarações sobre a
confiabilidade do software, fazem-se declarações de probabilidades sobre Ti .
De uma maneira geral, se T é a variável aleatória que representa o tempo para a ocorrência
da próxima falha com uma distribuição de probabilidades, então o tempo médio para falhas
pode ser calculado como:
∞
M T T F = E[T ] = tf (t)dt (13.9)
0
Nesse contexto, o MTTF é próximo de zero quando a taxa de falhas do software é grande
e próximo de 1 quando a taxa de falhas do software é pequena. Utilizando essa relação,
pode-se calcular a medida da confiabilidade de um software como:
MTTF
C=
1 + MTTF
t1
Ctt12 = 1 − P [t1 < T < t2 ] = f (t)dt
t2
Distribuição exponencial
f (t) = λe−λt
e
F (t) = 1 − λe−λt
+∞
R(t) = λe−λx dx = e−λt
t
A função Taxa de Falhas pode ser obtida da Equação (13.3) e tem a forma:
f (t)
Z(t) = =λ
R(t)
O emprego da distribuição exponencial deve ter a hipótese de que o sistema tem uma
taxa de falhas constante. Seu uso é recomendado quando o software tem uma taxa de falhas
constante, isto é, na Região II da Figura 13.1 anteriormente discutida.
13.2. Fundamentos de confiabilidade de software 329
Observando a Tabela 13.3, nota-se que para t = 1 tem-se P [T < t] = 0, 393. Isso
significa que há uma probabilidade de 0,393 de o tempo entre a ocorrência de falhas ser
menor que uma hora, ou seja, quase 40% dos tempos entre as falhas são menores que uma
hora. Do mesmo modo, 0,918 é a probabilidade de o tempo entre falhas ser menor que 5
horas, ou seja, quase 92% dos tempos entre as falhas são menores que 5 horas, ou, de outra
forma, apenas 8% dos tempo entre falhas excedem 5 horas.
É evidente que a distribuição de probabilidades não diz quando as falhas ocorrem. No
entanto, ela fornece informações sobre o processo aleatório de ocorrência das falhas.
Distribuição de Weibull
A distribuição de probabilidades de Weibull [286], como outras distribuições, tem uma grande
aplicação em confiabilidade devido à sua adaptabilidade. Dependendo dos valores dos parâ-
metros, pode-se ajustar a muitos conjuntos de dados sobre falhas. Ao utilizar essa distribui-
ção, assume-se que, no processo de falhas do software, a variável aleatória T (tempo entre
falhas) segue a distribuição de Weibull.
A função Densidade de Probabilidade de Weibull, f(t), tem a forma:
α
f (t) = αβtα−1 e−βt
α
F (t) = 1 − e−βt
A função Taxa de Falhas, que pode ser obtida pela Equação (13.3), tem a forma:
Z(t) = αβtα−1
α
R(t) = e−βt
A função Tempo Médio para Falhas, que pode ser obtida pela Equação (13.9), tem a
forma:
Γ[ α1 + 1]
MTTF = 1
βα
para y > 0.
Casos especiais:
Assim, tem-se:
2
f (t) = 2βte−βt
e ainda
2
R(t) = e−βt
Distribuição Gamma
β α α−1 −βt
f (t) = t e
Γ(α)
em que α e β são parâmetros de forma e escala, respectivamente, com α > 0, β > 0 e t > 0.
A função Confiabilidade, que pode ser obtida da Equação (13.2), tem a forma:
∞
β(βx)α−1 −βx
R(t) = e dx
t Γ(α)
A função Tempo Médio para Falha, que pode se obtida pela Equação (13.9), tem a forma:
α
MTTB =
β
Casos especiais:
β(βt)n−1 −βt
f (t) = e
(n − 1)!
n = 1, 2, 3,. . .
n−1
(βt)k −βt
R(t) = e
k!
k=0
f (t)
Z(t) α αβtα−1 R(t)
α β α α−1 −βt
f (t) αe−αt αβtα−1 e−βt Γ(β) t e
α t βα
F (t) 1 − e−αt 1 − e−βt 0 Γ(α)
xα−1 e−βt dx
α ∞ βα
R(t) e−αt e−βt t Γ(α) x
α−1 −βt
e dx
1
1 Γ[ α +1] α
MTTF α 1 β
βα
Restrições α > 0, t > 0 α > 0, β > 0, t > 0 α > 0, β > 0, t > 0
13.3.1 Modelagem
Nesta subseção são apresentadas algumas formas de classificação dos modelos de confiabili-
dade de software. Cada autor adota um ponto de vista diferente para fazer sua classificação,
mas algumas concordâncias têm estreita relação entre si.
Schick e Wolverton [352] distinguem duas abordagens na modelagem de confiabilidade
de software:
Nesta subseção são apresentados os modelos que se baseiam na inserção de defeitos no soft-
ware para estimar sua confiabilidade. Essa abordagem, proposta inicialmente por Mills [284],
envolve implantar, em um dado programa, um certo número de defeitos. A suposição é que a
distribuição dos defeitos implantados é a mesma dos defeitos inerentes do programa. Assim,
338 Introdução ao Teste de Software ELSEVIER
nr
N̂ =
k
em que denota a função maior inteiro.
Basin [27] propõe a seguinte técnica: supõe que um programa consiste em M comandos,
dos quais n são aleatoriamente selecionados para se introduzirem defeitos. Se r comandos
são escolhidos ao acaso e testados, sendo k1 com defeitos inerentes e k2 com defeitos im-
plantados, então pode ser mostrado que o estimador de máxima verossimilhança do número
total N de defeitos no programa é dado por:
k1 (M − n + 1)
N̂ =
r − k2
e as probabilidades de cada classe são fixadas de acordo com o perfil de uso do programa.
Como exemplo, vamos supor que o domínio de entrada de um programa seja o conjunto dos
números inteiros positivos. Sabe-se antecipadamente que 25%, 35%, 30% e 10% são, respec-
tivamente, as porcentagens dos dados de entradas referentes aos intervalos [0 – 1500], [1501
– 2500], [2501 – 3500] e [3501 e mais ]. Assim, em uma amostra aleatória de 200 casos de
teste, 50, 70, 60 e 20 devem ser os números de casos de teste, respectivamentes selecionados,
para representar cada um dos intervalos. Ou seja, a distribuição de probabilidades de seleção
seria 0,25, 0,35, 0,30 e 0,1. A confiabilidade estimada para o programa será o número de exe-
cuções com sucesso sobre o valor 200. De uma forma geral, se N entradas são selecionadas
de acordo com o perfil operacional e S são as execuções com sucesso (sem falhas), então a
estimativa da confiabilidade do programa é dada por:
S
R̂ =
N
Nesta abordagem, vários pesquisadores propuseram variações na forma de se estimar a
confiabilidade.
Hecht [175] propôs os estimadores
S
R̂1 =
NL
e
S
R̂2 =
N LW
em que L é o número de instruções de máquina submetido e W é o número médio de instru-
ções por bits. Essa modificação normaliza o estimador pelo tamanho do programa e pelo tipo
de máquina utilizada.
Nelson [299] propôs um modelo no qual n entradas são aleatoriamente selecionadas do
domínio de entrada E = Ei , i = 1, 2, 3, . . . , N , sendo cada Ei o conjunto de dados ne-
cessários para se fazer uma execução do programa. A amostra aleatória das n entradas
é feita de acordo com a distribuição de probabilidades Pi . O conjunto das probabilidades
Pi ; i = 1, 2, 3, . . . , N é o perfil operacional do usuário. Se ne é o número de entradas cu-
jas execuções resultam em falhas, então um estimador não viciado para a confiabilidade do
software será:
ne
R̂1 = 1 −
n
Brown e Lipow [47] sugerem uma modificação na qual o espaço de entradas é dividido
em regiões homogêneas, Ei ; i = 1, 2, 3, . . . , k. A homogeneidade das regiões é no sentido de
geração de defeitos. Supõe-se que Nj execuções sejam efetuadas e Fj falhas sejam detectadas
para a região Ej . Assim, Fj /Nj é uma estimativa da probabilidade de falhas da região Ej .
De acordo com o perfil operacional, se P Ej é a probabilidade de seleção da região Ej , então
a probabilidade de falha do software é estimada por:
340 Introdução ao Teste de Software ELSEVIER
k
Fi
P Ei
i=1
N i
k
Fi
R̂ = 1 − P Ei
i=1
Ni
S Fi
M
R̂ = + Yi
N i=1
N
ai se Fi > 0
Yi =
0 se Fi = 0
Pode ser mostrado que essa estimativa é assintoticamente não viciada e sua variância
tende a zero quando N é grande. A dificuldade em se aplicar esse modelo é conhecer os M
tipos de defeitos do software e as probabilidades ai .
Nesta subseção são apresentados os modelos que se baseiam na ocorrência de falhas do soft-
ware ao longo do tempo para se estimar a confiabilidade.
A modelagem da confiabilidade baseada no domínio do tempo é a abordagem que tem
recebido maior ênfase na pesquisa. Essa abordagem utiliza o tempo de ocorrência entre
falhas ou o número de falhas ocorridos em um intervalo de tempo, para modelar o processo
de falhas no software. Em geral, os modelos podem ser utilizados para predizer o tempo
até a ocorrência da próxima falha ou o número esperado de falhas no próximo intervalo de
tempo. Originalmente, esses modelos foram baseados nos conceitos sobre confiabilidade
13.4. Principais modelos de confiabilidade 341
Essas classes, contudo, não são disjuntas. Existem modelos que aceitam qualquer um dos
dois tipos de dados. Além disso, os dados podem ser transformados de um tipo para outro,
adaptando-se a qualquer uma das classes de modelos.
De acordo com a classificação de Musa [293] existem dois tipos importantes de modelos
cuja categoria de falhas é finita: o tipo Poisson e o tipo Binomial.
Para os modelos do tipo Poisson, considera-se que o processo de falhas no software segue
um processo de Poisson no tempo. Assim, a variável aleatória M (t) representa o número de
falhas observadas no tempo t com um valor médio dado por μ(t). Ou seja, μ(t) = E[M (t)].
Considerando-se t0 = 0, t1 , t2 , . . . , ti−1 , ti , . . . , tn = t uma partição no intervalo [0, t],
tem-se um processo de Poisson se as variáveis fi , i = 1, 2, . . . , n (representando o número
de falhas detectadas no intervalo [ti−1 , ti ]) forem independentes com uma distribuição de
probabilidade Poisson cujo valor médio é dado por E[fi ] = μ(ti ) − μ(ti−1 ).
Assim, para cada variável aleatória fi , a função Densidade de Probabilidade é dada por:
Observa-se que se μ(t) é uma função linear do tempo, então tem-se um processo de
Poisson homogêneo (HPP), isto é, μ(t) = αt, α > 0. Se μ(t) não é uma função linear no
tempo, tem-se um processo de Poisson não homogêneo (NHPP).
Musa [292] demonstra as seguintes relações para modelos tipo Poisson:
a) Z(ti |ti−1 ) = λ(ti ), ou seja, a taxa de falhas do processo é igual à função de intensidade
de falhas no intervalo ti ,
b) μ(ti ) = αFa (ti)
c) λ(ti ) = μ (ti ) = αFa (ti )
d) R(ti |ti−1 ) = e− μ(ti )−μ(ti−1 )
em que α é uma constante que representa o número de defeitos detectados no software, fa (t)
e Fa (t) são, respectivamente, a função Densidade de Probabilidade e a função Distribuição
de Probabilidades acumuladas do tempo de falha de um defeito “a”, e R(t) é a função Con-
fiabilidade.
Para os modelos do tipo Binomial, as seguintes suposições são consideradas:
Observa-se a similaridade entre as funções λ(t) = αfa (t) e λ(t) = N fa (t) para os
modelos dos tipos Poisson e Binomial, respectivamente. O mesmo acontece para as funções
μ(t) = αFa (t) e μ(t) = N Fa (t).
O parâmetro N nos modelos tipo Binomial representa o número de defeitos no software
no início do teste, enquanto o parâmetro α no tipo Poisson representa o eventual número de
defeitos que podem ser descobertos em um tempo infinito de teste.
A Tabela 13.7 ilustra as relações derivadas para os modelos tipo Binomial e Poisson.
13.4. Principais modelos de confiabilidade 343
Cada um dos modelos que serão descritos nesta abordagem foram criados com base em
suposições específicas, mas existem algumas suposições padrão comuns à maioria dos mo-
delos, tais como:
Modelo de Weibull
Suposições do modelo:
2. todos os defeitos de uma classe de dificuldade têm chance idêntica de serem encontra-
dos;
3. as falhas, quando os defeitos são detectados, são independentes;
α
fa (t) = αβtα−1 e(−βt )
t
α
Fa (t) = fa (x)dx = 1 − e(−βt )
0
α
λ(t) = N fa (t) = N αβtα−1 e(−βt )
α
μ(t) = N Fa (t) = N [1 − e(−βt ) ]
13.4. Principais modelos de confiabilidade 345
Observa-se que limt→∞ μ(t) = N é o número de falhas que podem ser detectadas no
software.
A função Confiabilidade é obtida da função Distribuição de Probabilidade como:
α
R(t) = 1 − F (t) = e(−βt )
Modelo de Jelinski-Moranda
Desde que o modelo é do tipo Binomial, tem-se que a função Intensidade de Falhas λ(t)
e a função Valor Médio μ(t) são dadas por:
n
θ̂ = n n
N̂ i=1 Xi − i=1 (i − 1)Xi
n
1 n
=
N̂ − i + 1 N̂ − Pn 1 n
i=1
i=1 Xi i=1 (i − 1)Xi
A segunda equação deve ser resolvida por técnicas numéricas para se encontrar a estima-
tiva de N. Ao se substituir o valor na primeira equação, tem-se a estimativa de θ.
Modelo geométrico
O modelo geométrico foi proposto por Moranda [288] e é uma variação do modelo De-
Eutrophication de Jelinski-Moranda. Este é um modelo interessante porque, de modo di-
ferente dos modelos anteriormente discutidos, não assume um número fixo de defeitos no
software nem assume que as falhas tenham a mesma probabilidade de ocorrência. O modelo
assume que, com o progresso da depuração, os defeitos tornam-se mais difíceis de ser de-
tectados. O tempo entre falhas é considerado como tendo uma distribuição exponencial cuja
média decresce em uma forma geométrica.
13.4. Principais modelos de confiabilidade 347
2. todos os defeitos de uma classe de dificuldade têm chance idêntica de serem encontra-
dos;
Observa-se que, de acordo com a classificação de Musa [293], esse modelo é de categoria
de falhas infinita e da família geométrica.
Os dados necessários à aplicação deste modelo são:
a) os tempos de ocorrência das falhas, ti ; ou
b) os tempos entre as ocorrências de falhas, xi (xi = ti − ti−1 ).
Conforme a suposição 6, a função Densidade de Probabilidade do tempo entre a ocorrên-
cia de falhas é dada por:
i−1
f (xi ) = Dθi−1 e[−Dθ xi ]
A função Intensidade de Falhas λ(t) e a função Valor Médio μ(t) são dadas por:
Deβ
λ(t) =
[Dβeβ ]t + 1
1
μ(t) = ln[(Dβeβ )t + 1]
β
nθ̂
D̂ =
ˆ i xi
Σni=1 (θ)
ˆ i xi
Σni=1 i(θ) n+1
=
Σn (θ) ˆ i xi 2
i=1
1
M TˆBF = = Ê[Xn+1 ]
D̂(θ̂n )
A estimação de confiabilidade de software tem a sua importância por várias razões bem co-
nhecidas na literatura. Nesse sentido, vários são os modelos criados para a estimação de
confiabilidade de software. No entanto, todos os modelos propostos são formulados e fun-
damentados em uma abordagem de teste funcional, ou teste caixa preta, nos quais a maior
preocupação é a obtenção de uma forma funcional que explique o comportamento das fa-
lhas no software. Nenhum dos modelos até agora apresentados utiliza a informação sobre a
cobertura do código.
A utilização da análise de cobertura dos elementos requeridos de um critério de teste
estrutural tem a vantagem de que, no decorrer do teste, obtém-se a informação sobre o per-
centual do código exercitado durante o teste. Uma outra vantagem é a possibilidade de avaliar
o problema da superestimação da confiabilidade do software criado pelo efeito de saturação
do critério de teste.
Nesse contexto, os modelos de confiabilidade apresentados a seguir utilizam a informação
da cobertura do critério de teste como um parâmetro próprio do modelo, isto é, a informa-
ção da cobertura é utilizada diretamente na forma funcional do modelo de confiabilidade.
Os modelos de confiabilidade anteriormente apresentados baseiam-se no tempo de teste
do software. Nos modelos de confiabilidade baseados em cobertura supõe-se que a execução
de um dado de teste corresponde a uma unidade de tempo de execução do software. A
informação da cobertura obtida é diretamente utilizada no processo de modelagem da confia-
bilidade.
Malaya [259] faz essa mesma suposição quando cria um modelo que relaciona a co-
bertura do código com número de dados de teste para avaliar a confiabilidade do software.
Trata-se de um modelo que explica a cobertura em função dos dados de teste. Da mesma
forma, Chen [68] também faz essa suposição quando utiliza a informação da cobertura para
definir um fator a ser utilizado nos tradicionais modelos de confiabilidade com a finalidade
de corrigir a confiabilidade estimada por esses modelos. Chen et al. [70] também relatam o
desenvolvimento de um trabalho que envolve a relação entre cobertura e confiabilidade.
13.4. Principais modelos de confiabilidade 349
2. todos os defeitos de uma classe de dificuldade têm chance idêntica de serem encontra-
dos;
5. a cobertura dos elementos requeridos pelo critério de seleção utilizado na avaliação dos
dados é calculada à medida que os dados de teste são aplicados, a cada ocorrência de
falha;
6. a taxa de falhas condicional tem a seguinte forma funcional:
em que:
αi
−(ni )αi ]
R(ki |ni ) = e−[N −i][(ni +ki )
em que:
Nesse sentido, a utilização da cobertura do critério obtida no teste é uma informação adi-
cional que pode ser utilizada na estimação da confiabilidade do software. A cobertura do
teste estrutural e a confiabilidade de software estão estreitamente relacionadas [148, 97]. O
uso da cobertura de elementos requeridos, no estudo da confiabilidade, está apoiado na exis-
tência de uma forte correlação com a confiabilidade [402]. Pesquisas, tanto no campo teórico
como no experimental, comprovam a existência de alguma relação entre a confiabilidade e a
cobertura de elementos requeridos de um teste estrutural. A abordagem que utiliza a cober-
tura do código como informação relacionada à confiabilidade é uma alternativa consistente
à tradicional abordagem caixa preta de teste, já que esta não considera a estrutura do código
para a estimação de confiabilidade.
Como visto anteriormente, existem vários modelos de confiabilidade de software que podem
ser utilizados na tomada de decisões. Além da simples satisfação dos requisitos básicos para
a utilização de determinado modelo, não existe um critério que possa ser utilizado para a se-
leção de um modelo de confiabilidade de software antes da realização dos testes do software.
Contudo, a seleção de um modelo de confiabilidade pode seguir um procedimento geral,
compreendendo os passos descritos a seguir:
1. Teste do software: esta fase inicial não é trivial nem a mais rápida, pois trata da reali-
zação dos testes do software. Requer o planejamento dos testes, a realização dos testes
e o registro das falhas.
2. Coleta dos dados: nesta fase os resultados dos testes são coletados, anotados e armaze-
nados. Normalmente, são anotados dados como: o tempo entre a ocorrência das falhas,
o tempo acumulado de falhas, o número acumulado de falhas em um período de tempo
e a cobertura do teste.
3. Seleção do modelo de confiabilidade: nesta fase verificam-se os modelos que satisfa-
zem as condições em que os testes foram realizados e estimam-se os parâmetros dos
modelos.
354 Introdução ao Teste de Software ELSEVIER
Uma análise detalhada de ferramentas sobre confiabilidade de software pode ser vista na
publicação de Lyu [255].
Basicamente, existem duas situações importantes de tomadas de decisão que precisam ser
investigadas com a ajuda dos modelos de confiabilidade de software, isto é, situações em que
o uso dos modelos de confiabilidade de software é indispensável:
• Liberação do software
O software está pronto para ser liberado? O nível de confiabilidade do software atin-
gido nos testes já realizados pode ser um critério para a liberação do software.
No início do teste, ocorre um número significativo de falhas. A remoção dos defeitos
que provocaram essas falhas pode gerar um crescimento significativo da confiabilidade
do software. Após essa fase inicial de aumento substancial da confiabilidade, atinge-se
um patamar em que o aumento da confiabilidade do software ocorre de forma muito
lenta. O processo de remoção de defeitos prossegue até atingir um nível de confiabili-
dade desejado.
• Teste
Duas questões podem ser feitas em relação ao teste. Se o teste do software é baseado
no tempo: quanto tempo de teste ainda é necessário para se atingir a confiabilidade
desejada no software? Se o teste do software é baseado no número de dados de teste:
quantos dados de teste ainda são necessários para se atingir a confiabilidade desejada
no software?
O crescimento da confiabilidade considerada como função do tempo ou como função
do número de dados de teste indica que qualquer aumento desejado na confiabilidade
do software pode requerer um tempo de teste excessivamente longo ou um grande
número de dados de teste.
O momento de liberação do software deve ser tal que minimize a soma desses dois com-
ponentes do custo.
[1] A. Abdurazik e J. Offutt. Using uml collaboration diagrams for static checking and
test generation. In: 3rd International Conference on the Unified Modeling Language
– UML’00 / LNCS, volume 1939, p. 383-395, York, UK, out. 2000. Springer Ber-
lin/Heidelberg.
[2] A. T. Acree. On Mutation. Tese de doutoramento, Georgia Institute of Technology,
Atlanta, GA, EUA, ago. 1980.
[3] A. T. Acree, T. A. Budd, R. A. DeMillo, R. J. Lipton, e F. G. Sayward. Mutation
analysis. Technical Report GIT-ICS-79/08, Georgia Institute of Technology, Atlanta,
GA, set. 1979.
[12] R. T. Alexander, J. M. Bieman, S. Ghosh, e B. Ji. Mutation of Java objects. In: 13th
International Symposium on Software Reliability Engineering – ISSRE’2002, p. 341-
351, Annapolis, MD, EUA, nov. 2002. IEEE Computer Society Press.
[16] K. Araki, Z. Furukawa e J. Cheng. A general framework for debugging. IEEE Soft-
ware, 8(3):14-20, mai.1991.
[17] T. R. Arnold e W. A. Fuson. Testing “in a perfect world”. Communications of the
ACM, 37(9):78-86, set. 1994.
[18] M. Auguston. A program behavior model based on event grammar and its applica-
tion for debugging automation. In: 2nd International Workshop on Automated and
Algorithmic Debugging, p. 277-291, Saint-Malo, França, mai.1995.
[19] T. M. Austin, S. E. Breach e G. S. Sohi. Efficient detection of all pointer and array
access errors. ACM SIGPLAN Notes, 29(6):290-301, jun. 1994.
[20] D. Baldwin e F. Sayward. Heuristics for determining equivalence of program muta-
tions. Research Report 276, Department of Computer Science, Yale University, New
Haven, CT, EUA, 1979.
[21] R. M. Balzer. Exdams: Extensible debugging and monitoring system. In: Spring Joint
Computer Conference, p. 567-589, Reston, VA, EUA, 1969. AFIPS Press.
[22] S. Barbey e A. Strohmeier. The problematics of testing object-oriented software. In:
2nd Conference on Software Quality Management – SQM’94, volume 2, p. 411-426,
jul. 1994.
[23] F. Barbier, N. Belloir e J.-M. Bruel. Incorporation of test functionality into software
components. In: 2nd International Conference on COTS-Based Software Systems,
volume 2580 de Lecture Notes in Computer Science, p. 25-35, Londres, UK, fev. 2003.
Springer-Verlag.
[27] S. L. Basin. Estimation of software error rates via capture-recapture sampling. Tech-
nical report, Science Applications, Inc., Palo Alto, CA, EUA, set. 1973.
[28] K. Beck e E. Gamma. JUnit cookbook. Página WEB, 2006. Disponível em: http:
//junit.sourceforge.net/.
[29] O. Beckman e B. Grupta. Developing test cases from use cases for web applications.
In: International Conference on Practical Software Testing Techniques – PSTT’2002,
Nova Orleans, 2002.
[30] A. L. Beguelin. Xab: A tool for monitoring pvm programs. In: 26th Hawaii Interna-
tional Conference on System Sciences, volume 2, p. 102-103. IEEE Press, jan. 1993.
[31] B. Beizer. Software Testing Techniques. Van Nostrand Reinhold Company, Nova York,
NY, EUA, 2. ed., 1990.
[32] S. Beydeda e V. Gruhn. An integrated testing technique for component-based soft-
ware. In: 1st AICCSA ACS/IEEE International Conference on Computer Systemsand
Applications, p. 328-334, Beirute, Líbano, jun. 2001. IEEE Computer Society Press.
[33] S. Beydeda e V. Gruhn. State of the art in testing components. In: Third International
Conference on Quality Software – QSIC’03, p. 146-153, Washington, DC, EUA, 2003.
IEEE Computer Society.
[35] R. V. Binder. Testing Object-Oriented Systems: Models, Patterns, and Tools, volume 1.
Addison Wesley Longman, Inc., 1999.
[36] A.S. Binns e G. McGraw. Building a Java software engineering tool for testing applets.
In: IntraNet 96 NY Conference, Nova York, NY, EUA, abr. 1996.
[41] B. Boothe. Efficient algorithms for bidirectional debugging. In: PLDI ’00: Procee-
dings of the ACM SIGPLAN 2000 conference on Programming language design and
implementation, p. 299-310, Nova York, NY, EUA, jun. 2000. ACM Press.
362 Introdução ao Teste de Software ELSEVIER
[42] L. Bottaci. A genetic algorithm fitness function for mutation testing. In: Seminal: Soft-
ware Engineering Using Metaheuristic Innovative Algorithms – Meeting 7. IEEE Inter-
national Conference on Software Engineering, Toronto, Canadá, mai.2001. Disponível
em: http://www.dcs.kcl.ac.uk/projects/seminal/pastmeeting/
(007)(12,13)-5-2001/bottaci.ps.
[44] R. S. Boyer, B. Elspas e K. N. Levitt. Select – a formal system for testing and de-
bugging programs by symbolic execution. In: International Conference on Reliable
software, p. 234-245, Nova York, NY, EUA, 1975. ACM Press.
[47] J. Brown e M. Lipow. Testing for software reliability. In: Proceedings of the Interna-
tional Conference on Reliable Software, 1975.
[48] J. M. Bruel, J. Araújo, A. Moreira e A. Royer. Using aspects to develop built-in tests
for components. In: The 4th AOSD Modeling With UML Workshop, São Francisco,
CA, EUA, out. 2003.
[49] T. A. Budd. Mutation Analysis: Ideas, Example, Problems and Propects, chapter
Computer Program Testing. North-Holland Publishing Company, 1981.
[50] T. A. Budd e D. Angluin. Two notions of correctness and their relation to testing. Acta
Informatica, 18(1):31-45, nov. 1982.
[51] T. A. Budd, R. A. DeMillo, R. J. Lipton e F. G. Sayward. Theoretical and empirical
studies on using program mutation to test the functional correctness of programs. In:
7th ACM Symposium on Principles of Programming Languages, p. 220-233, Nova
York, NY, EUA, jan. 1980.
[52] S. Budkowski e P. Dembinski. An introduction to Estelle: a specification language for
distributed systems. Computer Network and ISDN Systems, 14(1):3-23, 1987.
[53] P. M. S. Bueno e M. Jino. Automated test data generaton for program paths using
genetic algorithms. In: 13th International Conference on Software Engineering &
Knowledge Engineering – SEKE’2001, p. 2-9, Buenos Aires, Argentina, jun. 2001.
[56] M. Bybro. A mutation testing tool for Java programs. Dissertação de mestrado,
Stockholm University, Estocolmo, Suécia, ago. 2003.
[57] R. Calkin, R. Hempel, H.-C. Hoppe e P. Wypior. Portable programming with the
parmacs message-passing library. Parallel Computing, 20(4):615-632, 1994.
[58] L. F. Capretz. A brief history of the object-oriented approach. SIGSOFT Softw. Eng.
Notes, 28(2):6, mar. 2003.
[59] R. H. Carver e K.-C. Tai. Replay and testing for concurrent programs. IEEE Software,
8(2):66-74, mar. 1991.
[60] M. Ceccato, P. Tonella e F. Ricca. Is aop code easier or harder to test than OOP
code? In: Fourth International Conference on Aspect-Oriented Software Development
(AOSD’2005) – Workshop On Testing Aspect Oriented Programs, Chicago, Illinois,
EUA, mar. 2005.
[68] M.-H. Chen. Tools and techniques for testing based software reliability estimation.
Tese de doutoramento, Purdue University, West Lafayette, IN, EUA, 1994.
[70] M.-H. Chen, M. R. Lyu e W. E. Wong. An empirical study of the correlation between
code coverage and reliability estimation. In: METRICS ’96: Proceedings of the 3rd
International Symposium on Software Metrics, p. 133-141, Washington, DC, EUA,
mar. 1996. IEEE Computer Society.
[71] M.-H. Chen, M. R. Lyu e W. E. Wong. Effect of code coverage on software reliability
measurement. IEEE Transactions on Reliability, 50(2):165-170, jun. 2001.
[72] M.-H. Chen, A. P. Mathur e V. J. Rego. Effect of testing technique on software reliabi-
lity estimates obtained using a time-domain model. IEEE Transactions on Reliability,
44(1):97-103, mar. 1995.
[73] S.-K. Chen, W. K. Fuchs e J.-Y. Chung. Reversible debugging using program instru-
mentation. IEEE Transactions on Software Engineering, 27(8):715-727, ago. 2001.
[74] T. Y. Chen e Y. Y. Cheung. On program dicing. Journal of Software Maintenance,
9(1):33-46, 1997.
[75] P. Chevalley. Applying mutation analysis for object-oriented programs using a reflec-
tive approach. In: 8th Asia-Pacific Software Engineering Conference – APSEC’01, p.
267-272, Macau, China, dez. 2001. IEEE Computer Society Press.
[76] B. J. Choi, R. A. DeMillo, E. W. Krauser, R. J. Martin, A. P. Mathur, A. J. Offutt,
H. Pan e E. H. Spafford. The mothra toolset. In: Proceedings of the 22nd Annual
Hawaii International Conference on Systems Sciences, p. 275-284, Koa, Havaí, jan.
1989.
[77] B. J. Choi, A. P. Mathur e A. P. Pattison. pmothra: Scheduling mutants for execution
on a hypercube. In: 3rd Symposium on Software Testing, Analysis and Verification, p.
58-65, Key West, FL, dez. 1989. ACM Press.
[78] C. S. Chou e M. W. Du. Improved domain strategies for detecting path selection errors.
In: International Conference on Software Maintenance, p. 165-173, Los Angeles, CA,
EUA, set. 1987.
[79] T. S. Chow. Testing software design modeled by finite-state machines. IEEE Transac-
tions on Software Engineering, 4(3):178-187, mai.1978.
[80] I. S. Chung. Automatic testing generation for mutation testing using genetic operators.
In: International Conference on Software Engineering and Knowlede Engineering.
São Francisco, CA, EUA, jun. 1998.
[81] R. D. Yang C. G. Chung. Path analysis testing of concurrent programs. Information
and Software Technology, 34(1):43-56, jan. 1992.
[82] T. Chusho. Test data selection and quality estimation based on the concept of essential
branches for path testing. IEEE Transactions on Software Engineering, 13(5):509-517,
mai.1987.
[83] L. Clarke. A system to generate test data and symbolically execute programs. IEEE
Transactions on Software Engineering, 2(3):215-222, set. 1976.
[84] L. A. Clarke, J. Hassell e D. J. Richardson. A close look at domain testing. IEEE
Transactions on Software Engineering, 8(4):380-390, jul. 1982.
Referências Bibliográficas 365
[101] I. D.D. Curcio. ASAP – a simple assertion pre-processor. ACM SIGPLAN Notices,
33(12):44-51, dez. 1998.
[102] A. R. C. da Rocha, J. C. Maldonado e K. C. Weber. Qualidade de Software: Teoria e
Prática. Prentice Hall, São Paulo, SP, Brasil, 2001.
[103] S. K. Damodaran-Kamal e J. M. Francioni. Nondeterminacy: Testing and debugging
in message passing parallel programs. In: III ACM/ONR Workshop on Parallel and
Distributed Debugging, p. 118-128. ACM Press, Nova York, NY, EUA, 1993.
[104] C. Darwin. On the Origin of Species by Means of Natural Selection or thePreservation
of Favoured Races in the Struggle for Life. 1859. Disponível em: http://etext.
virginia.edu/toc/modeng/public/DarOrig.html.
[105] A. M. Davis. A comparison of techniques for the specification of external system
behavior. Communications of the ACM, 31(9):1.098-1.115, set. 1988.
[106] J. de S. Coutinho. Software reliability growth. In: IEEE Symposium on Computer
Software Reliability, 1973.
[107] M. E. Delamaro. Proteum: Um ambiente de teste baseado na análise de mutantes.
Dissertação de mestrado, ICMC/USP, São Carlos, SP, Brasil, out. 1993.
[108] M. E. Delamaro. Mutação de Interface: Um Critério de Adequação Interprocedimen-
tal para o Teste de Integração. Tese de doutoramento, IFSC/USP, São Carlos, SP,
Brasil, jun. 1997.
[109] M. E. Delamaro, J. C. Maldonado, M. Jino e M. L. Chaim. Proteum: Uma ferramenta
de teste baseada na análise de mutantes. In: Caderno de Ferramentas do VII Simpósio
Brasileiro de Engenharia de Software, p. 31-33, Rio de Janeiro, RJ, Brasil, out. 1993.
[110] M. E. Delamaro, J. C. Maldonado e A. P. Mathur. Interface mutation: An approach for
integration testing. IEEE Transactions on Software Engineering, 27(3):228-247, mar.
2001.
[111] M. E. Delamaro, M. Pezzè, A. M. R. Vincenzi e J. C. Maldonado. Mutant operators
for testing concurrent Java programs. In: XV Simpósio Brasileiro de Engenharia de
Software – SBES’2001, p. 272-285, Rio de Janeiro, RJ, Brasil, out. 2001.
[112] M. E. Delamaro e A. M. R. Vincenzi. Structural testing of mobile agents. In: Egi-
dio Astesiano Nicolas Guelfi and Gianna Reggio, editors, III International Workshop
on Scientific Engineering of Java Distributed Applications (FIDJI’2003), Lecture No-
tes on Computer Science, p. 73-85, Springer, nov. 2003.
[113] M. E. Delamaro, A. M. R. Vincenzi e J. C. Maldonado. A strategy to perform cove-
rage testing of mobile applications. In: Workshop on Automation of Software Test –
AST’2006, p. 118-124, Xangai, China, mai.2006. ACM Press.
[114] R. A. DeMillo, D. C. Gwind e K. N. King. An extended overview of the Mothra
software testing environment. In: II Workshop on Software Testing, Verification and
Analysis, p. 142-151. Computer Science Press, Banff, Canadá, jul. 1988.
[115] R. A. DeMillo, R. J. Lipton e F. G. Sayward. Hints on test data selection: Help for the
practicing programmer. IEEE Computer, 11(4):34-41, abr. 1978.
Referências Bibliográficas 367
[129] M. C. F. P. Emer, S. R. Vergilio e M. Jino. A testing approach for xml schemas. In:
XXIX Annual International Computer Software and Applications Conference, COMP-
SAC 2005 – QATWBA 2005, volume 2, p. 57-62. IEEE Press, jul. 2005.
368 Introdução ao Teste de Software ELSEVIER
[137] L.P. Ferreira e S.R. Vergilio. Tdsgen: An environment based on hybrid genetic al-
gorithms for generation of test data. In: 17th International Conference on Software
Engineering and Knowledge Engineering, volume 3103/2004, p. 1.431-1.432, Sprin-
ger, 2005.
[138] R. Filman e D. Friedman. Aspect-oriented programming is quantification and oblivi-
ousness. In: Workshop on Advanced Separation of Concerns – OOPSLA’2000, Min-
neapolis, MN, EUA, out. 2000.
[139] J. Flower e A. Kolawa. Express is not just a message passing system. current and
future directions in express. Parallel Computing, 20(4):597-614, abr. 1994.
[141] F. G. Frankl. The Use of Data Flow Information for the Selection and Evaluation of
Software Test Data. Tese de doutoramento, Nova York University, Nova York, NY,
EUA, out. 1987.
Referências Bibliográficas 369
[142] F. G. Frankl e E. J. Weyuker. A data flow testing tool. In: II Conference on Software
development tools, techniques, and alternatives, p. 46-53, Los Alamitos, CA, EUA,
dez. 1985. IEEE Computer Society Press.
[143] F. G. Frankl e E. J. Weyuker. Data flow testing in the presence of unexecutable paths.
In: Workshop on Software Testing, p. 4-13, Banff, Canadá, jul. 1986.
[144] P. G. Frankl e E. J. Weyuker. An applicable family of data flow testing criteria. IEEE
Transactions on Software Engineering, 14(10):1.483-1.498, out. 1988.
[145] P. G. Frankl e E. J. Weyuker. A formal analysis of the fault-detecting ability of testing
methods. IEEE Transactions on Software Engineering, 19(3):202-213, mar. 1993.
[147] C. Gane e T. Sarson. Análise estruturada de sistemas. LTC Editora, Rio de Janeiro,
RJ, Brasil, 1983.
[148] P. Garg. Investigating coverage-reliability relationship and sensitivity of reliability to
errors in the operational profile. In: I International Conference on Software Testing,
Reliability and Quality Assurance, p. 21-35. IBM Press, dez. 1994.
[152] A. Gill. Introduction to the Theory of Finite-State Machine. McGraw-Hill, Nova York,
NY, EUA, 1962.
[154] A. L. Goel e K. Okumoto. A time dependent error detection rate model for soft-
ware reliability and other performance measures. IEEE Transactions on Reliability,
28(3):206-211, ago. 1979.
[155] Al. L. Goel. Software reliability models: Assumptions, limitations and applicability.
IEEE Transactions on Software Engineering, 11(12):1.411-1.423, dez. 1985.
370 Introdução ao Teste de Software ELSEVIER
[156] M. Golan e D. Hanson. DUEL – A very high-level debugging language. In: Win-
ter USENIX Technical Conference, San Diego, CA, EUA, jan. 1993. Disponível em:
http://www273.pair.com/drh/documents/duel.pdf.
[157] N. Gupta, H. He, X. Zhang e R. Gupta. Locating faulty code using failure-inducing
chops. In: ASE ’05: Proceedings of the 20th IEEE/ACM international Conference
on Automated software engineering, p. 263-272, Nova York, NY, USA, 2005. ACM
Press.
[158] T. Gyimóthy, Á. Beszédes e I. Forgács. An efficient relevant slicing method for debug-
ging. In: VII European software engineering conference – ESEC/FSE-7, p. 303-321,
Londres, UK, 1999. Springer-Verlag.
[159] G. Gönenc. A method for the design of fault-detection experiments. IEEE Transactions
on Computers, 19(6):551-558, jun. 1970.
[161] D. Hamlet e R. Taylor. Partition testing does not inspire confidence (program testing).
IEEE Transactions on Software Engineering, 16(12):1.402-1.411, 1990.
[162] D. Harel. Statecharts: A visual formalism for complex systems. Science of Computer
Programming, 8(3):231-274, jun. 1987.
[163] D. Harel. Statecharts: On the formal semantics of statecharts. In: II IEEE Symposium
on Logic in Computer Science, p. 54-64, Ithaca, NY, EUA, 1987. IEEE Press.
[169] M. J. Harrold e G. Rothermel. Performing data flow testing on classes. In: II ACM SIG-
SOFT Symposium on Foundations of Software Engineering, p. 154-163, Nova York,
NY, EUA, dez. 1994. ACM Press.
Referências Bibliográficas 371
[170] M. J. Harrold e M. L. Soffa. Interprocedural data flow testing. In: III Symposium
on Software testing, analysis, and verification, p. 158-167, Key West, FL, EUA, dez.
1989. ACM Press.
[171] M. J. Harrold e M. L. Soffa. Selecting and using data for integration testing. IEEE
Software, 8(2):58-65, mar. 1991.
[172] J. Hartmann e D. J. Robson. Techniques for selective revalidation. IEEE Software,
7(1):31-36, jan. 1990.
[173] R. Hastings e B. Joyce. Purify: fast detection of memory leaks and access errors. In:
Proceedings of the Winter Usenix Conference, p. 125-136, 1992.
[178] P. M. Herman. A data flow analysis approach to program testing. Australian Computer
Journal, 8(3):92-96, nov. 1976.
[182] R. M. Hierons, M. Harman e S. Danicic. Using program slicing to assist in the de-
tection of equivalent mutants. Software Testing, Verification and Reliability, 9(4):233-
262, 1999.
[183] E. Hilsdale e J. Hugunin. Advice weaving in AspectJ. In: III International conference
on Aspect-oriented software development – AOSD’04, p. 26-35, Nova York, NY, EUA,
2004. ACM Press.
[184] D. Hoffman e P. Strooper. A case study in class testing. In: Conference of the Centre
for Advanced Studies on Collaborative research – CASCON’93, p. 472-482, Toronto,
Ontario, Canadá, out. 1993. IBM Press.
[185] D. Hoffman e P. Strooper. Classbench: a framework for automated class testing. Soft-
ware Practice and Experience, 27(5):573-597, mai.1997.
372 Introdução ao Teste de Software ELSEVIER
[186] W. E. Howden. Methodology for the generation of program test data. IEEE Computer,
24(5):554-559, mai.1975.
[187] W. E. Howden. Reliability of the path analysis testing strategy. IEEE Transactions on
Software Engineering, 2(3):208-215, set. 1976.
[188] W. E. Howden. Symbolic testing and DISSECT symbolic evaluation system. IEEE
Transactions on Software Engineering, 3(4):266-278, jul. 1977.
[189] W. E. Howden. Theoretical and empirical studies of program testing. IEEE Transac-
tions on Software Engineering, 4(4):293-298, jul. 1978.
[190] W. E. Howden. Weak mutation testing and completeness of test sets. IEEE Transac-
tions on Software Engineering, 8(4):371-379, jul. 1982.
[191] W. E. Howden. Functional Program Testing and Analysis. McGrall-Hill, Nova York,
NY, EUA, 1987.
[193] A. Hudson. Program errors as a birth and death process. Technical Report SP-3011,
Systems Development Corporation, Santa Monica, CA, EUA, dez. 1967.
[195] IBM. Rational PurifyPlus. Página WEB, 2005. Disponível em: http://www-306.
ibm.com/software/awdtools/purifyplus/.
[196] IEEE. IEEE standard glossary of software engineering terminology. Standard 610.12-
1990 (R2002), IEEE Computer Society Press, 2002.
[197] W. Isberg. Get test-inoculated! Artigo On-Line, abr. 2002. Disponível em: http:
//www.ddj.com/dept/architect/184414846.
[198] D. Jackson e M. Woodward. Parallel firm mutation of Java programs. In: Mutation
Testing for the New Century, p. 55-61, Norwell, MA, EUA, out. 2000. Kluwer Acade-
mic Publishers.
[218] A. Ko e B. Myers. Designing the whyline: a debugging interface for asking questions
about program behavior. In: The 2004 Conference on Human factors in Computing
Systems (SIGCHI), p. 151-158, Nova York, NY, EUA, abr. 2004. ACM Press.
[222] B. Korel e J. W. Laski. A tool for data flow oriented program testing. In: II conference
on Software development tools, techniques, and alternatives, p. 34-37, Los Alamitos,
CA, EUA, dez. 1985. IEEE Computer Society Press.
[223] B. Korel e J. Rilling. Application of dynamic slicing in program debugging. In: III
Workshop on Automated and Algorithmic Debugging, p. 43-58, Linköping, Suécia,
mai.1997. Linköping Electronic Articles in Computer and Information Science.
[224] B. Korel e J. Rilling. Program slicing in understanding of large programs. In: VI Inter-
national Workshop on Program Comprehension - IWPC’98:, p. 145-152, Washington,
DC, EUA, jun. 1998. IEEE Computer Society.
[225] B. Korel e S. Yalamanchili. Forward computation of dynamic program slices. In: 1994
ACM SIGSOFT international symposium on Software testing and analysis – ISSTA’94,
p. 66-79, Nova York, NY, EUA, ago. 1994. ACM Press.
[230] D. C. Kung, C.-H. Liu e P. Hsia. An object-oriented web test model for testing web
applications. In: XXIV International Computer Software and Applications Conference
– COMPSAC’00, p. 537-542, Washington, DC, EUA, out. 2000. IEEE Computer So-
ciety.
[231] J. W. Laski e B. Korel. A data flow oriented program testing strategy. IEEE Transac-
tions on Software Engineering, 9(3):347-354, mai.1983.
Referências Bibliográficas 375
[232] Y. Le Traon, T. Jéron, J.-M. Jézéquel e P. Morel. Efficient OO integration and regres-
sion testing. IEEE Transactions on Reliability, 49(1):12-25, mar. 2000.
[233] S. C. L. Lee e J. Offutt. Generating test cases for xml-based web component interacti-
ons using mutation analysis. In: XII International Symposium on Software Reliability
Engineering – ISSRE’01, p. 200, Washington, DC, EUA, nov. 2001. IEEE Computer
Society.
[240] H. Lieberman. The debugging scandal and what to do about it. Communications of
the ACM, 40(4):26-29, abr. 1997.
[241] H. Lieberman e C. Fry. Software Visualization, chapter ZStep 95: A Reversible, Ani-
mated Source Code Stepper, p. 277-292. MIT Press, 1998.
[245] B. Littlewood e J. L. Verral. A bayesian reliability growth model for computer soft-
ware. Applied Statistics, 22(3):332-346, 1973.
376 Introdução ao Teste de Software ELSEVIER
[246] C. Liu e D.J. Richardson. Software components with retrospectors. In: Internatio-
nal Workshop on the Role of Software Architecture in Testing and Analysis, Marsala,
Sicília, Itália, jul. 1998.
[247] C. H. Liu, D. C. Kung e P. Hsia. Object-based data flow testing of web applications.
In: I Asia-Pacific Conference on Quality Software, p. 7-16. IEEE Press, out. 2000.
[248] C.H. Liu, D.C. Kung, P. Hsia e C.T. Hsu. Structural testing of web applications. In: XI
International Symposium on Software Reliability Engineering, p. 84-96. IEEE Press,
out. 2000.
[249] B. Long, D. Hoffman e P. Strooper. Tool support for testing concurrent java compo-
nents. IEEE Transactions on Software Engineering, 29(6):555-566, jun. 2003.
[250] W. S. Lopes. Estelle: uma técnica para a descrição formal de serviços e protocolos de
comunicação. Revista Brasileira de Computação, 5(1):33-44, set. 1989.
[256] Y. Ma, A. J. Offutt e Y. Kwon. MuJava: An automated class mutation system. The
Journal of Software Testing, Verification, and Reliability, 15(2):97-133, 2005.
[257] Y.-S. Ma, Y.-R. Kwon e J. Offutt. Inter-class mutation operators for Java. In: XIII
International Symposium on Software Reliability Engineering- ISSRE’2002, p. 352-
366, Washington, DC, EUA, nov. 2002. IEEE Computer Society.
[258] N. Li Y. K. Malaiya. On input profile selection for software testing. In: V International
Symposium on Software Reliability Engineering – ISSRE’94, p. 196-205, nov. 1994.
[266] N. Malevris, D. F. Yates e A. Veevers. Predictive metric for likely feasibility of pro-
gram paths. Journal of Electronic Materials, 19(6):115-118, jun. 1990.
[267] Man Machine Systems. Java code coverage analyzer – JCover. Página WEB, 2002.
Disponível em: http://www.mmsindia.com/JCover.html.
[272] A. P. Mathur. Performance, effectiveness and reliability issues in software testing. In:
15th Annual International Computer Software and Applications Conference, p. 604-
605, Tóquio, Japão, set. 1991. IEEE Computer Society Press.
[275] A. P. Mathur e W. E. Wong. An empirical comparison of data flow and mutation based
test adequacy criteria. Software Testing, Verification and Reliability, 4(1):9-31, mar.
1994.
[276] T. J. McCabe. A complexity measure. IEEE Transactions on Software Engineering,
2(6):308-320, dez. 1976.
[277] R. McDaniel e J. D. McGregor. Testing polymorphic interactions between classes.
Technical Report TR-94-103, Clemson University, mar. 1994.
[278] J. D. McGregor. Functional testing of classes. In: Proc. 7th International Quality
Week, São Francisco, CA, mai. 1994. Software Research Institute.
[279] J. D. McGregor e D. A. Sykes. A Practical Guide to Testing Object-Oriented Software.
Addison-Wesley, 2001.
[280] P. Mellor. Software reliability modelling: the state of the art. Information and Software
Technology, 29(2):81-98, mar./abr 1987.
[281] B. Meyer. Applying design by contract. Computer, 25(10):40-51, out. 1992.
[282] C. C. Michael, G. McGraw e M. A. Schatz. Generating software test data by evolution.
IEEE Transactions on Software Engineering, 27(12):1.085-1.110, dez. 2001.
[283] Microsoft Corporation. COM: Delivering on the promises of component technology.
Página WEB, 2002. Disponível em: http://www.computer-society.com/
Redirect.php?id_url=532.
[284] H. D. Mills. On the statistical validation of computer programs. Relatório Técnico
FSC-72-6015, IBM Federal Systems Division, Gaithersburg, MD, 1972.
[285] S. Monk e S. Hall. Virtual mock objects using AspectJ with JUNIT. Artigo On-Line,
out. 2002. XProgramming.com. Disponível em: http://xprogramming.com/
xpmag/virtualMockObjects.htm.
[286] A. Mood, F. Graybill e D. Boes. Introduction to the Theory of Statistics. Probability
and Statistics. McGraw-Hill, 3. ed., abr. 1974.
[287] S. Moore, D. Cronk, K. S. London e J. Dongarra. Review of performance analysis tools
for mpi parallel programs. In: VIII European PVM/MPI Users’ Group Meeting on
Recent Advances in Parallel Virtual Machine and Message Passing Interface, volume
2131 of Lecture Notes In Computer Science, p. 241-248, Londres, UK, set. 2001.
Springer-Verlag.
[288] P. B. Moranda. Predictions of software reliability during debugging. In: Annual Reli-
ability and Maintainability Symposium, p. 327-332, Washington, DC, EUA, 1975.
[289] P. L. Moranda e Z. Jelinski. Final report on software reliability study. Technical Report
63921, McDonnell Douglas Astronautics Company, 1972.
[290] L. J. Morell. A theory of fault-based testing. IEEE Transactions on Software En-
gineering, 16(8):844-857, ago. 1990.
[291] G. C. Murphy, P. Townsend e P. S. Wong. Experiences with cluster and class testing.
Communications of the ACM, 37(9):39-47, set. 1994.
Referências Bibliográficas 379
[307] A. J. Offutt e J. H. Hayes. A semantic model of program faults. In: 1996 ACM
SIGSOFT International Symposium on Software Testing and Analysis – ISSTA’96, p.
195-200, Nova York, NY, EUA, jan. 1996. ACM Press.
[309] A. J. Offutt e K. N. King. A Fortran 77 interpreter for mutation analysis. In: Papers of
the Symposium on Interpreters and interpretive techniques – SIGPLAN’87, p. 177-188,
Nova York, NY, EUA, jun. 1987. ACM Press.
[315] A. J. Offutt e W. Xu. Generating test cases for web services using data perturbation.
ACM SIGSOFT Software Engineering Notes, 29(5):1-10, set. 2004.
[316] A. J. Offutt, R. Alexander, Y. Wu, Q. Xiao e C. Hutchinson. A fault model for subtype
inheritance and polymorphism. In: 12th International Symposium on Software Reliabi-
lity Engineering – ISSRE’01, p. 84-93, Hong Kong, China, nov. 2001. IEEE Computer
Society Press.
[317] A. Orso, M. J. Harrold, D. Rosenblum, G. Rothermel, H. Do e M. L. Soffa. Using
component metacontent to support the regression testing of component-based soft-
ware. In: IEEE International Conference on Software Maintenance - ICSM’01, p.
716, Washington, DC, EUA, nov. 2001. IEEE Computer Society.
[320] T. J. Ostrand e E. J. Weyuker. Using data flow analysis for regression testing. In:
VI Annual Pacific Northwest Software Quality Conference, p. 233-247, Portland, OR,
EUA, set. 1988.
Referências Bibliográficas 381
[321] H. Pan. Software Debugging with Dynamic Instrumentation and Test-Based Kno-
wledge. Tese de doutoramento, Purdue University, West Lafayette, IN, EUA, ago.
1993.
[322] H. Pan e E. H. Spafford. Toward automatic localization of software faults. In: X Pacific
Northwest Software Quality Conference, p. 192-209, Portland, OR, EUA, out. 1992.
[323] PARASOFT Corporation. C++ Test. Página WEB, 2000. Disponível em: http:
//www.parasoft.com/.
[324] PARASOFT Corporation. Insure++. Página WEB, 2000. Disponível em: http:
//www.parasoft.com/.
[333] Quest Software. JProbe Suite. Página WEB, 2003. Disponível em: http://www.
quest.com/jprobe/.
[337] S. Rapps e E. J. Weyuker. Selecting software test data using data flow information.
IEEE Transactions on Software Engineering, 11(4):367-375, abr. 1985.
382 Introdução ao Teste de Software ELSEVIER
[353] E. Y. Shapiro. Algorithmic Program DeBugging. MIT Press, Cambridge, MA, EUA,
1983.
[354] T. Shimomura. Critical slice-based fault localization for any type of error. IEICE
Transactions on Information and Systems, E76-D(6):656-667, jun. 1993.
Referências Bibliográficas 383
[371] A. L. Souter, L. L. Pollock e D. Hisley. Inter-class def-use analysis with partial class
representations. In: PASTE’99: Proceedings of the 1999 ACM SIGPLAN-SIGSOFT
Workshop on Program Analysis for Software Tools and Engineering, p. 47-56, Nova
York, NY, EUA, 1999. ACM Press.
[379] R. M. Stallman, R. H. Pesch e S. Shebs. Debugging with GDB: The GNU Source-Level
Debugger. Free Software Foundation, Cambridge, MA, EUA, 9. ed., fev. 2002.
[382] K.-C. Tai. Predicate-based test generation for computer programs. In: XV International
Conference on Software Engineering - ICSE’93, p. 267-276, Los Alamitos, CA, EUA,
mai.1993. IEEE Computer Society Press.
[383] K.-C. Tai, R. H. Carver e E. E. Obaid. Debugging concurrent Ada programs by de-
terministic execution. IEEE Transactions on Software Engineering, 17(1):45-63, jan.
1991.
[384] K.-C. Tai e F. J. Daniels. Interclass test order for object-oriented software. Journal of
Object-Oriented Programming, 12(4):18-25, 1999.
[385] A. S. Tanenbaum. Modern Operating Systems. Prentice Hall, 2. ed., 2001.
[386] R. E. Tarjan. Depth-first search and linear graph algorithms. SIAM Journal on Com-
puting, 1(2):146-160, 1972.
[387] M. Tatsubori, S. Chiba, M.-O. Killijian e K. Itano. OpenJava: A Class-Based Macro
System for Java. In: Reflection and Software Engineering, volume 1826 de Lecture
Notes in Computer Science, p. 117-133, Heidelberg, Alemanha, jun. 2000. Springer-
Verlag.
[388] R. N. Taylor, D. L. Levine e C. Kelly. Structural testing of concurrent programs. IEEE
Transactions on Software Engineering, 18(3):206-215, mar. 1992.
[389] Telcordia Technologies. xSuds Toolsuite. Página WEB, 1998. Disponível em: http:
//xsuds.argreenhouse.com/.
[390] K. Templer e C. Jeffery. A configurable automatic instrumentation tool for ansi c. In:
XIII IEEE international conference on Automated software engineering – ASE’98, p.
249-258, Washington, DC, EUA, out. 1998. IEEE Computer Society.
[391] The AspectJ Team. The AspectJ programming guide, fev. 2003. Xerox Corpo-
ration. Disponível em: http://dev.eclipse.org/viewcvs/indextech.
cgi/~checkout~/aspectj-home/doc/progguide/index.html.
[392] H. S. Thompson, D. Beech, M. Maloney e N. Mendelsohn. XML Schema part 1:
Structures second edition. Página WEB, out. 2004. W3C – World Wide Web Consor-
tium. Disponível em: http://www.w3.org/TR/xmlschema-1/.
[394] Jeff Tian. Integrating time domain and input domain analyses of software reliability
using tree-based models. IEEE Transactions on Software Engineering, 21(12):945-
958, dez. 1995.
[400] H. Ural e B. Yang. A structural test selection criterion. Information Processing Letters,
28(3):157-163, jul. 1988.
[424] Y. Wang, G. King e H. Wickburg. A method for built-in tests in component-based soft-
ware maintenance. In: III European Conference on Software Maintenance and Reen-
gineering – CSMR’99, p. 186-189, Washington, DC, EUA, mar. 1999. IEEE Computer
Society.
[425] J. Wegener, A. Baresel e H. Sthamer. Evolutionary test environment for automatic
structural testing. Information and Software Technology, 43(14):841-854, dez. 2001.
[430] E. J. Weyuker. The cost of data flow testing: an empirical study. IEEE Transactions
on Software Engineering, 16(2):121-128, fev. 1990.
[431] E. J. Weyuker e B. Jeng. Analyzing partition testing strategies. IEEE Transactions on
Software Engineering, 17(7):703-711, jul. 1991.
[432] J. Whaley e M. Rinard. Compositional pointer and escape analysis for Java programs.
ACM SIGPLAN Notices, 34(10):187-206, 1999.
[433] L. J. White e E. I. Cohen. A domain strategy for computer program testing. IEEE
Transactions on Software Engineering, 6(3):247-257, mai.1980.
[434] L. J. White e P. N. Sahay. A computer system for generating test data using the domain
strategy. In: II Conference on Software development tools, techniques, and alternati-
ves, p. 38-45, Los Alamitos, CA, EUA, 1985. IEEE Computer Society Press.
[435] W. E. Wong. On Mutation and Data Flow. Tese de doutoramento, Department of
Computer Science, Purdue University, West Lafayette, IN, EUA, dez. 1993.
[439] W. E. Wong e A. P. Mathur. Reducing the cost of mutation testing: an empirical study.
Journal of Systems and Software, 31(3):185-196, dez. 1995.
[440] W. E. Wong, A. P. Mathur e J. C. Maldonado. Mutation versus all-uses: An empirical
evaluation of cost, strength and effectiveness. In: Software Quality and Productivity:
Theory, practice and training, p. 258-265, Londres, UK, UK, dez. 1995. Chapman &
Hall, Ltd.
[441] M. Wood, M. Roper, A. Brooks e J. Miller. Comparing and combining software de-
fect detection techniques: a replicated empirical study. In: VI European conference
held jointly with the 5th ACM SIGSOFT international symposium on Foundations of
software engineering – ESEC’97/FSE-5, p. 262-277, Nova York, NY, EUA, set. 1997.
Springer-Verlag Nova York, Inc.
[442] M. R. Woodward. Mutation testing – its origin and evolution. Information and Soft-
ware Technology, 35(3):163-169, mar. 1993.
[443] M. R. Woodward e K. Halewood. From weak to strong, dead or alive? an analysis of
some mutation testing issues. In: II Workshop on Software Testing, Verification and
Analysis, p. 152-158, Banff, Canadá, jul. 1988.
[444] M. R. Woodward, D. Heddley e M. A. Hennel. Experience with path analysis and
testing of programs. IEEE Transactions on Software Engineering, 6(3):278-286,
mai.1980.
[445] Y. Wu e A. J. Offutt. Modeling and testing web-based applications. Relatório Técnico
ISE-TR-02-08, George Mason University, Fairfax, VA, EUA, nov. 2002. Disponível
em: http://ise.gmu.edu/techrep/2002/02_08.pdf.
[446] S. Xanthakis, C. Ellis, C. Skourlas, A. LeGall e S. Katsikas. Application of gene-
tic algorithms to software testing. In: V IEEE International Conference on Software
Engineering, p. 625-636, Tolouse, França, dez. 1992.
[447] M. Xie. Software reliability models – a selected annotated bibliography. Software
Testing, Verification and Reliability, 3(1):3-28, 1993.
[448] D. Xu e W. Xu. State-based incremental testing of aspect-oriented programs. In:
AOSD ’06: Proceedings of the 5th international conference on Aspect-oriented soft-
ware development, p. 180-189, Nova York, NY, EUA, 2006. ACM Press.
[449] D. Xu, W. Xu e K. Nygard. A state-based approach to testing aspect-oriented pro-
grams. In: XVII International Conference on Software Engineering and Knowledge
Engineering – SEKE’2005, p. 6, Taiwan, China, jul. 2005.
[450] W. Xu e D. Xu. State-based testing of integration aspects. In: WTAOP ’06: Procee-
dings of the 2nd workshop on Testing aspect-oriented programs, p. 7-14, Nova York,
NY, EUA, 2006. ACM Press.
[451] C.-S. Yang, A. L. Souter e L. L. Pollock. All-du-path coverage for parallel programs.
In: 1998 ACM SIGSOFT International Symposium on Software Testing and Analysis
– ISSTA’98, p. 153-162, Nova York, NY, EUA, jan. 1998. ACM Press.
[452] C-S. D. Yang. Program-Based, Structural Testing of Shared Memory Parallel Pro-
grams. Tese de doutoramento, University of Delaware, 1999.
390 Introdução ao Teste de Software ELSEVIER
[453] S. J. Zeil, F. H. Afifi e L. J. White. Detection of linear errors via domain testing. ACM
Transactions on Software Engineering Methodology, 1(4):422-451, out. 1992.
[454] A. Zeller. Isolating cause-effect chains from computer programs. ACM SIGSOFT
Software Engineering Notes, 27(6):1-10, nov. 2002.
[455] X. Zhang e R. Gupta. Cost effective dynamic program slicing. In: ACM SIGPLAN
2004 conference on Programming language design and implementation – PLDI’04, p.
94-106. ACM Press, jun. 2004.
[456] Xiangyu Zhang, Rajiv Gupta e Youtao Zhang. Precise dynamic slicing algorithms.
In: XXV International Conference on Software Engineering – ICSE’03, p. 319-329,
Washington, DC, EUA, mai.2003. IEEE Computer Society.
[457] J. Zhao. Dependence analysis of Java bytecode. In: XXIV IEEE Annual Internatio-
nal Computer Software and Applications Conference – COMPSAC’2000, p. 486-491,
Taipei, Taiwan, out. 2000. IEEE Computer Society Press.
[458] Y. Zhou, D. Richardson e H. Ziv. Towards a practical approach to test aspect-oriented
software. In: Testing Component-based Systems – TECOS’2004, Erfurt, Alemanha,
set. 2004.
[459] H. Zhu. A formal analysis of the subsume relation between software test adequacy
criteria. IEEE Transactions on Software Engineering, 22(4):248-255, abr. 1996.
Índice Remissivo