• Nenhum resultado encontrado

Uma abordagem para a verificação do comportamento excepcional a partir de regras de designe e testes

N/A
N/A
Protected

Academic year: 2017

Share "Uma abordagem para a verificação do comportamento excepcional a partir de regras de designe e testes"

Copied!
159
0
0

Texto

(1)

Universidade Federal do Rio Grande do Norte Centro de Ciências Exatas e da Terra

Departamento de Informática e Matemática Aplicada Programa de Pós-Graduação em Sistemas e Computação

UMA ABORDAGEM PARA VERIFICAÇÃO DO

COMPORTAMENTO EXCEPCIONAL A PARTIR DE

REGRAS DE DESIGN E TESTES

RICARDO JOSÉ SALES JÚNIOR

Dissertação apresentada ao Programa de Pós-Graduação em Sistemas e Computação do Departamento de Informática e Matemática Aplicada da Universidade Federal do Rio Grande do Norte como requisito parcial para a

obtenção do grau de Mestre em Sistemas e Computação.

(2)

Universidade Federal do Rio Grande do Norte Centro de Ciências Exatas e da Terra

Departamento de Informática e Matemática Aplicada Programa de Pós-Graduação em Sistemas e Computação

UMA ABORDAGEM PARA VERIFICAÇÃO DO

COMPORTAMENTO EXCEPCIONAL A PARTIR DE

REGRAS DE DESIGN E TESTES

Ricardo José Sales Júnior

Dissertação apresentada ao Programa de

Pós-Graduação em Sistemas e

Computação do Departamento de

Informática e Matemática Aplicada da Universidade Federal do Rio Grande do Norte como requisito parcial para a obtenção do grau de Mestre em Sistemas e Computação.

Profª. Drª. Roberta de Souza Coelho Orientadora

(3)

Catalogação da Publicação na Fonte. UFRN / SISBI / Biblioteca Setorial Especializada do Centro de Ciências Exatas e da Terra – CCET. Sales Júnior, Ricardo José.

Uma abordagem para verificação do comportamento excepcional a partir de regras de design e testes / Ricardo José Sales Júnior. – Natal, RN, 2013.

133 f. : il.

Orientadora: Profa. Dra. Roberta de Souza Coelho.

Dissertação (Mestrado) – Universidade Federal do Rio Grande do Norte. Centro de Ciências Exatas e da Terra. Departamento de Informática e Matemática Aplicada. Programa de Pós-Graduação em Sistemas e Computação.

1. Software – Teste - Dissertação. 2. Tratamento de exceções – Dissertação. 3. Comportamento excepcional – Dissertação. 4. Regras de design – Dissertação. I. Coelho, Roberta de Souza. II. Título.

(4)
(5)

Dedico essa dissertação a Jeová Deus, a

minha Esposa Raquel, meus pais e amigos que

(6)

Agradecimentos

(7)

Resumo

Verificar a conformidade entre a implementação de um sistema e suas regras de design é uma atividade importante para tentar garantir que não ocorra a degradação entre os padrões arquiteturais definidos para o sistema e o que realmente está implementado no código-fonte. Especialmente no caso de sistemas dos quais se exige um alto nível de confiabilidade é importante definir regras de design (design

rules) específicas para o comportamento excepcional. Tais regras descrevem como as exceções devem fluir através do sistema, definindo quais são os elementos responsáveis por capturar as exceções lançadas por outros elementos do sistema. Entretanto, as abordagens atuais para verificar automaticamente regras de design não proveem mecanismos adequados para definir e verificar regras de design específicas para a política de tratamento de exceções das aplicações. Este trabalho propõe uma abordagem prática para preservar o comportamento excepcional de uma aplicação ou família de aplicações, baseada na definição e verificação automática em tempo de execução de regras de design de tratamento de exceção para sistemas desenvolvidos em Java ou AspectJ. Para apoiar esta abordagem foi desenvolvida, no contexto deste trabalho, uma ferramenta chamada VITTAE (Verification and

Information Tool to Analyze Exceptions) que estende o framework JUnit e permite automatizar atividades do teste de regras de design excepcionais. Foi realizado um estudo de caso preliminar com o objetivo de avaliar a eficácia da abordagem proposta sobre uma linha de produto de software. Além deste, foi realizado um experimento cujo objetivo foi realizar uma análise comparativa entre a abordagem proposta e uma abordagem baseada na ferramenta JUnitE, que também propõe testar o código de tratamento de exceções utilizando testes JUnit. Os resultados mostraram que as regras de design excepcionais evoluem ao longo de diferentes versões de um sistema e que a VITTAE pode auxiliar na detecção de defeitos no código de tratamento de exceção.

Palavras-chave: Tratamento de Exceções, Comportamento Excepcional, Testes de

(8)

Abstract

Checking the conformity between implementation and design rules in a system is an important activity to try to ensure that no degradation occurs between architectural patterns defined for the system and what is actually implemented in the source code. Especially in the case of systems which require a high level of reliability is important to define specific design rules for exceptional behavior. Such rules describe how exceptions should flow through the system by defining what elements are responsible for catching exceptions thrown by other system elements. However, current approaches to automatically check design rules do not provide suitable mechanisms to define and verify design rules related to the exception handling policy of applications. This paper proposes a practical approach to preserve the exceptional

behavior of an application or family of applications, based on the definition and runtime automatic checking of design rules for exception handling of systems developed in Java or AspectJ. To support this approach was developed, in the context of this work, a tool called VITTAE (Verification and Information Tool to Analyze Exceptions) that extends the JUnit framework and allows automating test activities to exceptional

design rules. We conducted a case study with the primary objective of evaluating the effectiveness of the proposed approach on a software product line. Besides this, an experiment was conducted that aimed to realize a comparative analysis between the proposed approach and an approach based on a tool called JUnitE, which also proposes to test the exception handling code using JUnit tests. The results showed how the exception handling design rules evolve along different versions of a system and that VITTAE can aid in the detection of defects in exception handling code.

(9)

Sumário

1 Introdução ... 14

1.1 Exemplo Motivacional ... 17

1.2 Limitações das abordagens atuais ... 21

1.3 Solução proposta ... 22

1.4 Objetivos ... 22

1.5 Contribuições escritas ao longo da pesquisa ... 23

1.6 Organização do texto ... 23

2 Fundamentação Teórica ... 25

2.1 Tolerância a falhas ... 25

2.1.1 Tratamento de exceções ... 26

2.1.2 Tratamento de exceções em Java ... 26

2.2 Verificação de Software ... 27

2.2.1 Análise estática ... 28

2.2.2 Testes de Software ... 28

Testes funcionais (ou testes caixa-preta) ... 29

Testes estruturais (ou testes caixa-branca) ... 29

2.2.3 Níveis de teste ... 30

2.3 Automação de testes ... 31

2.3.1 JUnit ... 32

2.4 Programação Orientada a Aspectos e AspectJ ... 36

2.5 Regras de Design ... 37

2.6 Discussões ... 38

3 Abordagem Proposta ... 39

3.1 Exemplo Motivador – Refatoração de Sistemas Orientados a Objetos para Aspectos... 39

3.2 Visão Geral da Abordagem Proposta ... 43

3.2.1 Passo1: Definição das Regras de Design Excepcionais da Aplicação ... 44

3.2.2 Passo 2 : Projeto e Automação do Teste ... 47

3.2.3 Passo 3 : Execução do teste ... 49

(10)

3.3 Discussões ... 52

4 A Ferramenta VITTAE ... 54

4.1 Visão Geral da Arquitetura da VITTAE ... 54

4.2 Pacote de aspectos ... 54

4.3 Pacote de controladores ... 56

4.4 Pacote de exceções ... 56

4.5 Pacote de classes utilitárias ... 56

4.6 Pacote de geradores ... 57

4.7 Pacote de entidades ... 57

4.8 Verificação da regra de design ... 58

4.9 Interface gráfica ... 59

4.10 Discussões ... 59

5 Estudo de Caso ... 61

5.1 Mobile Media (MM) ... 61

5.2 Aplicando a Abordagem a uma Linha de Produto de Software ... 63

5.3 Discussões ... 67

6 Estudo avaliativo Comparação entre VITTAE e JUnitE ... 70

6.1 JUnitE ... 71

6.2 Avaliação quantitativa ... 73

6.2.1 Definição do experimento ... 74

6.2.2 Hipóteses ... 75

6.2.3 Projeto do experimento ... 76

6.2.3.1 Escolha dos sujeitos ... 77

6.2.3.2 Escolha dos sistemas ... 77

6.2.3.3 Organização do experimento ... 80

6.2.3.4 Operação do experimento ... 81

6.2.4 Análise dos resultados ... 86

6.2.5 Ameaças à validade do estudo ... 102

6.3 Avaliação qualitativa ... 102

6.3.1 Ameaças à validade do estudo ... 108

(11)

7 Trabalhos relacionados ... 110

7.1 Abordagens de verificação estática ... 110

7.2 Abordagens de teste ... 111

7.3 Abordagens de regras de design para tratamento de exceção ... 113

7.4 Processos de Desenvolvimento ... 115

7.5 Discussões ... 116

8 Considerações Finais ... 117

8.1 Publicações relacionadas ... 118

8.2 Trabalhos futuros ... 119

Referências ... 121

Apêndice A Artigos Publicados ... 131

(12)

Lista de Figuras

Figura 1: Arquitetura de um sistema baseada em camadas ... 18

Figura 2: Técnica de Teste Funcional ... 29

Figura 3:Técnica de teste estrutural ... 30

Figura 4: Modelo V que descreve a correspondência entre as atividades de desenvolvimento e os níveis de teste ... 30

Figura 5: Arquitetura do JUnit (Fonte: http://www.devmedia.com.br/junit-implementando-testes-unitarios-em-java-parte-i/1432) ... 32

Figura 6: Resultados visuais do JUnit ... 35

Figura 7: Arquitetura de um sistema refatorado para Aspectos ... 40

Figura 8: Visão esquemática do "ladrão de exceções" ... 42

Figura 9: Sequência de Atividades da Abordagem Proposta ... 44

Figura 10: XML Schema para o arquivo que define as regras de design da aplicação ... 46

Figura 11: Estrutura do projeto com a VITTAE e localização do arquivo de contratos ... 47

Figura 12: Tela do JUnit sem a extensão da VITTAE ... 50

Figura 13: Exemplo de utilização do framework JUnit em conjunto com a VITTAE para execução automatizada dos testes ... 50

Figura 14: Arquitetura da Vittae ... 55

Figura 15: Inicialização do aspecto de checagem ... 58

Figura 16: Diagrama de sequência para verificação da regra de design ... 59

Figura 17: Interface gráfica da VITTAE para configuração do projeto eclipse ... 60

Figura 18: Janela da VITTAE para escolha do projeto eclipse a ser configurado ... 60

Figura 19: Design OO da MobileMedia ... 62

Figura 20: Design AO da MobileMedia ... 62

Figura 21: Diagrama de classes e pacotes com a estrutura do Sistema Bancário .... 78

Figura 22: Diagrama de classes e pacotes com a estrutura do Sistema de Fichas .. 79

Figura 23:Resultado do teste na VITTAE ... 85

Figura 24: Resultado do teste na JUnitE ... 85

Figura 25: Gráfico Box-Plot para os dados da Q1 ... 89

(13)

Figura 27: Comparação visual entre os dados obtidos na Q1 e o modelo predito no

gráfico de envelope ... 91

Figura 28: Gráfico Box-Plot para os dados da Q2 ... 93

Figura 29: Resultados individuais para os dados da Q2 ... 94

Figura 30: Comparação visual entre os dados obtidos na Q2 após a transformação e o modelo predito no gráfico de envelope. ... 95

Figura 31: Gráfico em barras para a Q4 ... 98

Figura 32: Gráfico Box-Plot para a Q5 ... 99

Figura 33: Resultados individuais para os dados da Q5 ... 100

Figura 34: Comparação visual entre os dados obtidos na Q5 após a transformação e o modelo predito no gráfico de envelope. ... 101

Figura 35: Gráfico em barras com o resultado da P5 ... 104

Figura 36: Gráfico em barras com o resultado da P6 ... 104

Figura 37: Gráfico em barras com o resultado da P7 ... 104

Figura 38: Gráfico em barras com o resultado da P8 ... 105

Figura 39: Gráfico em barras com o resultado da P9 ... 105

Figura 40: Gráfico Box-Plot referente ao tempo para a configuração do ambiente para as duas ferramentas... 107

(14)

Lista de Tabelas

Tabela 1: Métricas da MobileMedia ... 61

Tabela 2: Exemplos de Contratos de Tratamento de Exceções da MobileMedia ... 64

Tabela 3:Resultados da execução da abordagem da MobileMedia em diferentes versões ... 69

Tabela 4: Comparação de características entre a VITTAE e JUnitE ... 73

Tabela 5: Questões relacionadas ao objetivo do experimento ... 75

Tabela 6: Métricas relacionadas com as questões levantadas ... 75

Tabela 7: Hipóteses baseadas nas questões ... 76

Tabela 8: Comparação de características entre os dois sistemas ... 79

Tabela 9: Organização dos sujeitos no experimento utilizando o delineamento em quadrado latino ... 81

Tabela 10 Relação entre as fases do experimento e os dias em que elas foram executadas ... 81

Tabela 11: Regra de Design Excepcional de cada sistema ... 82

Tabela 12: Resultado esperado das regras de design descritas nas ferramentas .... 83

Tabela 13: Lista de perguntas do questionário e a relação delas com as questões levantadas para o experimento ... 86

Tabela 14: Dados obtidos para a Q1 ... 88

Tabela 15: Mediana e Desvio padrão dos dados da Q1 ... 89

Tabela 16: Resultados do teste ANOVA para os dados da Q1 ... 92

Tabela 17: Dados obtidos para a Q2 ... 92

Tabela 18: Mediana e Desvio padrão dos dados da Q2: ... 93

Tabela 19: Dados da Q2 após transformação Box-Cox ... 95

Tabela 20: Resultados do teste ANOVA para os dados da questão 2 ... 96

Tabela 21: Dados obtidos para a Q3 ... 96

Tabela 22: Dados obtidos para a Q4 ... 97

Tabela 23: Dados obtidos para a Q5 ... 99

Tabela 24: Mediana e Desvio padrão dos dados da Q5: ... 99

Tabela 25: Dados da Q5 após transformação Box-Cox ... 101

Tabela 26: Resultados do teste ANOVA para os dados da questão 2 ... 102

(15)
(16)

1 Introdução

Na atualidade, os sistemas resultantes do desenvolvimento de software geralmente são compostos por uma coleção de componentes distribuídos que precisam lidar com entradas provenientes de uma variedade de fontes, executar em diversos tipos de ambientes e obedecer a requisitos rigorosos de confiabilidade. No que diz respeito aos requisitos de confiabilidade, diversas técnicas podem ser utilizadas, mas uma das mais conhecidas e que está embutida na maior parte das linguagens modernas de programação é a utilização de mecanismos de tratamento de exceção [Cristian 1982]. Estes mecanismos auxiliam os desenvolvedores a construir aplicações mais robustas através da separação do fluxo de comportamento excepcional do fluxo normal de controle [Parnas and Wurges 1976].

Estes mecanismos ajudam a garantir a modularidade do sistema na presença de erros, visto que eles oferecem abstrações para: (i) representar situações de erros em módulos do sistema como exceções; (ii) encapsular atividades de tratamento de exceção em entidades manipuladoras (handlers); (iii) definir partes dos módulos do sistema como regiões protegidas para a ocorrência do tratamento de exceções; (iv) associar essas regiões com os handlers; (v) especificar explicitamente as interfaces de tratamento de exceção dos módulos.

(17)

As construções para tratamento de exceções geralmente são propensas a falhas devido à falta de atenção com o comportamento excepcional durante outras disciplinas e atividades do desenvolvimento de software [Avizienis 1997]. Nesta realidade, mesmo que a intenção inicial seja utilizar o tratamento de exceções para melhorar a robustez dos sistemas, o uso indevido pode ser uma fonte de falhas. Um bom exemplo disso está na utilização da orientação a aspectos para modularizar o tratamento de exceções. Os trabalhos de Castor et al. [Castor et al. 2006], Coelho et al. [Coelho et al 2008a, Coelho et al 2008b] e Sales Júnior et al. [Sales Júnior et al. 2010] destacam que se determinados cuidados não forem tomados, a modularização do tratamento de exceções através de aspectos pode ser uma fonte de erros.

Para lidar com este tipo de problema, abordagens baseadas em análise estática foram propostas para descobrir falhas no código de tratamento de exceção [Coelho et al. 2008a, Fahndrich et al. 1998, Robiliard and Murphy 2003, Chang et al. 2001, Garcia et al. 2011]. Estas abordagens são baseadas em ferramentas que descobrem os caminhos que as exceções fazem a partir dos métodos que as lançam até os elementos responsáveis por capturá-las. Entretanto, devido às limitações inerentes às abordagens de análise estática e às características das linguagens de programação modernas, tais como herança, polimorfismo e chamadas virtuais, estas abordagens geralmente trazem um número muito grande de fluxos de exceção a serem analisados, sem contar os muitos falsos positivos, que nada mais são do que fluxos que na realidade não serão executados [Sinha and Harrold 2000]. Consequentemente, trabalho manual adicional deve ser realizado para verificar quando um fluxo excepcional detectado pode realmente acontecer, e quando ele realmente é importante. Esse trabalho adicional manual pode tornar tais abordagens tão caras em termos de tempo e recursos a ponto de serem na realidade proibitivas.

Além disso, uma vez que os fluxos de interesse são encontrados, tanto quanto é do nosso conhecimento, a única forma de documentá-los é através de um texto informal em linguagem natural. Ainda outra limitação destas abordagens é o fato de que elas somente podem ser utilizadas após pelo menos parte do sistema estar implementada.

(18)

documentação, impondo uma sobrecarga indesejável para ambientes em que se exige uma maior agilidade. A experiência de trabalhos como os de Misra, Kumar e Kumar [Misra, Kumar and Kumar 2009] tem mostrado que metodologias mais leves estão sendo usadas com mais sucesso no desenvolvimento de aplicações atualmente. Tais metodologias incentivam fortemente a automação dos testes, e a construção deles antecipadamente à implementação baseando-se nas especificações de requisitos, protótipos [Beck and Andres 2004, Palmer and Felsing 2002] e modelos arquiteturais.

Partindo do pressuposto que as regras de comportamento excepcional são padrões que devem ser obedecidos durante o desenvolvimento de um software, pode-se considerar então que elas são um tipo de regra de design [Baldwin and Clark 1999]. Uma das principais aplicações para utilização das regras de design é definir padrões arquiteturais que devem ser obedecidos durante a implementação e manutenção de um software. Tais padrões precisam ser estritamente seguidos em todas as fases do desenvolvimento de software. Atualmente estas regras de design, geralmente, são definidas e checadas manualmente. Porém essa atividade se torna demasiadamente custosa para sistemas de grande porte. Trabalhos como os de Brunet, Neto e Figueredo [Brunet, Neto and Figueredo 2009] e Neto [Neto 2010] apresentam abordagens para definir e automaticamente verificar as regras de design dos sistemas. Uma limitação comum dessas abordagens é que elas não proveem maneiras apropriadas de definir regras de design relacionadas com fluxos de exceção.

Pelas razões acima citadas verifica-se que são necessárias ferramentas que (i) ajudem os desenvolvedores a entender o impacto das mudanças na política de tratamento de exceções e (ii) encontrem inconformidades entre as regras de design e o código que está sendo incluído ou alterado.

(19)

Exceptions) que permite (i) verificar automaticamente através de testes unitários (estendendo o framework JUnit [JUnit 2012]) se as regras de design estão sendo obedecidas em sistemas Java [Java 2012] e AspectJ [AspectJ 2012], (ii) gerar parte do código dos testes de comportamento excepcional baseados nas regras de design, (iii) gerar também os objetos mock dinâmicos [Freese 2002] que simulam a instanciação de objetos de teste de comportamento excepcional e (iv) montar o ambiente no projeto Java para realização dos testes utilizando a ferramenta.

Nossa abordagem pode ser utilizada tanto para escrever as regras de design e os testes previamente às fases de desenvolvimento, usando o conceito de desenvolvimento dirigido a testes [Beck 2003], quanto posteriormente ao desenvolvimento, auxiliando nas atividades de manutenção.

Esta abordagem foi aplicada inicialmente em uma linha de produto de software [Clements and Northrop 2002] escrita em Java e AspectJ, a MobileMedia [Figueiredo et al. 2008], com o objetivo de avaliar sua eficácia em detectar a degradação das regras de design de uma aplicação durante sua evolução em diversas versões.

Adicionalmente foi realizado um experimento controlado, que comparou esta abordagem com a JUnitE [Di Bernardo et al. 2011], proposta também para especificação e verificação de fluxos de tratamento de exceções utilizando testes unitários.

1.1 Exemplo Motivacional

(20)

Figura 1: Arquitetura de um sistema baseada em camadas

Considere um sistema com arquitetura em camadas, como o exemplificado na Figura 1. Ele é composto pelas seguintes camadas:

 Dados (Data Layer)

 Negócio (Business Layer)

 Interface Gráfica (GUI Layer)

Uma regra de design de tratamento de exceções que deve ser obedecida nesta aplicação é a seguinte: as exceções lançadas por Objetos da camada de Dados (Data

Layer) representam problemas no acesso à base de dados. Elas são subtipos da exceção DAOException. Essas exceções devem ser tratadas pela Servlet na camada de interface gráfica (GUI Layer). Com o objetivo de verificar se esta política está sendo obedecida, o desenvolvedor pode tentar construir o seguinte caso de teste JUnit:

1. public void testDAOEHPolicy (){

2. Servlet s1 = new Servlet();

(21)

Se nenhuma instância de DAOException escapar deste método, o desenvolvedor pode presumir que a exceção foi adequadamente tratada pela Servlet. Entretanto, dois problemas podem acontecer, e estes não serão detectados por este teste:

 A instância de DAOException pode ter sido erroneamente tratada pelo

Facade que neste caso estaria atuando como um elemento intermediário entre o signaler e o handler definidos na regra de design.

 Durante o teste, o DAO pode não lançar a exceção, o que tornaria o teste sem efeito.

A construção do JUnit que permite verificar as exceções é a utilização da anotação @Test(expected=<NOME_DA_EXCEÇÃO>.class). Exemplificamos a utilização dessa construção no trecho de código a seguir, supondo agora uma outra situação onde o desenvolvedor deseja verificar se a Servlet lança uma exceção do tipo DAOException quando ocorre um erro no acesso aos dados.

1. @Test(expected=DAOException.class)

2. public void testDAOEHPolicy(){

3. Servlet s1 = new Servlet();

4. s1.service();

5. }

(22)

Outro cenário comum no contexto da utilização de orientação a aspectos, é o padrão de bug chamado “ladrão de exceções” [Coelho et al. 2008c]. Esse cenário ocorre quando um aspecto responsável por tratar uma exceção não consegue realizar a tarefa visto que um elemento do código base prematuramente capturou a exceção, geralmente também pelo abuso na utilização da cláusula catch-all.

Uma maneira de prevenir esses dois últimos problemas relatados seria a troca das cláusulas catch-all indevidamente utilizadas por cláusulas de catch específicas. No entanto, essa solução ainda é muito frágil, visto que depende de convenções de código. Além disso, nada impede que uma cláusula catch-all indevida possa ser inserida posteriormente durante uma manutenção de código.

O framework JUnit não provê uma maneira de verificar quando um elemento intermediário erroneamente trata uma exceção. A única maneira de realizar isso seria criar um teste para cada elemento intermediário, uma atividade que consome um tempo adicional muito grande e está sujeita a erros. Esta limitação é compreensível visto que o objetivo do JUnit é realizar testes unitários e não testes de tratamento de exceções. Não obstante, quando se trata de exceções, situações em que o tratamento de exceções é realizado incorretamente, como por exemplo uma propagação global de uma exceção inesperada, pode ser uma fonte significativa de bugs, como mostram os trabalhos de Coelho et al. [Coelho et al. 2008a] e Robillard e Murphy [Robillard and Murphy 2003]. Isso é particularmente percebido em linguagens de programação como Java e C#, onde exceções são objetos que podem ser capturados através da subsunção de tipos [Robillard and Murphy 2003], ou seja, um tratador para uma exceção E captura também exceções E’, onde E’ é um subtipo de E.

(23)

A linha de base desta dissertação é que existe a necessidade de uma abordagem que facilite o desenvolvimento de casos de teste para comportamento excepcional que permita que: (i) sejam definidas regras de design, especificando quais elementos (handlers) são responsáveis por tratar as exceções (exceptions) lançadas por outros elementos (signalers); (ii) essas regras sejam consideradas como uma documentação do tratamento de exceções do sistema; (iii) sejam construídos e executados testes que verifiquem se essas regras de design foram obedecidas;

1.2 Limitações das abordagens atuais

Como observado anteriormente, as abordagens baseadas em análise estática como as de Coelho et al. [Coelho et al. 2008a] e Garcia et al. [Garcia et al. 2011] descobrem todos os possíveis fluxos de exceção. Essas abordagens de análise estática possuem limitações como, por exemplo, o fato de que elas incluem uma massa de dados referentes aos fluxos de exceção muito grande a ser analisada, no caso de sistemas de grande porte, e podem retornar muitos falsos positivos, o que exige um trabalho manual adicional.

Outras abordagens, como por exemplo, as de Cacho et al. [Cacho et al. 2008] e Silva e Castor [Silva and Castor 2013] estendem as linguagens de programação Java e AspectJ para permitir que os desenvolvedores especifiquem os fluxos de exceções através da descrição de canais de exceção e verifiquem estaticamente esses fluxos. Essas abordagens compartilham das mesmas limitações das abordagens de análise estática visto que as exceções não são exercitadas em tempo de execução.

Também foi observado que os casos de teste JUnit puros não conseguem detectar problemas como por exemplo o bug “ladrão de exceções” e o tratamento indevido de exceções durante o seu fluxo no sistema.

As abordagens baseadas em documentação “pesada” como as de Rubira et al.

[Rubira et al 2005] e Kienzle [Kienzle 2008] vão de encontro à ideia de evitar uma sobrecarga nas atividades de desenvolvimento e ainda não permitem a verificação automática dos fluxos de exceção.

(24)

orientados a objetos e orientados a aspectos. Porém elas não possuem construções específicas para tratamento de exceções, ou as construções são por demais limitadas, para detectar as inconformidades alistadas na seção anterior.

1.3 Solução proposta

A solução proposta neste trabalho oferece uma abordagem baseada na definição de regras de design específicas para o comportamento excepcional em conjunto com a verificação dessas regras através de testes automáticos baseados no framework JUnit. Para isso é proposta uma sequência de atividades para a realização da abordagem, que são suportadas por uma ferramenta que é resultado desta dissertação, a VITTAE (Verification and Information Tool to Analyze Exceptions).

1.4 Objetivos

A dissertação tem como objetivo principal propor uma abordagem sistemática para a definição e checagem de regras de design voltadas para fluxos excepcionais. Além disso, o trabalho apresenta os seguintes objetivos específicos:

 Propor uma linguagem para definição das regras de design voltadas para fluxos excepcionais;

 Propor uma ferramenta que permita realizar a verificação automática das regras através de testes automatizados;

 Aplicar e avaliar a abordagem proposta através da sua aplicação em um sistema com várias versões;

(25)

1.5 Contribuições escritas ao longo da pesquisa

Parte dos resultados obtidos neste trabalho foram publicados nos seguintes artigos, que estão no Apêndice A:

 SALES JUNIOR, R. ; COELHO, R. S. Preserving the Exception Handling Design Rules in Software Product Line Context: A Practical Approach. In: I Workshop on Exception Handling in Contemporary Software Systems (EHCOS 2011), 2011. Proceedings of I Workshop on Exception Handling in Contemporary Software Systems (EHCOS 2011),, 2011

 SALES JUNIOR, R.; COELHO, R. S. ; LUSTOSA NETO, V. .Exception-Aware AO Refactoring. In: IV Latin American Workshop on Aspect Oriented Programming, 2010, Salvador. Anais do CBSoft, 2010.

 Di Bernardo, R., Sales Júnior, R., Castor, F. Coelho, R., Cacho, N., Soares S. Agile Testing of Exceptional Behavior . 25th Brazilian Symposium on Software Engineering (SBES 2011). São Paulo, September 28-30, 2011 DOI: 10.1109/SBES.2011.28.

1.6 Organização do texto

(26)
(27)

2 Fundamentação Teórica

O objetivo deste capítulo é apresentar uma fundamentação dos principais conceitos necessários para o entendimento do restante da dissertação. Inicialmente será apresentado o conceito de tolerância a falhas (seção 2.1). Outra parte importante, a verificação de software, bem como suas vertentes – testes e análise estática – receberão destaque na seção 2.2. Também será feita uma introdução breve a ideia de automação de testes (seção 2.3) e será apresentada a ferramenta de testes unitários para a Linguagem Java, o JUnit. Na seção seção 2.4 o paradigma de Programação Orientada a Aspectos será apresentado. Por fim, o conceito de Regras de Design será abordado na seção 2.5.

2.1 Tolerância a falhas

O termo tolerância a falhas [Avizienis 1998] tem sido utilizado pela comunidade acadêmica para designar a propriedade que permite que os sistemas, em geral computacionais, continuem a operar adequadamente mesmo com a presença de falhas. Porém, para entender melhor este termo é necessário entender os conceitos básicos de falha, falta e erro. Os conceitos a seguir são baseados nos trabalhos de Laprie [Laprie 1985] e Anderson e Lee [Anderson and Lee 1981].

(28)

Assim, o objetivo das técnicas de tolerância a falhas é evitar que erros acarretem falhas. Nesse contexto, exceções podem ser utilizadas para indicar um erro ou uma condição anormal do sistema, e mecanismos de tratamento de exceções podem ser empregados para prover tolerância a falhas.

2.1.1 Tratamento de exceções

De acordo com o clássico “The C++ Programming Language” [Stroustrup et al 2001] uma exceção é um mecanismo que permite a uma parte de um programa informar a outra parte de um programa que uma situação fora do normal foi detectada. Em linguagens modernas de programação uma exceção modela uma condição de erro de tal forma que ela possa ser tratada. Desta forma, tratamento de exceções é a capacidade que um software tem para reagir apropriadamente diante da ocorrência de uma exceção, continuando ou interrompendo sua execução, a fim de preservar a integridade do sistema [Cristian 1982]. Visto que este trabalho é focado em sistemas desenvolvidos utilizando-se as linguagens Java e AspectJ, a subseção a seguir apresenta como funciona o tratamento de exceções nestas linguagens.

2.1.2 Tratamento de exceções em Java

Em Java as exceções são objetos de tipos herdados de uma classe especial chamada Throwable. Na hierarquia de Classes Java essa classe se divide em duas Error e Exception. A classe Error representa os erros internos da linguagem Java ou erros da máquina virtual. A classe Exception representa os erros de programação. As exceções podem ser de dois tipos: checadas e não checadas. As checadas devem ser explicitamente tratadas ou propagadas no programa. As não checadas não são obrigadas a serem lançadas ou tratadas. Alguns exemplos de exceções não checadas são as exceções devido a acesso a objetos nulos

(NullPointerException), exceções devido a divisão por zero

(29)

transferido para o trecho de código que fica circundado pela palavra reservada catch e um conjunto de abre e fecha chaves associado. Blocos try-catch podem ser

aninhados sem restrição. Neste caso específico no momento que o sistema tentar realizar a divisão por zero o fluxo do sistema é transferido para o bloco catch onde será exibida uma mensagem de erro.

Uma exceção pode ser lançada ou relançada através do comando throw. A

palavra reservada throws na assinatura do método define quais as Exceções

checadas que podem ser lançadas pelo método. Caso uma exceção não seja capturada em nenhum ponto da hierarquia de chamadas ela pode ser propagada até o ponto de entrada do programa fazendo com que a execução seja interrompida de forma inesperada.

2.2 Verificação de Software

De acordo com o IEEE [IEEE 1983], a verificação de software permite constatar se o produto está sendo construído corretamente de acordo com sua especificação. O processo de verificação de software deve ser aplicado em cada estágio do desenvolvimento e tem basicamente o objetivo de descobrir problemas em um sistema. A verificação de software não permite garantir que o software está completamente livre de erros, mas sim que ele é bom o suficiente para o uso pretendido. A verificação de software pode ser estática, como por exemplo, através de análise estática de código, inspeção e verificação formal, e dinâmica, como é o caso dos testes de software. Os tipos de verificação tratados nesta dissertação são a análise estática e os testes de software.

1. try{

2. divisao = divisao / 0; 3. }

4. catch (Exception e){

(30)

2.2.1 Análise estática

A análise estática consiste na análise de representações estáticas do sistema com o objetivo de descobrir problemas sem executá-lo. Chess e West [Chess and West 2007] afirmam que a análise estática pode ser utilizada com diferentes objetivos, dentre eles a verificação de tipos, verificação de estilo de escrita e verificação do programa, ou seja, a análise verifica se o programa está de acordo com os requisitos. Nesta dissertação estamos interessados principalmente em encontrar inadequações deste último tipo.

A análise estática traz algumas vantagens, dentre elas: (i) para realizar a análise estática não é necessário que o código seja executado, o que permite que ela possa ser realizada até nas fases mais iniciais de desenvolvimento; (ii) por examinar o código a análise estática geralmente permite identificar a causa dos problemas, e não somente os sintomas; (iii) pode ser realizada tanto de forma manual como automática e sobre o código-fonte ou sobre o código-objeto. Pelos motivos acima citados podemos dizer que a análise estática é uma ótima complementação ao teste de software.

Porém, a análise estática tem algumas desvantagens. Por exemplo, a análise estática automatizada é um problema computacional indecidível no pior caso, visto que este é um caso clássico de um programa que analisa outro [Sipser 2005], ou seja, resolvê-lo é equivalente a resolver o problema da parada [Turing 1936]. Além disso, toda a análise estática produz algum falso positivo ou algum falso negativo, ou os dois [Chess and West 2007]. Ambos são indesejáveis, visto que os falsos positivos podem induzir o desenvolvedor a gastar tempo na resolução de um problema que não existe, e os falsos negativos escondem problemas que realmente existem. Além disso, em sistemas de grande porte, a massa de dados gerada pela análise estática e que deve ser analisada pode tornar inviável esse tipo de verificação.

2.2.2 Testes de Software

(31)

No que diz respeito às estratégias de teste elas podem ser classificadas de acordo com a informação usada como base para o design dos testes. Segundo Meyers [Meyers 1979] os testes podem ser divididos em testes funcionais ou testes estruturais, que são apresentados a seguir:

Testes funcionais (ou testes caixa-preta)

De acordo com Meyers [Meyers 1979], nos testes funcionais o programa é visto como uma caixa-preta, ou seja, não considera-se o comportamento interno do mesmo. Como ilustra a Figura 2 dados de entrada são fornecidos, o teste é executado e o resultado obtido é comparado ao resultado esperado de acordo com os requisitos. O teste passa caso o resultado obtido for igual ao esperado. Caso contrário, o teste falha. Entre alguns exemplos de critérios de teste para testes funcionais listados por Meyers estão: particionamento em classes de equivalência; análise do valor limite e grafo de causa-efeito. Os testes JUnit (apresentado na seção 2.3.1) são exemplos típicos de testes funcionais.

Figura 2: Técnica de Teste Funcional

Testes estruturais (ou testes caixa-branca)

(32)

Figura 3:Técnica de teste estrutural

Visto que este trabalho foca na realização de testes JUnit que envolvem o fluxo excepcional, para projetar tais testes é necessário conhecer a estrutura do programa, e esses testes portanto se caracterizam como caixa-branca. A seguir é apresentado o conceito de níveis de teste.

2.2.3 Níveis de teste

A atividade de testes é realizada em diferentes níveis ou estágios do desenvolvimento e pode envolver o sistema inteiro ou parte dele durante o andamento de seu desenvolvimento. Como o modelo V [Craig and Jaskiel 2002] da Figura 4 esses estágios dependem da fase de desenvolvimento em que os testes podem ser aplicados.

(33)

. Crespo et al. [Crespo et al. 2004] caracteriza tais níveis da seguinte forma:

 Testes de Unidade: também conhecidos como testes unitários, tem por objetivo explorar a menor unidade do projeto, procurando provocar falhas ocasionadas por defeitos de lógica e de implementação em cada componente de software, separadamente. O universo alvo desse tipo de teste são os métodos, classes ou pequenos trechos de código.

 Testes de Integração: visam provocar falhas associadas às interfaces entre os módulos quando esses são integrados para construir a estrutura do software que foi estabelecida na fase de projeto.

 Teste de Sistema: avalia o software em busca de falhas por meio da utilização do mesmo, simulando a utilização por um usuário final. Dessa maneira, os testes são executados nos mesmos ambientes, com as mesmas condições e com os mesmos dados de entrada que um usuário utilizaria no seu dia-a-dia de manipulação do software. Verifica se o produto satisfaz seus requisitos.  Teste de Aceitação: são realizados geralmente por um restrito grupo de

usuários finais do sistema. Esses simulam operações de rotina do sistema de modo a verificar se seu comportamento está de acordo com o solicitado.

Existem ainda os testes de regressão que não são considerados como um nível visto que podem ser executados durante todo o desenvolvimento. Eles são executados quando um componente é modificado e deseja-se verificar se outras funcionalidades não foram quebradas pela modificação.

2.3 Automação de testes

(34)

Com o objetivo de automatizar parte deste processo, algumas ferramentas de software vem sendo propostas. O trabalho de Meudec [Meudec 2001] divide em três principais categorias os softwares de automação de testes: (i) softwares para tarefas administrativas de testes – espeficicação dos testes e geração de relatórios de testes; (ii) softwares para tarefas mecânicas de testse – execução e monitoramento de testes, captura e execução de testes automatizados; (iii) softwares para geração de testes. Dentre as ferramentas da segunda categoria, que são o foco desta dissertação, podemos destacar a ferramenta JUnit.

2.3.1

JUnit

O JUnit [JUnit 2012] foi criado como um framework para escrever testes de unidade automatizados em Java. Esse framework facilita a criação de código para automação de testes com apresentação de resultados. Com ele pode ser verificado se cada método de uma classe funciona da forma esperada, exibindo possíveis erros ou falhas.

(35)

Como pode ser observado na Figura 5, no JUnit um teste de unidade corresponde a uma classe que estende a classe TestCase. Essa classe possui os seguintes métodos:

 run(): Cria um contexto (método setUp), em seguida executa o código e verifica o resultado (método runTest), limpando o contexto ao final (método tearDown);

 setUp(): Método chamado antes de cada método de teste;

 runTest(): Método responsável por controlar a execução do teste em si;  tearDown(): Método chamado após cada método de teste, devendo ser

utilizado para desfazer as operações realizadas no método setUp;

Para entendermos melhor como funciona o JUnit apresentamos a seguir um trecho de código de uma classe de um sistema de gerenciamento de contas bancárias chamada Conta.

1. package br.ufrn.banco; 2.

3. //imports…

4.

5. public class Conta{ 6.

7. private double saldo; 8.

9. //gets e sets 10.

11. public void creditar(double valor){ 12. saldo += valor;

13. Logger.log(“Movimentação na conta em ” +

14. dataAtual); 15. }

16.

17. ... 18.

19. public void debitar(double valor){…}

20.

21. ... 23.

24. public void realizarEmprestimo(doubleValor valor){…}

(36)

O trecho de código a seguir exemplifica um teste simples em JUnit (Classe SomaTeste) para os métodos da classe Conta. Os métodos desta classe correspondem aos métodos de teste, sendo que na versão 3 do JUnit cada método de teste precisa ter seu nome iniciado com a palavra “test”. Neste exemplo, depois de instanciar uma conta e definir o saldo com um determinado valor é realizada a operação que se deseja testar, neste caso a operação creditar. Após a execução do método, deseja-se verificar se ele foi executado corretamente. Então é feita uma asserção, ou uma verificação de condição, e para isso o JUnit disponibiliza métodos que realizam estas verificações. Neste caso foi utilizada a asserção assertEquals que avisa ao framework que ocorreu uma falha caso os dois parâmetros não sejam os mesmos.

1. //Exemplo de teste no JUnit 3

2. package br.ufrn.testes;

3.

4. import br.ufrn.banco.Conta;

5. import junit.framework.TestCase;

6.

7. public class SomaTest extends TestCase{

8. public void testSoma(){

9. Conta conta = new Conta();

10. conta.setSaldo(100.0);

11. conta.creditar(50.0);

12. assertEquals(150.0,conta.getSaldo());

13. }

14.}

15. //Agora o mesmo teste no JUnit 4

16. package br.ufrn.testes;

17.

18. import br.ufrn.banco.Conta;

19.

20. public class SomaTest{

(37)

23. Conta conta = new Conta();

24. conta.setSaldo(100.0);

25. conta.creditar(50.0);

26. assertEquals(150.0,conta.getSaldo());

27. }

28.}

Como pode ser observado no trecho código acima, na versão 4 do framework para criar um teste não é mais necessário estender a classe TestCase. Além disso, para identificar um método como sendo de teste não é necessário mais que o método

inicie com a palavra “test”, bastando incluir a anotação @Test para identificá-lo. Também podemos observar na Figura 5, que existe uma classe chamada TestSuite. Essa classe permite que sejam executados vários testes, adicionando-os através do método addTest().

Para exibir os resultados, o Junit apresenta visualmente barras que identificam se o teste passou, conforme mostra a Figura 6. A barra verde identifica que o teste passou. A barra vermelha identifica que houve um erro durante a execução do teste.

Figura 6: Resultados visuais do JUnit

(38)

2.4 Programação Orientada a Aspectos e AspectJ

A Programação orientada a Aspectos é uma metodologia utilizada em conjunto com os paradigmas de programação orientado a objetos e procedural, que visa incrementá-los com conceitos e construções que permitam modularizar conceitos transversais do sistema [Laddad 2003].

A programação Orientada a Aspectos permite aos desenvolvedores e projetistas de software separarem e organizarem o código de interesse comum a várias partes de um sistema (como por exemplo, conexão com banco de dados, logging, tratamento de exceções) encapsuladas em unidades de programação chamadas aspectos. A esses interesses comuns chamamos “crosscutting concerns” (ou interesses transversais).

Tomemos como exemplo o caso de logging de uma aplicação, como mostrado no código abaixo na implementação de orientação a aspectos utilizando a linguagem AspectJ [AspectJ 2012].

Em basicamente toda a operação que se deseja registrar seria necessário incluir manualmente código para armazenamento do logging, código este que não teria nenhuma relação com o objetivo da classe em que ele estaria sendo inserido. A este tipo de código chamamos de código entrelaçado. Agora imaginemos o quanto isso dificultaria a manutenção e evolução do sistema se houvesse uma quantidade grande de classes em que seria necessário fazer logging. A orientação a aspectos resolve esse problema e complementa a orientação a objetos com a criação de um aspecto logging (exemplificado no código AspectJ abaixo) que encapsula a operação de logging e é ativado de acordo com uma regra indicada no aspecto (neste caso, todas as operações na classe conta bancária).

1. package br.ufrn.aspects; 2.

3. imports…

4.

5. public aspect Logging{ 6.

7. pointcut operacaoBancaria(): execution( 8. *br.ufrn.banco.Conta.*(..));

(39)

11. Logger.log(“Movimentação na conta em ” + dataAtual);

12. } 13.}

Vamos analisar melhor este código. O primeiro elemento que aparece é o

pointcut. O pointcut é uma regra que define onde o aspecto deverá ser ativado. No

nosso exemplo criamos um pointcut chamado operacaoBancaria que é ativado toda vez que um método com quaisquer tipos de parâmetros da classe Conta é executado.

O segundo elemento do nosso aspecto é o advice. O advice define que após a execução do pointcut operacaoBancaria será executado um trecho de código, neste caso o trecho de Logging. Dessa forma em apenas algumas linhas de código definimos um conceito transversal, evitando a replicação em diversas partes de código de logging.

Outro elemento importante do AspectJ é a declaração declare soft. Esta

declaração da linguagem permite “suavizar” uma exceção, o seja, silenciar a exceção e relançá-la como uma exceção não-checada.

A ferramenta fruto deste trabalho utiliza AspectJ no seu core. Além disso, ela permite a verificação de regras de fluxo excepcional também em código orientado a aspectos usando AspectJ. A seguir apresentamos o conceito base para as regras de fluxo excepcional que servem de dados de entrada para os testes executados utilizando a ferramenta desta dissertação.

2.5 Regras de Design

Uma atividade comum a todo processo de software é o design do software. O

design incorpora a descrição da estrutura do software, os dados que são manipulados pelo sistema, a descrição das interfaces entre os componentes do sistema, e algumas vezes o algoritmo utilizado [Belady 1981]. Ele implementa importantes decisões sobre a arquitetura do software e possui um papel crucial no desenvolvimento, implantação e evolução do sistema [Krutchen et al. 2006].

(40)

sido apontada como uma das principais causas de baixa qualidade de software [Parnas 1994, van Gurp and Bosh 2002].

Neste contexto, a verificação de conformidade entre o design e a implementação é uma prática que promove a qualidade do software. Neste sentido as regras de design (do inglês design rules) [Baldwin and Clark 1999] especificam como as entidades de software devem ou não se relacionar com as demais. Essas regras definem contratos que devem ser rigidamente obedecidos em todas as fases posteriores do ciclo de vida do processo de construção do software. Essa abordagem, além de promover a modularidade do sistema, favorece o paralelismo de desenvolvimento. Ela pode ser considerada como uma camada de abstração acima da camada de implementação.

Nosso trabalho propõe uma forma de descrever regras de design específicas para o comportamento excepcional da aplicação.

2.6 Discussões

(41)

3 Abordagem Proposta

O objetivo deste capítulo é apresentar a abordagem de testes de fluxos excepcionais proposta neste trabalho. Um exemplo motivador é apresentado na seção 3.1. Logo depois a abordagem é descrita na seção 3.2 com a ajuda de um exemplo. Por fim, na seção 3.3, é feito um fechamento do capítulo e algumas discussões são realizadas.

3.1 Exemplo Motivador

Refatoração de Sistemas Orientados a

Objetos para Aspectos

Na seção 1.1 apresentamos um exemplo motivacional que mostrou algumas limitações da utilização de casos de teste JUnit puros para detectar problemas de comportamento excepcional. Nesta seção, apresentamos um exemplo adicional que demonstra ainda outros problemas no tratamento de exceções que podem ocorrer quando um sistema computacional é refatorado para utilizar um mecanismo de orientação a aspectos.

Quando um sistema orientado a objetos é refatorado para utilizar orientação a aspectos uma questão surge: o conceito transversal a ser encapsulado em um

aspecto pode lançar alguma exceção? Se a resposta for positiva, o time de desenvolvimento precisa lidar com duas tarefas igualmente importantes: (i) permitir que o aspecto criado lance a exceção; (ii) pôr em prática a regra de design voltada para fluxos excepcionais descrita no trabalho de Coelho et al. como solução para bug

patternHandlerless Signaler Aspect” [Coelho et al. 2008b] que consiste em criar um aspecto tratador de erro que será responsável por tratar as exceções lançadas pelo aspecto que encapsula o conceito transversal. Se a exceção lançada pelo aspecto criado é checada, devido a uma limitação da linguagem AspectJ, é necessário convertê-la em uma outra exceção não checada, através da utilização da construção declare soft do AspectJ, para que ela possa ser lançada. Este tipo de exceção também deve ser tratada pelo aspecto tratador de erro.

(42)

aspectos de tratamento de erro realizem o tratamento de exceção adequadamente, levando assim a quebra de uma regra de design da aplicação. Para ilustrar, na Figura 7 é apresentada a arquitetura de um sistema de gerenciamento de contas bancárias que foi refatorado para utilizar orientação a aspectos.

Figura 7: Arquitetura de um sistema refatorado para Aspectos

Como pode ser observado este sistema utiliza o padrão de arquitetura em camadas e implementa os seguintes interesses transversais:

 PerformanceMonitoring: É o responsável por monitorar a performance de cada requisição à servlet.

 NegativeValueOnOpCheck: Verifica quando a transferência foi realizada com um valor negativo.

 NegativeValueOnOpHandler: Responsável por tratar as instâncias da exceção não checada NegativeValueOnOpException. Lançadas pelo aspecto NegativeValueOnOpCheck.

No sistema, nenhum aspecto tratador de erro foi definido para o aspecto

PerformanceMonitoring porque cada exceção lançada por este monitor pode ser tratada dentro do próprio aspecto. Em outras palavras, as exceções lançadas durante o monitoramento não devem causar distúrbios aos fluxos excepcionais do sistema que está sendo monitorado.

(43)

1. public aspect NegativeValueOnOpCheck {

2.

3. //intercepta retiradas, créditos e transferências

4. pointcut valueReceiversOps = ...;

5.

6. before (double value) :

7. execution(valueReceiversOps) && args(value)

8. {

9. if (value < 0){

10. throw new NegativeValueOnOpException();

11. }

12. } 13.}

Percebemos que nesta aplicação, uma regra de design de fluxo excepcional é que todas as instâncias da exceção não checada NegativeValueOnOpException lançadas pelo aspecto NegativeValueOnOpCheck devem ser tratadas pelo aspecto tratador de erro NegativeValueOnOpHandler, cujo código é exibido a seguir.

1. public aspect NegativeValueOnOpHandler { 2. ...

3. pointcut valueReceiversOps=...;

4. void around () : call (valueReceiversOps){ 5. try{

6. proceed();

7. }catch(NegativeValueOnOpException nvoe){ 8. //treating exception

9. } 10. } 11.}

(44)

prematuramente captura a exceção. No exemplo do sistema de gerenciamento de contas bancárias o aspecto NegativeValueOnOpHandler foi criado para tratar qualquer instância de NegativeValueOnOpException lançada pelo aspecto NegativeValueOnOpCheck. Entretanto, uma cláusula catch-all definida no código base irá tratar qualquer instância NegativeValueOnOpException antes de NegativeValueOnOpHandler entrar em ação, como ilustra a Figura 8.

Figura 8: Visão esquemática do "ladrão de exceções"

O trecho de código a seguir ilustra a classe Bank que contém a cláusua

catch-all (linha 6), impedindo o aspecto NegativeValueOnOpHandler de tratar as instâncias de NegativeValueOnOpException.

1. public class Bank{

2. ...

3. public void credit(Account c, double val){

4. try{

5. //method body

(45)

7. //handles the exception

8. }

9. }

10. public void debit (Account c, double val) throws

11. NegativeBalanceException{

12. double aux = c.getBalance();

13. aux = aux – val;

14. if(aux<0) throw new NegativeBalanceException();

15. else c.setBalance(aux);

16. }

17. }

Este problema pode ocorrer de forma similar também em sistemas orientados a objetos. Uma maneira de prever este problema seria substituir todas as cláusulas catch-all por cláusulas de tratamento específicas de acordo com o tipo da exceção. Mas como já explicado na seção 1.1, essa solução ainda é passível de problemas.

Observa-se então que tanto no caso de sistemas orientados a objetos, como sistemas que utilizam orientação a aspectos seria bastante útil a existência de uma abordagem que permitisse a especificação das regras de design do comportamento excepcional da aplicação para que depois elas sejam verificadas ao longo de todo o desenvolvimento do sistema, e posteriores manutenções, evitando assim o problema de quebra da regra de design explanada nesta seção.

Esta dissertação visa propor uma abordagem que permite a especificação e verificação das regras de design da aplicação, tanto em nível de métodos, quanto de classes e pacotes de software. Essa abordagem começa a ser apresentada a partir da próxima seção.

3.2 Visão Geral da Abordagem Proposta

(46)

Figura 9: Sequência de Atividades da Abordagem Proposta

Com o objetivo de auxiliar nas atividades dessa abordagem, foi elaborada a ferramenta VITTAE [Sales Júnior and Coelho 2011] que estende o framework JUnit e apoia as atividades de projeto e automação dos testes, execução e avaliação.

A estrutura da ferramenta será explanada no próximo capítulo. Porém, seu funcionamento será explicado à medida que os passos da abordagem forem explicados a partir das próximas subseções.

3.2.1 Passo1: Definição das Regras de Design Excepcionais da Aplicação

A tarefa de definir as regras de design excepcionais é essencial, visto que estas regras é que deverão ser obedecidas durante o desenvolvimento e manutenção do software. Além disso, as regras de design são muito úteis como uma forma de documentação complementar do sistema. A descoberta das regras de design foge do escopo desta dissertação. Entretanto, podemos indicar algumas fontes de informações podem ser utilizadas para realizar esta tarefa. Para tanto podemos vislumbrar duas situações possíveis:

a) As regras de design são estabelecidas antes do início do

desenvolvimento: Quando isso acontece as regras podem ser extraídas de

(47)

exceção, mas nenhum handler é definido para trata-la. Para evitar este problema, pode-se previamente definir uma regra de design excepcional que exija que uma exceção lançada por um aspecto seja capturada por outro determinado aspecto (Error Handling Aspect).

b) As regras de design são estabelecidas ou evoluídas após o início

do desenvolvimento: Nestes casos a variedade de fontes é maior. Além

das fontes citadas anteriormente podemos nos valer de uma análise do código-fonte e dos relatórios de bugs e logs de exceção do sistema.

Para entender melhor a estrutura do XML Schema a exibe o XML Schema do arquivo de regras de design.

Figura 10: XML Schema para o arquivo que define as regras de design da aplicação

(48)

Java em outras ferramentas e, além disso, possui diversas bibliotecas estáveis e simples em diversas linguagens de programação.

Podemos observar que o XML inicia com um elemento raiz chamado contract. A partir daí podemos definir diversos elementos signaler. Este elemento representa um método (ou conjunto de métodos através do uso de um wildcards com o uso do caractere ‘*’ semelhante aos utilizado nos pointcuts do AspectJ) responsável por

lançar, através da criação ou propagação, um ou mais tipos de exceção, que é definido através do atributo signature. Para cada signaler podemos ter diversos elementos do tipo exception que representam as exceções lançadas por este método, descritas no atributo type. Para cada elemento exception podemos ter um ou mais elementos handler. Esse elemento representa o método (ou conjunto de métodos utilizando o mesmo wildcard descrito para o elemento signaler) responsável por capturar a exceção, definido através do atributo signature.

Para ilustrar como esse XML é preenchido vamos tomar como base o código do método debit da classe Bank exibida na seção 3.1. Este método pode lançar uma exceção se o saldo da conta ficar negativo com a operação de débito. Para este método podemos formular a seguinte regra:

Neste exemplo analisado, a especificação é que cada instância da exceção NegativeBalanceException lançada pela operação debit com valores do tipo

Account e double da camada Facade deve ser tratada pelo método handlerNegativeBalance() da classe ServletClient.

Em um projeto que utiliza nossa abordagem, os contratos ficam armazenados em um arquivo chamado contract.xml que deve ficar dentro de uma pasta chamada contracts na raiz do projeto, conforme mostra a Figura 11.

<contract>

<signaler signature=“Bank.debit(Account,double)”> <exception type=“NegativeBalanceException”>

<handler signature=“ServletClient.handlerNegativeBalance()”/> </exception>

(49)

Figura 11: Estrutura do projeto com a VITTAE e localização do arquivo de contratos

3.2.2 Passo 2 : Projeto e Automação do Teste

Após a definição das regras de design excepcionais é necessário que estas sejam verificadas. Para isso é preciso estimular tanto o código que lança a exceção quanto o código que trata a exceção. Para estimular o código que trata a exceção e automatizar este teste, deve ser criado um caso de teste JUnit que chama o método responsável por tratar uma determinada exceção do contrato. Através da VITTAE nossa abordagem provê um suporte ferramental para criação de um esqueleto do teste, destacando a chamada do método que trata a exceção. Para fazer isso é

necessário chamar o método chamado generateTests() da classe

br.ufrn.gits.fear.generators.ExceptionContractTestGenerator. No código abaixo é apresentado um exemplo de esqueleto de teste gerado pela ferramenta, baseado no contrato apresentado na seção anterior para a o método debit da classe Bank.

1. //declare package 2.

3. //imports 4.

5. ...

6. public class NegativeBalanceExceptionBankOperationEHTest extends 7. TestCase{

(50)

10. ServletClient handler = new ServerClient(); 11.

12. handler.handlerNegativeBalance(); 13. }

14.}

Como podemos observar somente o esqueleto gerado ainda não é suficiente para realizar o teste. A complementação do caso de teste e a garantia de que o método que trata a exceção seja chamado corretamente ficam a cargo do testador.

Além disso, faz-se necessário garantir que durante a realização do teste, existam condições para que a exceção esperada definida na regra de design seja lançada. No caso do nosso exemplo acima, seria necessário durante o teste instanciar os objetos de tal forma que quando o método handlerNegativeBalance seja chamado, o fluxo alcance o método debit da classe Bank. Além disso, o método debit precisa ser chamado com parâmetros que o levem a lançar a exceção NegativeBalanceException (por exemplo com o parâmetro value com valor negativo). Com esse objetivo de auxiliar nessa tarefa aconselha-se o uso de objetos mock [Freeman and Craig 2001]. Os objetos mock são um padrão de design para testes unitários onde um objeto mock simula o propósito de um objeto real sem a necessidade de instanciar o objeto real. Em nossa abordagem, a ferramenta VITTAE gera juntamente com as classes de Teste, mocks para simular o lançamento da exceção quando o método signaler for chamado com qualquer parâmetro. Em termos simples, o mock gerado é um aspecto que, independente da situação, quando o método signaler é chamado dentro do teste JUnit lança automaticamente a exceção definida na regra. O código a seguir apresenta um exemplo de aspecto mock para simular o lançamento da exceção pelo signaler.

1. package br.ufrn.gits.tests; 2.

3. //imports 4. @Aspect

5. public class

(51)

9. cflow(execution(*

10. NegativeBalanceExceptionBankOperationEHTest.*(..)))") 11.

12. public Object

13. launchNegativeBalanceException(ProceedingJoinPoint 14. thisJoinPoint)

15. throws NegativeBalanceException 16. {

17. throw new NegativeBalanceException(); 18. }

19. }

O aspecto acima, que utiliza a forma de anotações do AspectJ, é executado no lugar do método signaler, neste caso, Bank.debit, e lança automaticamente a exceção NegativeBalanceException.

Caso não fosse utilizado este mock, seria necessário garantir que durante a execução do método para debitar, o método handlerBankOperation chamasse o método Bank.debit com um valor para o parâmetro value negativo.

3.2.3 Passo 3 : Execução do teste

Após a construção do teste, faz-se necessário realizar a execução automatizada dos testes. Visto que a abordagem constrói os casos de teste em cima do framework JUnit e utiliza os mecanismos desse framework para reportar os sucessos e erros, pode-se utilizar toda a sua estrutura para automatizar essa execução. O JUnit permite a criação de conjuntos, ou suítes, de testes, e a geração de relatórios de execução, que são usados para avaliação dos testes. Como já explicado no capítulo anterior o JUnit reporta visualmente os casos de uso que passaram e que falharam.

3.2.4 Passo 4 : Avaliação do Teste

(52)

Quando ocorre uma quebra da regra de design a VITTAE lança uma exceção do tipo ContractCheckingException, exibindo qual a regra que foi quebrada e quais foram os signalers e handlers. Dessa forma é possível analisar os motivos pelos quais ocorreu a quebra de design.

A Figura 12 a seguir exibe inicialmente como o JUnit puro não consegue fornecer informações relacionadas ao fluxo do tratamento de exceção, enquanto a Figura 13 exibe um exemplo da atuação da VITTAE para avaliação do resultado durante a execução de um teste.

Figura 12: Tela do JUnit sem a extensão da VITTAE

Figura 13: Exemplo de utilização do framework JUnit em conjunto com a VITTAE para execução automatizada dos testes

(53)

1. public class Bank{

2. ...

3. public void credit(Account c, double val){

4. try{

5. //method body

6. }catch (Exception e){

7. //handles the exception

8. }

9. }

10. public void debit (Account c, double val){

12. double aux = c.getBalance();

13. aux = aux – val; 14. try{

15. ...

16. if(aux<0) throw new NegativeBalanceException();

15. else c.setBalance(aux);

16. }

17. catch(Exception e){

18. ...

19. }

16. }

17. }

Observe que agora dentro do método debit foi inserido um catch-all, o que quebraria a regra de design, visto que o fluxo da exceção NegativeBalanceException nunca chegará ao método

handlerNegativeBalance da classe ServletClient.

Caso executássemos o teste de fluxo excepcional o JUnit retornaria um erro (red

Imagem

Figura 4: Modelo V adaptado de Craig e Jaskiel [Craig and Jaskiel, 2002] que descreve a  correspondência entre as atividades de desenvolvimento e os níveis de teste
Figura 10: XML Schema para o arquivo que define as regras de design da aplicação
Figura 13: Exemplo de utilização do framework JUnit em conjunto com a VITTAE para  execução automatizada dos testes
Figura 16: Diagrama de sequência para verificação da regra de design
+7

Referências

Documentos relacionados

Because this marketing management delivers a strong and cost-efficient online presence specifically in Portugal, this temporary advantage is non-transferable to

Esses objetivos aproximam o Observatorio Global de Medios aos demais observatórios latino-americanos que possuem, apesar das diferenças na origem, composição e orientação

Na hepatite B, as enzimas hepáticas têm valores menores tanto para quem toma quanto para os que não tomam café comparados ao vírus C, porém os dados foram estatisticamente

É nesta mudança, abruptamente solicitada e muitas das vezes legislada, que nos vão impondo, neste contexto de sociedades sem emprego; a ordem para a flexibilização como

Os caminhos percorridos, as travessias nos países periféricos desde o sequestro das terras ancestrais à diáspora culminaram com mudanças na forma de ver e viver as

Descrição Técnica e Discussão dos Resultados 23 Figura 18 - Teor de humidade do tall-oil em função da massa volúmica da lixívia ácida. 3.4 Balanço mássico

Mais dados referem ainda que apenas 10.6% das escolas do 1º Ciclo possuem acesso à Internet enquanto que nos 2º, 3º Ciclos e Secundário 89.3% já acedem à Internet.. Dos

O empreendimento etnográfico aqui exposto, no sentido de descrever e interpretar as representações de alunos e do professor sobre o discurso de esportivização da EF, aponta para a