Wallace Felipe Francisco Cardoso
StateMutest: uma ferramenta de apoio ao teste
baseado em modelos de estado estendidos
CAMPINAS
2015
Agradeço primeiramente a Deus, pela saúde e proteção durante minha estadia em Cam-pinas, pelos cuidados prestados, por ter colocado em minha vida pessoas de caráter nobre durante os anos em que estudei longe de casa.
A minha mãe, pelo apoio constante, pela preocupação com meu bem estar físico e mental, e pelo amor incondicional presente em todos os momentos. Ao meu avô, pela consideração, e principalmente por estar sempre disposto a me ajudar, independente das circunstâncias. Ao meu pai, pelo apoio nanceiro, nos momentos em que precisei, pude contar com sua ajuda. Ao meu tio e minha tia, e meu primo, pela disponibilidade em me socorrer quando precisei de transporte.
Agradeço a minha orientadora e amiga professora doutora Eliane Martins, pela paci-ência, pela conança, por me ensinar através de seus conhecimentos e sua sabedoria, e por auxiliar em meu crescimento prossional durante dois anos. Eu devo reconhecer que sua orientação e experiência foram fontes importantíssimas para a conclusão de mais uma etapa de minha vida, portanto, meu muito obrigado.
Agradeço a Milena Mendes, pela força e apoio, pela ajuda técnica na língua inglesa, por transmitir sempre pensamentos positivos, e pelo carinho.
Aos amigos de igreja em Pirapozinho e Presidente Prudente, por me proporcionarem bons momentos de descontração durante o pouco tempo que tivemos juntos nesses últimos anos.
Aos amigos em Artur Nogueira, à família Ferreira, pela hospitalidade, carinho e pron-tidão, obrigado por me receber.
Às amizades estabelecidas em Campinas, listo a Thaís, a Juliana, o Anderson, o Narcísio, o Takeo e o Erick, e a Maria Angélica.
Não poderia me esquecer dos professores doutores que auxiliaram e disponibilizaram de seu tempo para conhecer e ajudar neste trabalho, o professor Adenilso Simão, a professora Sandra Fabbri, e a professora Ana Ambrósio.
Aos funcionários do Instituto de Computação da Unicamp, em especial aos da secre-taria, e aos professores. A Unicamp pela infraestrutura, eventos e cursos oferecidos.
Em nossos dias é visível a presença de sistemas de software espalhados por todos os lugares. Uma parte desses sistemas hoje têm grandes responsabilidades quando em operação, pois caso falhem podem provocar sérios danos à vida humana. Consequentemente, o teste de software tem o objetivo de descobrir erros (os quais levam às falhas), visando revelar a presença de defeitos antes mesmo de o sistema ser implantado para operar efetivamente. Atribui-se a efetividade do teste então à probabilidade deste em revelar a maior quantidade de defeitos possível (mas não a ausência deles). Ainda mais, além da efetividade muito se tem feito visando redução de custos através do projeto e implementação de ferramentas de automação dos testes. Neste trabalho, é proposto o projeto e a implementação da StateMutest, uma plataforma baseada na análise de mutantes com suporte à geração de casos de teste, a partir de máquina de estados nita estendida. A geração nativa de casos de teste é baseada em uma abordagem meta-heurística multi objetiva chamada MOST. Visando redução de custos na análise de mutantes, a StateMutest permite a geração de um número reduzido de mutantes; a ferramenta também paraleliza a execução para melhorar o desempenho. São algumas das funcionalidades da StateMutest: edição de modelo, execução de modelo, e o mecanismo de extensão, o qual permite ao usuário inserir outras ferramentas no ambiente. Utilizando a StateMutest foram conduzidos alguns estudos visando vericar o potencial de detecção de defeitos das sequências de teste geradas pelo MOST, e se existem melhorias em desempenho ao aplicar as técnicas de redução de custos na análise de mutantes. Os resultados apontam que os conjuntos de teste gerados pelo MOST são ecientes em revelar defeitos. A partir das sequências de entrada também é possível estabelecer uma estratégia visando reduzir a quantidade destas durante um teste, o que signica redução de custos sem perdas de efetividade. Em consideração às reduções, as técnicas utilizadas (mutação aleatória e execução paralela) alcançaram redução signicativa em relação à execução padrão sem perdas drásticas de efetividade.
Nowadays, we can see the presence of systems everywhere. Today, most systems have several responsibilities when in operation, whose failure may result in serious damage, including, to the human life. Hence, software testing aims to reveal the presence of faults even before the system is ready for use. Test eectiveness is the probability of a given test set to reveal the higher number of faults as possible (but not the absence). Furthermore, researches have been proposed with the goal to reduce costs in applied software testing through project and implementation of testing tools. In this work, it is proposed the StateMutest, a platform for mutation analysis and test case generation from extended nite state machine models. The native test case generator is based on a multi-objective meta-heuristic approach called MOST. In order to reduce mutation analysis costs, StateMutest allows the construction of a reduced number of mutants; it also uses parallelism to improve execution time. StateMutest oers other features such as: model edition, model execution and extension mechanisms to allow the insertion of other tools. We use the StateMutest to conduce some studies targeting to verify the potential of fault detection of sequences generated by MOST, and whether there are improvements in apply techniques to reduce costs in the mutation analysis. Our results shows that the sets generated by MOST are ecient in the sense that it reveals faults. From the sequence set it is possible to dene a strategy to reduce the test set size, which means the reduction of costs without loss of eectiveness. Concluding, by applying techniques of cost reduction it is possible to reduce the time of testing without loss of eectiveness.
2.1 Exemplo de um mutante gerado a partir de um trecho de pseudocódigo. . . 10
2.2 Diagrama de atividade de exemplo para a técnica mutação fraca. . . 13
3.1 Processo de teste baseado em modelo, uxograma. . . 16
3.2 Taxonomia proposta em [54], para teste baseado em modelo. . . 18
3.3 Submáquina chamada de map0. Modelo de MEFE do SMC. . . 24
3.4 Submáquina chamada de map1. Modelo de MEFE do SMC. . . 24
4.1 Exemplos da aplicação dos operadores de mutação de MEF. a) alteração do estado inicial; b) ausência de uma transição; c) ausência de um evento; d) permutação de evento; e) ausência de estado; f) permutação de função de saída; g) ausência de função de saída; h) função de saída extra. . . 29
5.1 Arquitetura Geral da StateMutest. . . 44
5.2 Geração de Mutantes, diagrama de atividades. . . 46
5.3 Exemplo da estratégia utilizada para se reduzir custos durante a geração de mutantes. . . 46
5.4 Construção e Compilação, diagrama de atividades. . . 48
5.5 Execução do modelo, diagrama de atividades. . . 49
5.6 Diagrama simples, o qual mostra gracamente a interação do usuário na StateMutest para executar a análise de mutantes. . . 51
5.7 Diagrama de classes do pacote model. Contém as classes que representam um modelo de estados. . . 53
5.8 Diagrama de classes para os serviços da StateMutest. . . 54
5.9 Diagrama de classes do pacote test. Contém as classes que representam os conjuntos de casos de teste. . . 55
5.10 Edição gráco de um modelo de estados, StateMutest. . . 58
5.11 Edição gráca de um modelo de estados, identicação das transições ativa, StateMutest. . . 59
5.12 Diagrama de atividades para o procedimento básico de importação de um modelo. . . 61
5.13 Trecho de um relatório de teste gerado pelo MOST. . . 62
5.14 Caso de teste, padrão XML, gerado pela StateMutest. . . 63
5.15 Roteiro de teste utilizado para aplicar a análise de mutantes em um con-junto de teste. . . 65
5.16 Tela de inicialização. . . 67
5.17 Conteúdo do arquivo main.inf, exemplo. . . 68
5.18 Modelo exemplo de uma máquina de estados estendida da StateMutest, com dois estados e duas transições. . . 69
A.1 Tela principal da StateMutest. . . 97 A.2 Tela Principal, com um projeto de teste aberto, o editor de modelos aberto,
e as guias de saídas produzidas abertas. . . 98 A.3 Terminal de comandos. . . 101
4.1 Operadores de Mutação de Statecharts. . . 32 4.2 Comparativo vericando quais ferramentas implementam extensões e
tec-nologias de redução de custo: mutação restrita (MR), mutação aleatória (MA) e execução concorrente. . . 38 4.3 Comparativo das ferramentas e dos modelos utilizados para geração de
casos de teste . . . 39 4.4 Comparativo vericando o tipo de execução utilizada pela ferramenta para
executar um modelo. . . 39 5.1 Conguração e mapeamento para a geração de casos de teste com o MOST. 62 6.1 Modelos e seus respectivos atributos, utilizados no estudo de caso. . . 73 6.2 Modelos e informações dos conjuntos de teste: sequência de entrada
(quan-tidade), sequência de entrada máxima (maior), sequência de entrada mí-nima (menor). . . 73 6.3 Tempos de construção e compilação dos modelos dos projetos utilizados,
número de modelos (original + mutantes), e carga média de processamento. 76 6.4 Informações gerais dos testes realizados com 11 projetos, total de modelos
(originais + mutantes), quantidade de casos de teste, execuções (total de modelos * quantidade de casos de teste) e tempo. . . 76 6.5 Escores de mutação para o conjunto teste nominal, e o conjunto de teste
de sequências inoportunas. Também são apresentados a quantidade de mutantes inativos (MI), e a quantidade de mutantes equivalentes (ME). . . 77 6.6 Tempo de execução de cada modelo, referente ao conjunto de teste de
sequência nominal, e ao conjunto de teste de sequência inoportuna. . . 78 6.7 Escores de mutação para o conjunto de teste de SEML, e para o conjunto
de teste de SEMC. . . 79 6.8 Tempo de execução de cada modelo, referente ao conjunto de teste de
sequência mais longa, e ao conjunto de teste de sequência mais curta. . . . 80 6.9 Escores de mutação para o conjunto de 10% de mutantes, e para o conjunto
de todos os mutantes. . . 81 6.10 Tempo de execução de cada modelo, para o conjunto de 10% de mutantes,
e para o conjunto de todos os mutantes. . . 82 6.11 Escores de mutação para o conjunto de mutantes de operadores de mutação
de MEF, e para o conjunto de mutantes de operadores de mutação de MEFE. 83 6.12 Tempo de execução para a execução concorrente utilizando 4 subprocessos,
Agradecimentos 5 Resumo 6 Abstract 7 1 Introdução 1 1.1 Motivação . . . 2 1.2 Objetivos . . . 3 1.3 Contribuições do Trabalho . . . 4 1.4 Organização do Trabalho . . . 4 2 Referencial Teórico 6 2.1 Teste de Software . . . 6 2.1.1 Jargão de Teste . . . 6 2.1.2 Conceito . . . 7 2.1.3 Processo Geral . . . 7
2.1.4 Classicação das Técnicas . . . 8
2.2 Teste de Mutação . . . 9
2.2.1 Problemas . . . 11
2.2.2 Técnicas de Redução de Custo . . . 11
3 Teste Baseado em Modelo de Estados 15 3.1 Teste Baseado em Modelo . . . 15
3.1.1 O Processo de Teste Baseado em Modelo . . . 16
3.1.2 A Taxonomia . . . 17
3.1.3 Limitações . . . 19
3.2 Modelos de Estado . . . 19
3.2.1 Máquina de Estados Finita . . . 20
3.2.2 Máquina de Estados Finita Estendida . . . 21
3.2.3 A Máquina de Estados do SMC . . . 23
3.3 O Método MOST . . . 25
4 Teste de Mutação Baseado em Modelo de Estados 27 4.1 Operadores de Mutação de Máquina de Estados Finita . . . 27
4.2 Operadores de Mutação de Máquina de Estados Finita Estendida . . . 28
4.3 Operadores de Mutação de Statecharts . . . 30
4.4 Ferramentas de Teste para Análise de Mutantes . . . 31
5 StateMutest 40
5.1 Requisitos . . . 40
5.1.1 Requisitos Funcionais . . . 41
5.1.2 Requisitos Não Funcionais . . . 43
5.1.3 Escopo Simplicado . . . 43
5.2 Arquitetura da Ferramenta . . . 44
5.3 Visão de Processos . . . 45
5.3.1 Geração de Mutantes . . . 45
5.3.2 Construção e Compilação . . . 45
5.3.3 Execução dos Modelos . . . 47
5.4 Procedimento de Utilização para a Análise de Mutantes . . . 50
5.5 Visão de Desenvolvimento . . . 51
5.5.1 Projeto Detalhado e Implementação . . . 51
5.5.2 Detalhes Técnicos de Implementação dos Serviços . . . 52
5.5.3 Tecnologias Utilizadas . . . 56
5.6 Editor Gráco de Modelos de Estado . . . 56
5.7 Importação dos Modelos e dos Casos de Teste . . . 60
5.7.1 Importação de Modelos . . . 60
5.7.2 Importação de Casos de Teste . . . 61
5.8 Automação dos Testes . . . 64
5.9 Empacotamento . . . 66
5.10 Extensões . . . 67
5.10.1 Extensão TestC Alpha Filter e TestC Alpha Selector . . . 68
6 Experimentos 71 6.1 Objetivos . . . 71
6.2 Estudos de Caso . . . 72
6.3 Processo . . . 74
6.3.1 Denição do Projeto e Modelagem . . . 75
6.3.2 Denição dos Casos de Teste . . . 75
6.3.3 Conguração dos Testes . . . 75
6.4 Resultados . . . 75
6.4.1 Estudo de Caso 1 Nominal Contra Inoportuno . . . 77
6.4.2 Estudo de Caso 2 Sequência Longa Contra Sequência Curta . . . 78
6.4.3 Estudo de Caso 3 Todos os Mutantes Contra 10% dos Mutantes . 80 6.4.4 Estudo de Caso 4 Operadores de MEF Contra Operadores de MEFE 82 6.4.5 Estudo de Caso 5 - Execução Concorrente . . . 82
6.5 Discussão dos Resultados . . . 83
6.6 Ameaças à Validade . . . 85
7 Conclusões 86 7.1 Questões Respondidas e Contribuições . . . 88
Introdução
A indústria e as corporações governamentais estão cada vez mais dependentes de sistemas complexos baseados em computador, presentes na automação e distribuição industrial, em produtos elétricos, assim como em sistemas nanceiros [49, p. 2-10].
Em todo o mundo, o software é considerado como uma tecnologia de extremo valor e indispensável aos negócios, para a ciência e engenharia. Muitos destes são sistemas de alta complexidade, e podem trazer sérios riscos à vida e ao meio ambiente, assim, cuidados quanto à qualidade de software são prioridades que visam a minimização dos riscos envolvidos [16].
Garantia de qualidade de software é denominada como um conjunto de atividades de apoio que devem ser realizadas em todas as etapas do desenvolvimento de software. Dentre as atividades de apoio, a atividade de teste é um item importante considerado crítico para se garantir a qualidade de software desejada [13, p. 27-29].
Uma das prioridades do teste de software é automatizar a atividade de teste, reduzindo custos, evitando-se enganos humanos e fazendo do teste uma tarefa mais simples. Os custos gastos durante toda a atividade de teste em projetos de desenvolvimento de software ultrapassam os 50% dos custos totais de um projeto, portanto, a construção de ferramentas que auxiliem o teste durante as etapas de desenvolvimento de software são de grande valia para a engenharia de software [4, p. 10] [10].
O objetivo do teste é encontrar erros (do inglês error). Devem ser os testes capazes de revelar a presença do maior número de defeitos existentes, com o mínimo de esforço possível. Para se alcançar produtividade e qualidade, foram propostos conjuntos de re-quisitos a serem cumpridos durante a atividade de teste, e seus respectivos critérios de teste. Através de uma análise de cobertura de um critério obtém-se uma quanticação mensurável da atividade de teste [4, p. 16-20] [43, p. 429].
O critério de teste Análise de Mutantes, o qual se aplica no contexto deste trabalho, é um critério de teste baseado em defeitos. A ideia visa tentar garantir que todos os possíveis defeitos típicos produzidos por programadores sejam descobertos. Existem duas formas de se aplicar o critério: na geração de casos de teste, e na avaliação de um conjunto de casos de teste. O processo de teste consiste em matar os mutantes gerados, os quais são versões defeituosas do artefato original. Para matar um mutante, basta fazer com que ele falhe [30].
Inicialmente, o teste de mutação foi proposto para ser aplicado nos programas de com-1
surgiram a partir de 1983 [30]. Assim como em programas, o teste de mutação aplicado à especicação de sistemas tem o princípio básico de reproduzir os defeitos frequentemente produzidos por engenheiros de software, a m de adequar os conjuntos de casos de teste à cobertura destes defeitos típicos. Especicações formais, tais como a máquina de estados nita (MEF) e o Statecharts, são fortemente utilizadas na modelagem de sistemas com-plexos, assim, existem vários métodos de validação e vericação baseados nestas técnicas, visando atender à garantia da qualidade de software [16].
Desde 1970, marco inicial de pesquisas sobre teste de mutação, pesquisadores têm se esforçado para reduzir os problemas encontrados durante anos de estudo. As técnicas de redução de custos lidam com o problema do grande número de mutantes que podem ser gerados, os quais são utilizados no processo da análise de mutantes. Dentre as técnicas de redução de custos propostas para o teste de mutação, destacam-se a mutação seletiva, a mutação de ordem-k, a mutação aleatória, a técnica mutant schemata, a execução paralela, e a mutação restrita. Além destas, plataformas avançadas de distribuição de carga de execução foram propostas, com o objetivo de reduzir também os custos com a execução dos testes com mutantes [57] [52] [29] [41].
Neste trabalho foram realizados alguns estudos envolvendo a técnica Análise de Mu-tantes para avaliação de conjuntos de teste gerados a partir de modelos de estados. Uma ferramenta de apoio ao teste de software foi desenvolvida, nomeada StateMutest, a qual oferece suporte à Análise de Mutantes, geração de sequências de teste (através do MOST, do inglês Multi-Objective Search-based Testing, descrito na seção 3.3), e integração com extensões de desenvolvedores terceiros. Em [33] foi realizado um estudo visando avaliar o MOST, no qual os autores utilizaram também a análise de mutantes para avaliação das sequências de teste geradas a partir de modelos de máquina de estados nita estendida (MEFE). Entretanto, ao invés dos autores utilizarem os operadores de mutação de MEFE, eles utilizaram os operadores de mutação da linguagem Java. Os estudos realizados neste trabalho tiveram por base o estudo exploratório proposto em [33].
1.1 Motivação
O MOST é um algoritmo de geração de sequências de teste multiobjetivo: objetivo de gerar sequências factíveis, e objetivo de gerar sequências de tamanho mínimo ótimo. Uti-lizando o MOST, torna-se possível gerar um conjunto de sequências de entrada capaz de exercitar as transições de um modelo, desde que estas sejam factíveis. Por outro lado, no teste com mutantes, os casos de teste devem ser capazes de matar mutantes. Cada mutante representa um requisito de teste, assim, quando todos os mutantes são mortos então todos os requisitos de teste são satisfeitos. O teste de mutação é uma técnica de teste baseada em defeitos, portanto, é interessante avaliar a capacidade de adequação do MOST ao critério análise de mutantes, visando modelos de estados.
Neste contexto, o trabalho proposto em [33] abordou a utilização da análise de mu-tantes para avaliar os conjuntos de sequências de entrada geradas pelo MOST. Os autores utilizaram os operadores de mutação propostos para a linguagem Java. Entretanto, os
sujeitos da pesquisa foram modelos de estados. Tecnicamente, os modelos de estados são traduzidos para a linguagem Java, os quais são compilados para se obter a versão execu-tável de cada modelo. Um dos problemas em utilizar operadores de mutação propostos para Java em modelos executáveis de máquina de estados é que muitos dos operadores de mutação propostos para máquina de estados não têm correspondência com operadores de mutação propostos para Java.
Visando resolver este problema, propõe-se neste trabalho a utilização da análise de mutantes para avaliação dos casos de teste gerados para modelos de máquina de estados, utilizando uma ferramenta de apoio ao teste de software com suporte a análise de mutan-tes, através de operadores de mutação propostos para modelos de estados. A ferramenta de teste além de automatizar reduz as chances de ocorrerem enganos humanos durante cargas pesadas de trabalho manual.
Diferentemente de outras ferramentas de teste de software com suporte a análise de mutantes, a StateMutest utiliza apenas mutação de modelos de máquina de estados atra-vés dos operadores de MEFE. Depois de gerados os modelos mutantes, cada um deles é traduzido em código para ser gerado um modelo executável. Para viabilizar e reduzir os custos gastos durante a atividade de teste, a ferramenta de teste StateMutest, proposta neste trabalho, implementa algumas das técnicas de redução de custos propostas para a análise de mutantes.
1.2 Objetivos
O estudo exploratório conduzido em [33] serviu de base para os estudos realizados neste trabalho. Neste foram utilizados os operadores de mutação para máquina de estados. Para realização da pesquisa, foi necessário o desenvolvimento de uma ferramenta de teste, com suporte a análise de mutantes e suporte a importação e gerenciamento das sequências de teste geradas em [55]. Existem outras formas de lidar com casos de teste gerados por outras ferramentas, como por exemplo, através da integração com extensões que permitam a geração ou a importação de casos de teste.
Os estudos realizados abordaram a redução da quantidade de mutantes, e a redução da quantidade de casos de teste utilizados durante um teste, preservando a efetividade de teste. Os sujeitos deste trabalho são modelos estendidos dos autômatos nitos clássicos.
Em suma, a avaliação da efetividade das sequências de teste geradas pelo MOST é a base das questões levantadas nesta pesquisa. A avaliação é importante tanto no contexto do algoritmo de geração de sequências quanto no contexto da ferramenta. A ferramenta implementa a geração de sequências de teste, utilizando o MOST. Assim, vericar o potencial de detecção de defeitos do MOST implica também em vericar a eciência de uso da StateMutest. As mesmas questões levantadas neste trabalho podem ser utilizadas para outros algoritmos de geração de sequências de entrada que tenham por base uma máquina de estados nita estendida, contudo, neste trabalho, o foco manteve-se em utilizar a StateMutest e o MOST no contexto do teste baseado em modelo. O interesse em avaliar o MOST e não outro gerador de sequências de entrada deve-se ao fato do algoritmo ter sua origem no Grupo de Teste de Software do Instituto de Computação da Unicamp [33, 55],
1.3 Contribuições do Trabalho
Neste trabalho, uma ferramenta de teste baseado em modelos estendidos das máquinas de estado foi projetada e implementada, denominada StateMutest. A StateMutest é ca-paz de automatizar a geração de casos de teste utilizando extensões, e também através do MOST, um algoritmo meta-heurístico multiobjetivo, embutido na ferramenta. Outra funcionalidade é a avaliação de conjuntos de casos de teste através da análise de mutan-tes. Foram exploradas questões de desempenho quanto ao uso de técnicas de redução de custos, as quais a StateMutest se propõe a realizar com eciência através da utilização da maior quantidade dos recursos disponíveis na máquina, como núcleos de processamento e memória. Dentre as técnicas de redução de custos propostas para a análise de mutantes, a StateMutest implementa a mutação restrita e a mutação aleatória, a execução paralela e uma estratégia alternativa para reduzir custos durante a geração de mutantes.
As extensões podem ser utilizadas para que usuários possam embutir seus algoritmos dentro da ferramenta de teste. A StateMutest vem com um algoritmo de geração de casos de teste, no qual os casos de teste (teoricamente, sequências de entrada) gerados por este foram utilizados tanto para validar requisitos funcionais e não funcionais (desempenho, entre outros) da StateMutest, quanto também para vericar o potencial do conjunto de teste em revelar a presença de defeitos de software, de acordo com o critério análise de mutantes.
1.4 Organização do Trabalho
Neste capítulo foi apresentado o contexto, a motivação, as contribuições e os objetivos desse trabalho. O Capítulo 2 apresenta os conceitos básicos sobre Teste de Software, o qual consiste da denição dos termos técnicos, do processo geral de teste, e da apresentação das técnicas clássicas: teste funcional, teste estrutural, e teste baseado em defeitos. Também apresentam-se os conceitos sobre o Teste de Mutação, a denição, as hipóteses básicas, os problemas em aberto, e as técnicas de redução de custos. O Teste Baseado em Modelos de Estado é apresentado no Capítulo 3, o qual aborda o processo, a taxonomia e as limitações deste. Também são apresentadas as máquinas de estados, a denição e os modelos mais conhecidos de máquinas de transição de estados, e por último, é apresentado o MOST, um método de geração de sequências de entrada a partir de máquinas estendidas. O Capítulo 4 apresenta a Mutação de Modelos, e os operadores de mutação denidos para Máquina de Estados. Também é apresentado uma revisão das ferramentas de teste para mutação de programas, e para mutação de modelos, e ao nal é feito um comparativo entre as ferramentas de mutação de modelos, incluindo também a StateMutest. No Capítulo 5 é apresentada a StateMutest, a ferramenta de teste projetada e desenvolvida através deste trabalho. O capítulo traz os requisitos, a visão geral, a visão de desenvolvimento, entre outros. No Capítulo 6 apresentam-se os Experimentos, no qual foram realizados 5 estudos de caso para avaliar as sequências de entrada geradas pelo MOST e as técnicas de redução
Referencial Teórico
Neste capítulo serão apresentados os principais conceitos sobre teste de software e os fundamentos do teste com mutantes.
2.1 Teste de Software
Tem-se no mínimo duas décadas onde o fato de 50% dos custos de um projeto de software serem gastos em testes permanecer como verdade. Muitas propostas vieram no sentido de aperfeiçoar os processos que auxiliam tanto o desenvolvimento quanto o teste de software [4, p. 10].
O teste de software deveria ser considerado tão importante quanto o desenvolvimento de software pelas equipes de desenvolvimento, fato este que é frequentemente negligenciado na prática [38, p. 8]. Muitos fatores podem contribuir para que isto ocorra: inexperiência da equipe com teste de software, prazos curtos de um projeto mal planejado, entre outros. A verdade é que a atividade de teste é muito importante quando se tem o objetivo de entregar software de qualidade aos clientes. Bom seria se fosse verdade que programadores são bons o suciente para escrever seus códigos livres de defeitos. Entretanto, códigos são escritos por seres humanos que frequentemente se enganam [18].
2.1.1 Jargão de Teste
Para um bom entendimento de assuntos recorrentes da área de teste de software, torna-se essencial entender as diferenças entre os termos: engano, defeito, erro e falha (do inglês mistake, fault, error, and failure).
Dene-se engano como uma ação humana que produz um defeito. Por sua vez, dene-se defeito como um passo, processo ou denição de dados incorretos. Quando em execução, se há defeitos no software, provavelmente há erros que podem ocorrer ou não durante a execução. Quando um determinado estado do sistema que contém erros produz saídas incorretas, assume-se que ocorreu uma falha neste sistema [13, p. 2].
Como se pode observar existe uma relação de causalidade entre os termos. Os even-tos parecem estar na ordem: engano, defeito, erro e falha. Entretanto, há erros que podem somente ser revelados sob condições especícas de execução, como por exemplo, em computadores antigos com determinadas limitações de memória ou processamento.
Por mais que um determinado código esteja livre de defeitos, ainda há a possibilidade de ocorrerem falhas decorrentes do ambiente em que este código será executado [43, p. 472]. Assim, torna-se necessário testar os sistemas de software durante todas as fases do ciclo de desenvolvimento em que seja possível executar os artefatos.
2.1.2 Conceito
O objetivo de um teste de software é executar um programa, seja simbólica ou efetiva-mente, no sentido de revelar a maior quantidade de defeitos tanto quanto possível. Por-tanto, testar um determinado programa não visa garantir que ele funciona apenas, mas sim, partindo do pressuposto de que este contém defeitos em sua construção, signica que a partir do teste deseja-se revelar a maior quantidade de defeitos possível, expondo o sistema às situações em que este venha a falhar [38, p. 10-11].
Existe uma correlação entre o teste de software e a conabilidade de software, quanto mais defeitos são descobertos mais conabilidade um determinado software terá. Con-abilidade pode ser denida como a probabilidade de nenhuma falha ocorrer durante a execução de um software em um determinado período de tempo [22]. Através da atividade de teste não é possível garantir que um determinado software estará livre de defeitos, o que pelo teste isto é praticamente impossível. Mesmo que para um software muito pe-queno, com poucas linhas de código, há uma innidade de possíveis entradas de dados, o que resultada em uma explosão combinatória de possíveis casos de teste. Testar todos os possíveis casos torna-se impraticável [13, p. 4-5].
Embora existam outras abordagens que avaliem a existência de defeitos em um pro-grama, por exemplo, a prova de corretude [22], as quais podem ser utilizadas comple-mentarmente ao teste de software, a atividade de teste ainda assim torna-se indispensável devido aos mais variados tipos de ambientes em que tais sistemas de software podem ser operados [37].
2.1.3 Processo Geral
Um processo geral de teste de software envolve a escolha de um conjunto de casos de teste, a execução deste conjunto contra o software sob teste, e a análise das saídas produzidas para cada caso de teste. Um caso de teste é composto por uma entrada e uma saída esperada [13, p. 3]. Os casos de teste são utilizados visando revelar a presença de defeitos no software. Se um caso de teste é executado contra o software, e este apresenta uma saída diferente da saída esperada, então ocorreu uma falha. Para completar a atividade de teste, todas as saídas produzidas pelo sistema sob teste devem ser iguais às saídas esperadas pelos casos de teste. Se um caso de teste revela uma falha, então é necessário que o defeito que causou a falha seja corrigido [43, p. 450-455].
O domínio de todas as possíveis entradas que um determinado programa pode aceitar é enorme, e testar todas elas é simplesmente impraticável. O que se faz na prática é selecionar apenas um subconjunto a partir de todas as possibilidades, um subconjunto que tente representar o todo [13, p. 4-6].
es-determinado software é testado de acordo com requisitos que se deseje cobrir ou satisfazer. Por exemplo, se dado critério estabelece como regra que todas as expressões aritméticas devem ser testadas pelo menos uma vez, então cada expressão aritmética contida no có-digo torna-se um requisito a ser satisfeito. Vários casos de teste podem ser derivados para satisfazer um requisito. Todo critério de cobertura de teste fornece uma métrica, conhecida como nível de cobertura, a qual indica o quanto um determinado conjunto de teste satisfaz os requisitos de teste exigidos pelo critério. Calcular o nível de cobertura é relativamente simples, o qual é dado pela razão entre a quantidade de requisitos satisfeitos e a quantidade total de requisitos [4, p. 17-18].
2.1.4 Classicação das Técnicas
São três as técnicas de teste mais comuns encontradas na literatura, as quais se baseiam na forma em como os testes são derivados e aplicados contra o software sob teste [13, p. 9].
A primeira técnica é conhecida como teste funcional, ou teste caixa preta. No teste funcional os requisitos de teste e os casos de teste são gerados sem conhecimento algum da estrutura interna do software que está sendo testado. É muito comum no teste caixa preta que os requisitos de teste sejam derivados ou a partir da especicação das interfaces do sistema ou a partir do modelo comportamental do sistema [4, p. 21].
A segunda técnica é conhecida como teste estrutural, ou teste caixa branca. No teste estrutural os requisitos de teste e os casos de teste são gerados a partir do conhecimento da estrutura interna do sistema, o que signica que o código do sistema está disponível e pode ser utilizado como fonte de derivação de requisitos para os testes [4, p. 21]. Myers et. al. [38, p. 13-14] argumentam que no teste caixa branca existe o interesse em se testar toda a estrutura lógica do programa (o código), e que frequentemente a especicação do sistema é negligenciada, infelizmente.
A terceira técnica é conhecida como teste baseado em defeitos. No teste baseado em defeitos os requisitos de teste e os casos de teste são gerados baseados nos defeitos típicos cometidos pelos desenvolvedores [13, p. 77].
Todo sistema é construído baseado em requisitos funcionais e não funcionais, os quais também são explorados na atividade de teste. Os requisitos funcionais de um sistema descrevem o que ele deve fazer. Por sua vez, os requisitos não funcionais são restrições aos serviços ou funcionalidades oferecidas pelo sistema, como conabilidade, tempo de resposta e desempenho [49, p. 59-60].
De acordo com Delamaro et. al. [13, p. 3-4], os testes podem ser realizados em três níveis de granularidade: teste de unidade, teste de componente, e teste de sistema.
O teste de unidade é utilizado para se testar a partes menores de um sistema, como as classes ou os métodos. No teste de componente assume-se que os componentes (conjunto de classes) prestam serviços os quais internamente já foram testados através do teste de unidade, assim neste caso necessita-se apenas mostrar que a interação entre componentes e as interfaces apresentadas foram implementadas corretamente. Por último, o teste de sistema visa revelar falhas que podem ocorrer após a integração de todas as componentes
do sistema, ou seja, o teste do sistema integrado.
Assim também, no teste de sistema são vericadas se as funcionalidades do sistema foram implementadas corretamente de acordo com a especicação, ou seja, os requisi-tos. Nesta, são explorados aspectos de correção, completude, coerência, e também dos requisitos não funcionais tais como segurança, desempenho e robustez [13, p. 4].
2.2 Teste de Mutação
Há aproximadamente pouco mais de três décadas desde o surgimento do teste de muta-ção, período em que muitas técnicas, provas teóricas e métodos foram propostos com o objetivo de popularizar uma das técnicas de teste considerada das mais poderosas pelos pesquisadores da área. Uma das tarefas mais importantes do teste de software é a seleção de casos de teste. Um bom conjunto de casos de teste é aquele que tem uma chance alta de revelar a presença de defeitos no software. Basicamente, o teste de mutação auxilia uma equipe de teste fornecendo a ela uma mensuração da quantidade dos tipos de defeitos que um conjunto de teste pode revelar [15].
O teste de mutação é uma técnica de teste baseada em defeitos. Isto signica que a fonte de derivação dos requisitos de teste é o conjunto dos típicos defeitos produzidos pelos programadores. Logo, casos de teste gerados a partir do teste de mutação devem ser aptos a revelar a presença de determinados tipos especícos de defeitos [30].
Duas hipóteses sustentam toda a teoria do teste de mutação: a hipótese do efeito acoplamento e a hipótese do programador competente. Torna-se impraticável considerar todos os possíveis tipos de defeitos e suas combinações. Programadores, em sua maioria, possuem competência e conhecimento para escrever programas ou corretos ou próximos do correto. Neste sentido, a hipótese do programador competente supõe que programadores criam seus programas muito próximos de serem corretos.
A hipótese do efeito acoplamento supõe que se um caso de teste é capaz de revelar a existência de um erro simples, este também é capaz de detectar erros complexos. O erro simples é a causa de um único defeito, enquanto que o erro complexo é a causa de dois ou mais defeitos [15].
Ambas as hipóteses reduzem a complexidade de se aplicar o teste de mutação, as hipóteses restringem o domínio de todos os possíveis tipos de defeitos e suas combinações para apenas um subconjunto de tipos especícos de defeitos, descartando os defeitos que dicilmente seriam produzidos por um programador experiente, e desconsiderando suas combinações [13, p. 84-87].
O processo do teste de mutação inicia-se com a geração de mutantes [41]. Dado um artefato sob teste, são geradas várias versões defeituosas desse artefato, denominados mu-tantes. Cada mutante deve ser sintatica e semanticamente diferente do artefato original. Casos de teste são gerados, com o objetivo de mostrar que todos os mutantes apresen-tam resultados diferentes do artefato sob teste. Se um caso de teste consegue revelar a diferença do mutante e do artefato original, é dito que o caso de teste conseguiu matar o mutante, e o mutante é considerado morto. Porém, se nenhum caso de teste foi capaz de matar um determinado mutante, este mutante é considerado vivo. Mutantes vivos podem
Figura 2.1: Exemplo de um mutante gerado a partir de um trecho de pseudocódigo. signicar que ou o mutante é equivalente ao artefato original (sintaticamente diferente e semanticamente equivalente), ou o conjunto de casos de teste não é bom o suciente para revelar o tipo de defeito representado pelo mutante vivo [4, p. 180-181]. A Figura 2.2 apresenta um exemplo de um mutante, gerado a partir de um pseudocódigo (original), com apenas uma alteração (return a + b para return a b).
Durante a etapa de geração, mutantes são gerados a partir da aplicação de operadores de mutação. Cada operador de mutação contém uma regra simples de como e onde a mutação deverá ocorrer. Além disso, os operadores costumam representar os tipos especícos de defeitos, e em alguns trabalhos costumam ser separados em categorias.
Um exemplo de operador de mutação é o operador Required Constant Replacement (CRCR, traduzido como Substituição de Constante Requerida) proposto para a linguagem C [2]. Considere a seguinte sentença: k = j + *p, onde k e j são inteiros, e *p é um apontador para um inteiro. Dado um conjunto I = {0, 1, -1, u}, onde u é um inteiro fornecido, cada referência a um escalar é trocada por um dos elementos de I. No caso de apontadores, substituem-se estes pelo valor null. Neste exemplo, têm-se então cinco mutações simples: (i) k = 0 + *p; (ii) k = 1 + *p; (iii) k = -1 + *p; (iv) k = u + p; (v) k = j + null.
A análise de mutação, no que diz respeito ao processo, é muito semelhante ao teste de mutação. A única diferença é que na análise de mutação não há geração de casos de teste visando matar os mutantes gerados [30].
Conhecida a quantidade de mutantes mortos e a quantidade de mutantes não equiva-lentes, o escore de mutação é obtido através de um cálculo muito simples: a razão entre a quantidade de mutantes mortos e a quantidade de mutantes não equivalentes. O cálculo do escore de mutação é dado pela equação 2.1, onde mortos é uma função que retorna a quantidade de mutantes mortos em M através de T, M é o conjunto de mutantes, T é o conjunto de casos de teste, e equi é uma função que retorna a quantidade de mutantes equivalentes em M.
escore(M, T ) = mortos(M, T )
|M | − equi(M ) (2.1)
Um escore muito alto, próximo a 1, indica que um determinado conjunto de teste é muito bom em revelar a presença de determinados tipos especícos de defeitos [13, p. 89]. Por outro lado, um escore muito baixo, próximo à zero, indica que o conjunto de teste é muito fraco em revelar a presença de determinados tipos especícos de defeitos, e que este provavelmente deve ser melhorado.
2.2.1 Problemas
Embora seja uma técnica muito eciente no sentido de construir bons conjuntos de casos de teste, existem implicações que fazem com que o teste de mutação não seja tão utilizado na prática.
Inicialmente, quando foi proposto, muito se fez em trabalhos visando a redução da quantidade de mutantes gerados. Mesmo com a redução no domínio de versões defeituosas a partir das hipóteses básicas do teste de mutação, a quantidade de mutantes gerados ainda permaneceu como um problema. Uma quantidade muito grande de mutantes poderia ser gerada a partir de um artefato sob teste, os quais eram ou interpretados ou compilados para serem posteriormente executados contra todo o conjunto de casos de teste. No pior dos casos, são executados todos os mutantes contra todos os casos de teste do conjunto. Neste caso, são necessários níveis altos de processamento, armazenamento e tempo, os quais impactam negativamente nos custos de teste [30].
O problema do mutante equivalente ainda permanece como um grande desao. Depen-dendo do artefato sob teste, isto não é um problema. Por exemplo, existem algoritmos de minimização de autômatos nitos que conseguem mostrar a equivalência entre dois autômatos [36, p. 66-72]. Entretanto, para sistemas de software e para a maioria dos modelos de software, não existe nenhum algoritmo que consiga decidir se há equivalência ou não.
A forma mais simples de resolver o problema do mutante equivalente é deixar a cargo da equipe de teste a decisão de quais são os mutantes equivalentes. É importante que haja o mínimo de mutantes equivalentes possíveis, pois a presença deles prejudica no cálculo do escore de mutação. Suponha que a metade dos mutantes gerados são mutantes equivalentes, e que nenhum deles foi considerado como tal durante o cálculo do escore. Com metade dos mutantes equivalentes gerados, o escore de mutação não seria maior do que 0.5, o que daria uma falsa impressão de que o conjunto de casos de teste deveria ser melhorado.
A maior parte dos trabalhos que visam melhorias práticas é voltada para reduções de custo. Os trabalhos variam com abordagens que propõem técnicas de seleção de um subconjunto representativo de mutantes, e propostas de aplicação do teste de mutação em arquiteturas e ambientes especícos [13, p. 90-93].
2.2.2 Técnicas de Redução de Custo
Devido aos altos custos em se aplicar a análise de mutantes, técnicas de redução de custo foram propostas buscando solucionar este problema. As técnicas de redução podem ser divididas em duas categorias, conforme o objetivo da técnica: redução de custos com a geração, ou com a execução dos mutantes.
As técnicas propostas para geração buscam maneiras de se reduzir o número de mu-tantes utilizados sem perdas de efetividade de teste, enquanto que as técnicas de redução para execução buscam soluções que consigam reduzir o tempo gasto com a compilação e a construção dos artefatos executáveis, assim como paralelizar a execução.
A mutação aleatória pode ser considerada uma técnica muito simples de redução de custo [1]. A partir do conjunto total de mutantes que podem ser gerados, seleciona-se
pelos pesquisadores, onde resultados mostram que é possível selecionar apenas 10% do total de mutantes, para cada operador de mutação, com perda de 5% no escore de mutação, no máximo.
Uma outra forma de se reduzir a quantidade de mutantes é restringir o conjunto de operadores de mutação utilizados para gerar mutantes. Cada operador é responsável por gerar uma determinada quantidade de mutantes. A equipe de teste pode então selecionar apenas operadores de mutação que geram poucos mutantes, por exemplo. Esta técnica é conhecida como mutação restrita: um subconjunto dos operadores de mutação é selecionado para gerar mutantes.
Ainda visando utilizar a menor quantidade de mutantes possível, Hussain [28] propôs uma técnica chamada agrupamento de mutantes (do inglês mutant clustering). A ideia é utilizar algum algoritmo de agrupamento por características no sentido de identicar os possíveis grupos de mutantes com características semelhantes e selecionar apenas alguns representantes de cada grupo.
A mutação de ordem-k pode ser tanto utilizada para redução no conjunto de mutantes quanto na redução do conjunto de casos de teste. Tradicionalmente o teste de mutação utiliza mutações simples (apenas um alteração sintática), o que é equivalente à mutação de ordem-1 (k=1). O valor de k indica a quantidade de alterações de cada mutante. Por exemplo, um mutante de ordem-3 contém três alterações, em outras palavras, três defeitos introduzidos, que podem ser de tipos diferentes ou não.
Utilizar todas as possíveis combinações de defeitos é algo impraticável, o que resulta no problema da explosão combinatória. No contexto de mutação de ordem-k, destacam-se os trabalhos realizados por Polo et. al. [41] e Jia e Harman [29], os quais se propõem a resolver o problema da explosão combinatória utilizando heurísticas para selecionar apenas um subconjunto dos mutantes de ordem-k.
Assume-se um certo risco ao se utilizar as mutações de ordem-k. Teoricamente, casos de teste que matam mutantes de ordem-1 são também capazes de revelar mutantes de ordem-k, entretanto, a recíproca não é verdadeira. Segundo Polo et. al. [41], quanto maior for o valor associado a k maior será o risco, e consequentemente menor será a eciência do teste.
Após a geração de mutantes de um determinado artefato, os mutantes têm de ser executados contra os casos de teste. Isso pode ser feito ou através da compilação de código ou da interpretação de código. Embora compilar todos os mutantes para depois executá-los pareça muito custoso, o tempo de execução dos mutantes compensa o tempo gasto na compilação. Alguns estudos comprovaram que a compilação é mais eciente do que a interpretação na aplicação do teste de mutação [30].
A forma mais simples de compilação no teste de mutação é compilar cada mutante individualmente. Assim, o tempo de compilação neste caso sempre será igual ao tamanho do conjunto de mutantes gerados. Pensando nisto, Untch et. al. [52] propuseram uma técnica chamada mutant schemata. Na técnica mutant schemata todas as mutações devem estar presentes em um único fonte, o que resulta na necessidade de apenas uma compilação. Como consequência, todos os mutantes deverão ser sintática e semantica-mente corretos.
Figura 2.2: Diagrama de atividade de exemplo para a técnica mutação fraca. Com o intuito de melhorar o tempo gasto durante a execução dos mutantes, Howden [27] propôs a mutação fraca. Tradicionalmente, a comparação das saídas obtidas por um mutante contra as saídas produzidas pelo artefato original é feita ao nal da execução de ambos, o que é conhecido como mutação forte [15]. Na mutação fraca, primeiro identicam-se os componentes do artefato sob teste. Por exemplo, em um programa, os componentes podem ser métodos, classes, ou trechos de código dentro dos métodos. A comparação das saídas então é feita após a execução do componente mutante, não neces-sitando mais de todo o restante da execução. Caso um componente seja dependente de outro componente, a comparação é feita somente quando todos os componentes depen-dentes também forem executados.
Considere o exemplo da Figura 2.2.2. Suponha que o Componente A é um compo-nente mutante. Se a mutação forte for utilizada, os resultados produzidos pela execução do Componente A e do Componente B devem ser considerados na análise de mutantes. Mas, na mutação fraca, os resultados produzidos após a execução do Componente A (componente mutante) são ignorados, assim torna-se desnecessária a execução dos com-ponentes que são executadas após a execução do componente mutante através da técnica mutação fraca. Portanto, na mutação fraca são comparadas somente as saídas produzidas pelo uxo de execução que antecede o componente mutante.
Para calcular o escore de mutação não é necessário que todos os mutantes sejam executados contra todos os casos de teste, a menos que seja para ns acadêmicos. Para o cálculo do escore, apenas é preciso saber a quantidade de mutantes mortos e a quantidade de mutantes não equivalentes gerados. Algumas ferramentas de teste, que implementam
tempo gasto durante a execução dos mutantes. A abordagem propõe que cada mutante precisa ser morto por apenas um caso de teste do conjunto [12]. Logo, se um mutante é morto por algum caso de teste, então este não será executado contra o restante dos casos de teste do conjunto.
Um recurso muito utilizado por alguns algoritmos visando melhorar o desempenho é o paralelismo. Como a execução de cada mutante é totalmente independente, muitos trabalhos dividem a tarefa de executar mutantes em tarefas paralelas [47]. Estas tarefas podem ser delegadas a várias unidades de processamento, que podem estar distribuídas em rede ou mesmo presentes na plataforma local de computação.
Teste Baseado em Modelo de Estados
Neste capítulo serão apresentados os conceitos sobre testes baseados em modelo, a deni-ção de alguns dos modelos formais (as máquinas de transideni-ção de estados) mais utilizados em desenvolvimento de software, e uma descrição do algoritmo meta-heurístico multi-objetivo para geração de casos de teste a partir de modelos de estados estendidos.
3.1 Teste Baseado em Modelo
Programadores frequentemente utilizam modelos para construir seus códigos. Modelos são representações abstratas visando simplicidade e compreensão do sistema. O que se espera é que os modelos utilizados possam ser executados, ou efetivamente ou simbolicamente.
Se modelos de software são frequentemente utilizados para concretizar a implemen-tação do software, então utilizando desses modelos é possível testar, com intenção de vericar a conformidade da implementação concreta com os modelos de software que a derivou [42].
Existem vários tipos de modelos, desde especicações informais de software a até especicações formais do comportamento do software [49, p. 63-67]. Os diferentes níveis de abstração podem ser observados em diferentes tipos de modelos. A utilização de modelagem de software auxilia todo o processo de desenvolvimento, suprindo a distância que há entre o domínio do problema e a solução do problema. Neste contexto há modelos que se aproximam mais do domínio do problema, e há modelos que se aproximam mais da implementação concreta do sistema [20].
Idealmente os modelos devem ser completos, não ambíguos e não contraditórios, os quais devem explicitamente representar o que realmente o sistema deverá fazer [44]. As-sim, é possível que testes sejam derivados automaticamente de modelos de software, fa-zendo com que a atividade de teste se torne menos onerosa.
Baseado nos modelos as partes criteriosamente selecionadas são utilizadas para ex-tração de casos de teste. Quanto maior a absex-tração do modelo, mais distante este é de sua implementação concreta, o que signica que mais abstrato também serão os casos de teste derivados a partir dele. Nestes casos, uma transformação faz-se necessária para que os casos de teste derivados a partir dos modelos de software sejam executados contra a implementação concreta do sistema [42].
Figura 3.1: Processo de teste baseado em modelo, uxograma.
3.1.1 O Processo de Teste Baseado em Modelo
O teste baseado em modelo tem o objetivo de automatizar a geração de casos de teste. O processo consiste em derivar requisitos em modelos abstratos de teste e a partir destes modelos gerar casos de teste para um sistema o qual deve ser desenvolvido baseado nos requisitos. Os requisitos podem ser modelos informais ou modelos formais de especicação [53, p. 6-10].
Na prática, o processo tende a reduzir custos com a atividade de teste, no qual todo o código de testes produzido manualmente por testadores seria não mais necessário, pois toda a geração, execução e análise de teste seriam realizadas automaticamente. O único trabalho do testador neste caso é o de estudar os requisitos para desenvolver um modelo comportamental do sistema, um modelo não tão completo quanto o sistema em si, mas com detalhes sucientes para se produzir um bom conjunto de casos de teste.
A Figura 3.1 apresenta um uxograma adaptado encontrado em [53, p. 27], para o processo de teste baseado em modelo.
O processo tem início com a derivação em um modelo comportamental a partir dos requisitos do sistema. O modelo comportamental é um modelo abstrato, o qual não deve ser rico em detalhes de programação, mas deve fornecer apenas informações sucientes sobre o comportamento do sistema e saídas esperadas para que a geração automática de casos de teste possa produzir bons conjuntos de casos de teste.
O segundo passo é a partir de um critério de teste escolhido gerar casos de teste abstratos. Casos de teste abstratos são casos de teste que não podem ser executados contra o sistema sob teste diretamente sem que uma transformação seja realizada (concretização
a nível de código).
O terceiro passo do processo é a partir dos casos de teste abstratos gerar um roteiro de teste, e em seguida adaptar o roteiro para que os casos de teste possam ser executados contra o sistema sob teste. Neste ponto já é possível então aplicar o roteiro de teste executando este contra o sistema.
Assim, o quarto passo então é executar os casos de teste executáveis (gerados a partir do terceiro passo), vericando se as saídas produzidas pelo sistema correspondem às saídas esperadas. O resultado de teste então corresponde à associação dos casos de teste, suas saídas esperadas com as saídas produzidas.
Por último, uma análise é feita vericando quantos casos de teste passaram e quan-tos falharam. Um caso de teste passa quando sua saída esperada corresponde à saída produzida, e falha quando sua saída esperada não corresponde à saída produzida. Outra possibilidade é de que os resultados são inconclusivos, o que signica que nenhuma decisão pode ser tomada baseada nos resultados apresentados.
3.1.2 A Taxonomia
O trabalho de Utting et. al. [54] fornece uma perspectiva detalhada dos vários conceitos envolvidos no teste baseado em modelo. A taxonomia proposta identica sete diferentes dimensões do teste baseado em modelos, e pode ser utilizada para classicar e avaliar abordagens e ferramentas correlacionadas.
A Figura 3.2 apresenta a taxonomia proposta por Utting et. al. [54], e os parágrafos a seguir descrevem as sete diferentes dimensões apresentadas no artigo deles.
A primeira dimensão é o sujeito do modelo, o qual corresponde ao comportamento esperado do sistema sob teste. O modelo do sistema sob teste pode ser necessário como artefato para geração de casos de teste, e como oráculo.
A segunda dimensão diz respeito ao quanto de redundância existe em aplicar o teste baseado em modelo. A taxonomia descreve dois possíveis cenários de aplicação, o primeiro no qual o modelo é utilizado tanto para gerar casos de teste quanto para gerar o código (modelo compartilhado), o segundo no qual o modelo somente é utilizado para a geração dos casos de teste (modelo separado).
A terceira dimensão está relacionada com as características do modelo. Os mode-los podem ser não determinísticos ou determinísticos, podem conter restrições de tempo associadas, podem ser de eventos contínuos ou discretos, entre outros.
A quarta dimensão é o paradigma do modelo, o qual implica também em qual notação utilizar para modelar. Por exemplo, modelos baseados em transições (FSM, EFSM, entre outros) em contraste com modelos baseados em estado (Z, B, entre outros) requerem diferentes tipos de notações.
A quinta dimensão é o critério de teste. O critério de teste na maioria dos casos pode denir e classicar uma ferramenta de teste pelo critério de seleção dos testes utilizado.
A sexta dimensão diz respeito à tecnologia utilizada pelo gerador de casos de teste. Um exemplo de tecnologia utilizada por geradores de casos de teste é a execução simbólica, a qual executa simbolicamente o modelo através de algoritmos de busca em grafos.
capaz de executar as possíveis soluções de casos de teste durante a geração. Existem duas classicações: on-line e o-line. A geração de casos de teste on-line assume que o gerador de casos de teste é capaz de executar as soluções de casos de teste e tomar decisões baseadas nos resultados, dinamicamente. Por outro lado, a geração de casos de teste o-line assume que o gerador de casos de teste não é capaz de executar o sistema sob teste, portanto os casos de teste são gerados antes de qualquer execução do modelo.
3.1.3 Limitações
O principal desao do teste baseado em modelo é denir diretrizes sistemáticas do como construir o modelo abstrato de teste. O sucesso ou fracasso do teste depende do quão bom e do quão suciente em detalhes é o modelo abstrato de teste. Tal sucesso frequentemente está correlacionado a habilidade e o conhecimento do testador na construção de modelos e na implementação de sistemas [54].
Segundo Jöbstl [31], o teste baseado em modelo acresce esforço ao teste de software tradicional com a construção, a validação e a manutenção do modelo. Além disso, para Jöbstl [31] a análise de um teste que falha é um pouco mais complexa, pois não existe apenas uma única fonte de causa da ocorrência da falha, a qual pode vir ou do sistema testado, ou do modelo, ou da própria implementação.
3.2 Modelos de Estado
Modelos comportamentais são utilizados para representar as características de um sistema quando em execução. Modelos de estados evoluíram desde simples máquinas com poder de representação apenas do uxo de controle de uma aplicação, a até máquinas mais complexas, com poder de representação do uxo de controle e do uxo de dados de uma aplicação [49, p. 93].
Um modelo de estado possui estados e transições. Cada estado responde a estímulos internos e externos, que podem ser tanto dados quanto eventos. Dependendo do tipo de modelo de estados, podem haver ou não uma condição de guarda nas transições, a qual corresponde a uma condição lógica que deve ser verdadeira para que ocorra a transição [19].
Diferentemente dos modelos informais não estruturados, os modelos de máquina de estados possuem uma sintaxe muito bem denida e estruturada, também conhecidos como modelos formais de representação comportamental de sistema, permitem a aplicação sis-temática de técnicas de validação e a geração automática de casos de teste [18].
Durante as últimas décadas os modelos de software evoluíram proporcionalmente à complexidade dos sistemas desenvolvidos. Sistemas de pequeno porte se tornaram sis-temas de grande porte, com muitas funcionalidades e muita responsabilidade envolvida durante sua operação. Sistemas que oferecem riscos a saúde, a vida e ao meio ambiente devem passar conança aos seus usuários, por isso necessitam de uma atenção maior em sua construção [45]. Logo, muitas técnicas formais foram introduzidas durante algumas das fases de desenvolvimento com intenção de melhorar a conabilidade no produto de software. Uma outra característica que faz com que os modelos de máquina de estados
o próprio código fonte, apenas com o uso de algumas regras de transformação [21, p. 338-348].
3.2.1 Máquina de Estados Finita
Uma máquina de estados nita (MEF) ou autômato nito é um modelo matemático que pode ser visto como composto por três componentes abstratas: uma ta que contêm as entradas a serem processadas, uma unidade de controle que determina o estado atual da máquina, e um programa ou função de transição que, de acordo com a entrada lida e o estado atual da máquina, determina o próximo estado. Geralmente, autômatos nitos possuem limitações práticas. Um fator que contribui para que isto ocorra é que os esta-dos nais da máquina conseguem apenas representar duas situações: se um conjunto de entradas é válido ou não [36, p. 33].
Denição 3.2.1 Autômato Finito. Um autômato nito é uma 5-upla:
M = (E, S, s0, T, F ) , onde:
E conjunto de símbolos de entrada; S conjunto nito de estados do autômato; s0 estado inicial, tal que s0 é um elemento de S;
F conjunto de estados nais, tal que F está contido em S; T função programa ou função de transição: T : S × E → S;
Com intenção de estender este conceito básico, os autômatos nitos incluíram também a geração de saídas associadas aos estados (máquina de Moore), ou às transições (máquina de Mealy).
Denição 3.2.2 Máquina de Mealy.
Uma máquina de Mealy é uma 6-upla, com saídas associadas às transições: M = (E, S, s0, T, F, ∆)
,onde:
E conjunto de símbolos de entrada; S conjunto nito de estados da máquina;
s0 estado inicial, tal que s0 é um elemento de S;
F conjunto de estados nais, tal que F está contido em S;
T função de programa ou função de transição: T : S × E → Sx∆∗;
∆ conjunto de símbolos de saída. Denição 3.2.3 Máquina de Moore
Uma máquina de Moore é uma 7-upla, com saídas associadas aos estados: M = (E, S, s0, T, F, ∆, Q)
,onde:
E conjunto de símbolos de entrada; S conjunto nito de estados da máquina;
s0 estado inicial, tal que s0 é um elemento de S;
F conjunto de estados nais, tal que F está contido em S; T função de programa ou função de transição: T : S × E → S; ∆ conjunto de símbolos de saída;
Q função de saída: Q : S → ∆∗ (função total).
Uma máquina determinística só pode estar em um estado por vez, deste modo, não é possível que dois estados estejam ativos ao mesmo tempo. Em vários trabalhos os termos máquina de estados nita e máquina de Mealy são tipicamente considerados um mesmo modelo.
Os autômatos nitos são capazes de modelar uma grande quantidade de sistemas reais, tal como representar protocolos de comunicação de rede e circuitos lógicos. Entretanto, embora sejam capazes de representar sistemas complexos, como uma rede neural cere-bral, a quantidade de estados utilizados é exponencialmente enorme. Então, torna-se impraticável a representação de tais sistemas através de MEF [13, p. 27-45].
3.2.2 Máquina de Estados Finita Estendida
Máquina de estados nita estendida (MEFE) é uma derivação da máquina de estados nita. A MEFE basicamente contêm estados, transições e variáveis locais. Transições são componentes do modelo que fazem a conexão de um estado para outro. Cada transição obrigatoriamente deve conter um evento associado. As guardas contidas nas transições são condições lógicas, que quando verdadeiras, permitem que ocorra uma transição entre estados. As variáveis locais armazenam dados, e estes podem ser utilizados pelas guardas
de dados internos ou externos. Dados externos são passados através dos parâmetros de uma transição [44].
Devido à limitação prática da MEF (explosão combinatória de estados), a MEFE foi proposta com intenção de auxiliar na representação de sistemas complexos [3, 105-106]. Denição 3.2.4 Máquina de estados nita estendida.
Uma máquina de estados nita estendida é uma 8-upla: M = (E, S, s0, F, T, ∆, V, P )
E conjunto de símbolos de entrada, também conhecido como conjunto de eventos de entrada;
S conjunto nito de estados da máquina;
s0 estado inicial, tal que s0 é um elemento de S;
F conjunto de estados nais, tal que F está contido em S; ∆ conjunto de símbolos de saída;
V conjunto de variáveis;
P conjunto de parâmetros de entrada;
T conjunto de transições, tal que toda transição t pertencente a T é uma 5-upla: t = (o, d, e, g, A) , onde: o estado de origem; d estado de destino; e símbolo de entrada; g condição de guarda; A conjunto de ações.
Existem outros modelos com maior poder de representação, que são derivações da MEFE. Por exemplo, o modelo de máquina de estados proposto por Harel [24], nomeado Statecharts, contêm mecanismos de história, paralelismo, hierarquia e broadcasting. O modelo Statecharts serviu de base para o modelo de máquina de estados da Unied Mo-deling Language (UML), uma linguagem de modelagem visual de propósito geral que é utilizada para especicar, visualizar, construir e documentar artefatos de um sistema de software [46, p. 3].
3.2.3 A Máquina de Estados do SMC
Neste trabalho foi utilizado um modelo de máquina de estados o qual estende a MEFE apresentada na seção 3.2.2. A máquina de estados utilizada pelo SMC (State Machine Compiler, traduzido como Compilador de Máquina de Estados) possui alguns elementos e mecanismos de uma máquina de estados da UML, pode-se então dizer que esta é um sub-conjunto dos diagramas de estados da UML. Nesta seção serão apresentados os elementos e os mecanismos da máquina de estados do SMC.
Na seção 3.2.2 foram apresentados os elementos de uma MEFE através de uma deni-ção formal. Os mesmos elementos, como estados, transições, ações, guardas, entre outros, também são elementos da máquina de estados do SMC. Os elementos e mecanismos não mencionados são: a pilha de chamada, as transições loopback, e a hierarquia.
Na máquina do SMC são permitidas transições de estados entre máquinas, o que signica que podem existir máquinas comunicantes (submáquinas). Isto é possível através da diretiva push da linguagem de modelagem do SMC. Deste modo, em uma transição do modelo pode haver, além da transição comum de um estado da máquina para outro estado, uma transição adicional do estado origem para outro estado de outra máquina. O estado destino da transição ca aguardando uma resposta da submáquina. As respostas são disparadas através da diretiva pop.
Consequentemente, a comunicação entre as máquinas cria uma espécie de "pilha de execução", onde no topo da pilha está a máquina correntemente em execução, e no m da pilha a primeira máquina executada. Os elementos push e pop são responsáveis por criar a pilha de chamadas, e através deles é que torna-se possível desenvolver o mecanismo de hierarquia na máquina do SMC.
Outro elemento importante é a transição loopback. A transição loopback é um self-loop (estado inicial e estado nal da transição são iguais) interno ao estado, ou seja, a transição realizada em uma transição loopback não caracteriza uma saída e entrada de estado. Assim, os eventos de entrada e de saída não são disparados quando uma transição loopback é exercitada no modelo.
As Figuras 3.3 e 3.4 ilustram duas submáquinas comunicantes na notação visual da ferramenta StateMutest, apresentada no Capítulo 5. A Figura 3.3 corresponde à submá-quina chamada map0, a Figura 3.4 corresponde à submásubmá-quina chamada map1. O estado inicial é o estado Desligado da submáquina map0. O estado Desligado responde a dois eventos: o evento ligar e o evento aguardar. A transição com o evento aguardar é uma transição loopback, representada pelo contorno em torno da transição. A transição com o evento ligar, quando ativada, realiza a transição para o estado Ligado e em seguida passa o controle para o estado Iniciar da submáquina map1. A submáquina map0 ca aguardando a submáquina map1 retornar um evento através de um pop.
A descrição completa da linguagem de modelagem da máquina de estados apresen-tada nesta seção, assim como a descrição de todos os seus elementos, os guias e a do-cumentação, podem ser encontrados ao acessar o site ocial da biblioteca, disponível em http://smc.sourceforge.net/.
Figura 3.3: Submáquina chamada de map0. Modelo de MEFE do SMC.
3.3 O Método MOST
A geração de sequências de teste guiada pelo teste baseado em busca multiobjetivo (do inglês Multi-Objective Search-based Testing, MOST), proposto em [55], auxilia a atividade de teste através da geração automática de sequências de entrada para modelos de máquina de estados nitos estendida (MEFE). O método faz uso de dois conceitos: teste baseado em modelo e teste baseado em busca. Neste contexto, o MOST deriva as sequências automaticamente a partir de modelos, utilizando um algoritmo meta-heurístico.
Para executar o MOST, além do modelo descrito em MEFE, é necessário também um modelo executável da MEFE. O modelo executável é utilizado visando evitar a geração de sequências de entrada para caminhos infactíveis.
As etapas básicas do método consistem em: denir uma especicação M em MEFE, gerar o modelo executável de M e validar M, pré-processar M, gerar as sequências de entrada (eventos e dados paramétricos) e avaliar os caminhos gerados.
A especicação sob teste deve estar de acordo com os requisitos do sistema. Esta é uma etapa que depende muito da habilidade e competência dos projetistas. Após a especica-ção ser validada, um modelo executável é gerado a partir dela. Com o modelo executável, é possível de se obter dinamicamente os caminhos de transições disparados pelos eventos e dados paramétricos gerados pelo MOST durante a execução do algoritmo. Assim, é possível descobrir quais caminhos são factíveis e quais são infactíveis, a m de evitar os caminhos infactíveis. Um caminho infactível é um caminho que existe sintaticamente no modelo, mas que, não pode ser exercitado semanticamente. Algumas transições podem conter uma condição de guarda, e condições de guarda podem utilizar-se dos valores das variáveis e dos parâmetros de entrada do modelo. Para uma transição ser ativada, a con-dição de guarda deve ser satisfeita. Se o contexto do modelo restringe que um requisito de teste seja satisfeito, diz-se que o caminho é infactível, ou seja, uma transição alvo não pode ser exercitada devido ao contexto do modelo.
Antes de iniciar a etapa de geração, é realizado um pré-processamento, o qual visa fornecer ao algoritmo de geração as informações necessárias para sua inicialização. O MOST realiza uma análise de dependência das transições do modelo na etapa de pré-processamento do modelo. Dois conjuntos são fornecidos para cada transição: o conjunto das transições que dependem de controle e/ou dados (TA), e o conjunto das transições críticas, transições que desviam o uxo do caminho alvo (TC ). Estes dois conjuntos são utilizados pelo MOST. Os caminhos que contêm transições em TA são graticados, e tendem a estar próximos à solução nal, enquanto que os caminhos que contêm transições em TC são penalizados.
A última etapa consiste na geração das sequências de entrada, com auxílio de um algo-ritmo meta-heurístico. O algoalgo-ritmo meta-heurístico utilizado pelo MOST é um algoalgo-ritmo evolutivo denominado M-GEOvsl. São utilizadas duas funções objetivo no M-GEOvsl: uma função cujo objetivo é garantir a cobertura da transição alvo e das transições em TA (F1 ), outra função cujo objetivo é encontrar sequências de entrada de tamanho mínimo ótimo (F2 ).
Com os objetivos denidos em F1 e F2, as sequências de entrada para o modelo são geradas, as quais exercitam somente caminhos factíveis de tamanho mínimo ótimo.
F2 tem também a função de reduzir custos computacionais através do MOST. Maiores detalhes sobre o MOST podem ser encontrados em [55].