Testes e Métricas de Software
2.2 Métricas de Software
Segundo Cukic [18], não é surpresa que em engenharia de software a maioria dos modelos tenha os seus defensores e os seus opositores. Para a predição de módulos de software propensos a falhas, esse fato não é diferente. Pesquisas recentes têm demonstrado a utilidade de métricas de software como fonte de informação para modelos de predição de módulos defeituosos. Muitos pesquisadores já estão convencidos da eficácia das métricas nesta tarefa, outros nem tanto.
As métricas McCabe [37] foram desenvolvidas por T. J. McCabe em 1976. Elas são métri- cas de complexidade que fornecem uma visão sobre a confiabilidade e a manutenibilidade de um módulo de software. São obtidas a partir do diagrama do fluxo de controle de um módulo de software. Segundo Pressman [3], durante o teste e a manutenção de um software, essas mé- tricas fornecem informações detalhadas que ajudam a localizar áreas de instabilidade potencial nos módulos. Por sua vez, Menzies [14], afirma que existem diferentes pontos de vista sobre o uso dessas métricas para prever erros de software. Segundo ele, dentre as métricas McCabe, alguns afirmam que uma complexidade ciclomática>10 é um bom indicador da existência de falhas, enquanto outros afirmam que uma complexidade essencial>4 é melhor. Os detalhes sobre essas duas métricas serão vistos na Subseção 2.2.1.
Maurice Halstead desenvolveu em 1977 [38] um conjunto de métricas que relacionam in- formações sobre softwares e estudos ligados a área de psicologia [39, 40]. Durante os anos subseqüentes, essas métricas geraram bastante controvérsia entre os pesquisadores. Fenton e
Pfleeger [40] afirmam que Halstead não deixou claro o relacionamento entre alguns compo- nentes de sua teoria, de forma que não é possível afirmar se ele estava definindo métricas ou alguma técnica de previsão.
Conforme será visto no Capítulo 5, dos projetos disponíveis, a minoria deles foi desenvol- vida em Linguagem Orientada a Objeto. Por conta disso, neste trabalho, as métricas orientadas a objeto não serão consideradas. Um resumo de todas as métricas utilizadas pode ser visto através da Tabela 2.1. Nas Subseções 2.2.1, 2.2.2 e 2.2.3 serão apresentadas, respectivamente, as métricas McCabe, as de tamanho de código e as Halstead. A Subseção 2.2.4, apresenta um conjunto de métricas derivadas a partir das métricas de tamanho de código e McCabe.
Tabela 2.1 Resumo das métricas utilizadas nesta dissertação.
Tipo da Métrica Métrica Definição
McCabe v(G) Complexidade Ciclomática
ev(G) Complexidade Essencial
iv(G) Complexidade de Projeto
Halstead N Tamanho
V Volume
L Nível do Programa
D Nível de Dificuldade
I Conteúdo Inteligente
E Esforço para Gerar o Programa
B Estimativa de Erro
T Tempo de Programação
Tamanho de Código LCExecutavel Linhas de Código Executáveis
LComentario Linhas de Comentários
LBranco Linhas em Branco
LCodigoComentario Linhas de Código e Comentários
µ1 Operadores Únicos
µ2 Operandos Únicos
N1 Total de Operadores
N2 Total de Operandos
ContagemBranch Contagem de Branches
Atributos Derivados LCTotalExec Total de Linhas de Código Executáveis
LCTotal Número Total de Linhas
DensidadeCiclomatica Densidade Ciclomática DensidadeProjeto Densidade de Projeto DensidadeEssencial Densidade Essencial
2.2.1 Métricas McCabe
As Métricas McCabe [37] foram criadas a partir de conceitos da teoria dos grafos [41]. Um grafo é basicamente um conjunto de nós e arcos, onde cada arco é responsável por fazer a ligação de um nó a outro. Dependendo do objetivo, os arcos de um grafo podem ter direção ou não. Quando possuem direção, o grafo é chamado de direcionado, caso contrário, é chamado de não-direcionado.
McCabe utilizou o conceito de grafo direcionado para descrever a estrutura de execução de um módulo de software. No modelo proposto por McCabe, cada módulo é formado por um conjunto de blocos de código, os quais seriam representados pelos nós do grafo. Por sua vez, os arcos do grafo representam o fluxo de execução de um bloco de código para outro. Nesse modelo, McCabe considerou que cada caminho dentro do grafo representa um fluxo básico de execução do módulo em questão. Para McCabe, um módulo é considerado como uma função ou um método de uma linguagem de programação, ou seja, um módulo seria a menor unidade funcional de um software. O repositório do MDP também utiliza esse mesmo conceito. Assim, neste trabalho, essa será a convenção utilizada para definir módulos de software.
Outro conceito de teoria dos grafos utilizado por McCabe foi o do número ciclomático [41], que tem o objetivo de avaliar a quantidade de ciclos básicos independentes que existem dentro de um dado grafo. Em teoria dos grafos, o número ciclomático de um grafo G é dado pela Equação 2.1, onde m, n e p, são, respectivamente, a quantidade de arcos, vértices (nós) e componentes conectados. Um grafo é classificado como conectado se, e somente se, existir um caminho entre qualquer par de seus vértices. Quando um grafo é conectado o parâmetro p da Equação 2.1 recebe o valor 1, caso contrário, p > 1. Em um grafo não-direcionado conectado o número ciclomático representa a quantidade de ciclos desse grafo, enquanto que em um grafo direcionado conectado, ele representa a quantidade de caminhos existente entre o nó de entrada e o nó de saída. Em ambos os casos o parâmetro p recebe o valor 1.
Cyc(G) = m − n + p (2.1)
Em analogia ao conceito do número ciclomático, McCabe criou a complexidade ciclomá- tica (v(G)). O objetivo dele era avaliar a quantidade mínima de fluxos básicos independentes, que combinados podem gerar qualquer caminho de execução dentro de um módulo de soft- ware. McCabe afirma [37] que a complexidade de um software é independente do seu “ta- manho físico” (número de linhas, por exemplo), dependendo apenas da estrutura de decisão do programa. Esse é um fato relevante e decisivo na escolha dessa métrica para a avaliação da qualidade de um software.
O valor da complexidade ciclomática representa a quantidade de fluxos básicos de execução de um módulo de software. Como todos os fluxos de execução devem ser avaliados durante a fase de testes, a quantidade de testes caixa-branca necessários para um módulo de software será igual a sua complexidade ciclomática. O valor da complexidade ciclomática também pode ser usado como um limiar que auxilia na manutenção da confiabilidade, da testabilidade e da gerenciabilidade de um módulo de software.
O fluxo de execução de um módulo de software é representado por um grafo direcionado não-conectado que possui um único nó de entrada e um único nó de saída, de forma que cada
fluxo de execução deve iniciar no nó de entrada e finalizar no nó de saída. Para que seja possível transformar um grafo de execução de um software em um grafo direcionado conectado, é necessária a adição de um arco ligando o nó de saída do grafo ao nó de entrada. McCabe chamou esse arco de arco virtual. Através do arco virtual, sempre existirá um caminho entre qualquer par de vértices de um grafo que represente o fluxo de execução de um módulo de software.
Segundo Watson e McCabe [42], a representação do arco virtual é feita através da adição do valor 1 a fórmula do número ciclomático. Watson afirma ainda que, o arco virtual não é apenas uma conveniência matemática, ele representa o fluxo de controle do restante do programa em que o módulo é utilizado. O arco virtual também pode representar a quantidade de vezes que um módulo foi executado. Dessa forma, a complexidade ciclomática é dada pela Equação 2.2, onde n representa a quantidade de blocos de código e m representa a quantidade de arcos que os interligam.
v(G) = m − n + 1 + 1
= m − n + 2 (2.2)
Para demonstrar como se calcula a complexidade ciclomática, será apresentado o exemplo de uma função em linguagem C que calcula o Máximo Divisor Comum (MDC) entre dois nú- meros. Essa função pode ser vista através do Código-Fonte 2.1 e o seu grafo de execução pode ser visto através da Figura 2.3. Nesse grafo, os nós são numerados de N0 a N8 e representam os blocos do Código-Fonte 2.1 — que também receberam a mesma numeração. Por sua vez, o arco virtual é representado por um arco tracejado ligando o nó final ao nó inicial. Conforme dito anteriormente, através da adição do arco virtual, o grafo de execução se transforma em um grafo direcionado conectado. Para o exemplo em questão a complexidade ciclomática é calculada como v(G) = 10 − 9 + 2 = 3.
Código-Fonte 2.1 Código-fonte da função MDC
1 N0 : mdc (i n t num1 , i n t num2 ) { 2 N1 : i n t aux , count , r e s u l t ; 3 N2 : i f ( num2>num1 ) { 4 N3 : aux=num1 ; 5 num1=num2 ; 6 num2=aux ; 7 }
8 N4 : aux = num1 % num2 ;
9 N5 : while ( aux ! = 0 ) {
10 N6 : num1 = num2 ;
11 num2 = aux ;
12 aux = num1 % num2 ;
13 }
14 N7 : return num2 ;
15 N8 : }
Além do método tradicional de cálculo da complexidade ciclomática, McCabe apresenta um método simplificado [37, 42] para execução da mesma tarefa. Esse segundo método consiste
em calcular a complexidade ciclomática através da contagem das regiões existentes em um grafo, sendo o valor da complexidade definido pela quantidade total de regiões. A Figura 2.4 demonstra a eficácia dessa segunda abordagem através de um grafo simplificado do Código- Fonte 2.1. Nesse existem três regiões, de forma que a v(G) = 3.
Figura 2.3 Fluxo da execução da função que calcula o MDC de dois números
Outra métrica proposta por McCabe é a complexidade essencial (ev(G)). O objetivo dessa métrica é avaliar se o programador utilizou programação estruturada ou não na construção de um dado módulo de software. Um código não estruturado torna o entendimento difícil e exige um esforço extra de testes para se alcançar um alto grau de confiabilidade. Caso o programador utilize programação não estruturada, a complexidade essencial quantificará o quão o módulo é não estruturado.
A programação estruturada reduz a complexidade, melhora a testabilidade e a manutenibili- dade de um código-fonte. Dijkstra mostrou em seus trabalhos [43, 44, 45], que a programação estruturada oferece mecanismos que facilitam a compreensão e a escrita de programas. Ela é formada por um conjunto restrito de primitivas de programação, através das quais é possí- vel representar qualquer algoritmo. As primitivas da programação estruturada são: seqüência, seleção e iteração. Essas primitivas podem ser vistas através da Figura 2.5. Por outro lado, a lógica de programação não-estruturada utiliza mecanismos de controle de fluxo que fazem
Figura 2.4 Grafo simplificado do fluxo da execução da função que calcula o MDC de dois números. A quantidade de regiões do grafo é igual a nível de complexidade ciclomática, logo, v(G) = 3.
a transferência incondicional de uma parte do código para outra. Um exemplo desse tipo de mecanismo é o comando GOTO encontrado em diversas linguagens de programação como C, C++, C#, Pascal, Visual Basic, dentre outras.
A utilização de programação estruturada permite realizar a decomposição (ou redução) do código através das primitivas de programação. Segundo Watson e McCabe [42], essa capa- cidade de decomposição facilita a modularização do software, permitindo a aplicação de uma estratégia de “dividir para conquistar”, o que facilita o entendimento e a manutenção do código. Um código não-estruturado não possui a capacidade de decomposição, e deve ser tratado como bloco único. O exemplo da Figura 2.6 foi criado por McCabe [37], ele é usado para demons- trar o funcionamento de um processo de redução. Nesse exemplo, o grafo G de um código estruturado passa por um processo de redução até que sua complexidade ciclomática se torne unitária.
Conforme exibido no exemplo da Figura 2.6, um código é decomposto através da redução aplicada sobre as primitivas de programação. Em um grafo de código não-estruturado essa decomposição só pode ser aplicada sobre tais primitivas. Nesses casos, o grafo conseguirá ser reduzido até o momento em que sua complexidade ciclomática se torne igual a sua complexi- dade essencial, ou seja, até que v(G) = ev(G). Um exemplo dado por McCabe [37] ilustra esse tipo de situação e pode ser observado através da Figura 2.7.
Figura 2.5 Primitivas de linguagem de programação estruturada: seqüência, seleção e iteração.
Figura 2.6 Redução em grafo construído a partir de um código estruturado. Através da remoção das primitivas de programação esse código pode ser reduzido até que v(G) = 1.
Figura 2.7 Redução em grafo construído a partir de um código não-estruturado. O grafo consegue ser reduzido até que v(G) = ev(G).
Para se calcular a complexidade essencial de um módulo, é necessário levar em considera- ção a quantidade de primitivas de programação estruturada que podem ser reduzidas do grafo de fluxo (G) deste módulo. Dessa maneira, a complexidade essencial pode ser calculada através da Equação 2.3, onde m representa a quantidade de primitivas que podem ser reduzidas. No exemplo da Figura 2.7, inicialmente o grafo suporta a redução de duas primitivas (m = 2), de forma que ev(G) = 6 − 2 = 4.
ev(G) = v(G) − m (2.3)
Outra métrica criada por McCabe é a complexidade de projeto (iv(G)) [42], também conhe- cida como complexidade de design. Essa métrica tenta quantificar o grau de acoplamento entre os módulos. Em outras palavras, dado o grafo de fluxo (G) de um módulo, essa métrica irá me- dir como a estrutura de G irá se relacionar com outros módulos através de chamadas externas.
O cálculo da complexidade de projeto inicia-se com a construção do grafo de fluxo de um módulo. A partir desse ponto, o grafo deve sofrer uma forma diferente de redução. Todas as primitivas que não estiverem envolvidas em chamadas a outros módulos devem ser removidas do grafo. O grafo resultante G"deve então ser utilizado no cálculo da complexidade de projeto, a qual é definida como sendo a complexidade ciclomática de G", ou seja, iv(G) = v(G").
2.2.2 Métricas de Tamanho de Código
O tamanho físico de um código-fonte é um atributo básico que pode ser medido sem a ne- cessidade de execução do software. Diversas métricas podem ser utilizadas para calcular esse tamanho, portanto, nesta subseção, serão apresentadas diferentes maneiras de realizar esta ta- refa.
A contagem de linhas de código é uma métrica simples, confiável e a forma mais comum de se medir o tamanho de um software. Essa contagem pode ser dividida em diversos subgru- pos específicos. Neste trabalho, por exemplo, serão utilizados: o número de linhas contendo apenas código executável (LCExecutavel), o número de linhas contendo apenas comentários (LComentario), o número de linhas em branco (LBranco) e o número de linhas que contém de forma conjunta código executável e comentários (LCodigoComentario). Desses subgrupos, as linhas em branco e as linhas com comentários são usadas para facilitar a leitura e o en- tendimento do programa. A junção dessas métricas pode também ser utilizada na geração de métricas derivadas, as quais serão vistas na Subseção 2.2.4.
Diversos pesquisadores apresentam um certo ceticismo quanto a quantidade de informações que as métricas de contagem de linhas podem disponibilizar. Segundo Fenton e Pfleeger [40], as métricas de contagem de linhas por si só não apresentam informações relativas a esforço, produtividade ou custo. Conte et al. [46] afirmam que a contagem de linhas não é consistente porque algumas linhas são mais difíceis de codificar do que outras. Por outro lado, Fenton e Pfleeger [40] também afirmam que a contagem de linhas é útil para prever duração da fase de projeto do software — que de certa forma também permite um previsão do tamanho do código — e para indicar a quantidade de esforço necessária no desenvolvimento de novos projetos.
Outras medidas, referentes ao código-fonte de um programa, preocupam-se em contar a quantidade de operandos e operadores existentes. Os operandos são todas as variáveis, constantes e chamadas às funções que existem dentro de um módulo. Por sua vez, os ope- radores são um conjunto de símbolos que, junto com os operadores, formam expressões que possibilitam a transformação de valores. Como exemplo, é possível citar os operadores aritmé- ticos, os de atribuição, os condicionais, os relacionais, etc.
A primeira métrica baseada na contagem de operadores e operandos é a quantidade de operadores únicos (µ1), que se refere a quantidade de operadores distintos que estão contidos
no código. Outra métrica é a quantidade de operandos únicos (µ2), a qual faz referência a
quantidade de diferentes operandos. Por fim, também serão utilizadas métricas que contam o total de operadores (N1) e operandos (N2).
Outra métrica de tamanho de código utilizada neste trabalho é chamada de contagem de branches (ContagemBranch). No grafo de fluxo de um programa, cada possível saída de um nó de decisão representa um branch. Por exemplo, o ponto de entrada de um módulo representa um branch, da mesma forma que cada saída de uma decisão também. A declaração de um “if” tem dois possíveis fluxos: o “verdadeiro” e o “falso”. Cada um deles representa um branch. Em um módulo de software, uma grande quantidade de branches indica que mais recursos de teste serão necessários para avalia-lo.
2.2.3 Métricas Halstead
Segundo Pressman [3], as métricas Halstead foram as primeiras “leis” analíticas para software de computador. Halstead considerou que um programa seria constituído por tokens, que, por sua vez, seriam os operadores e os operandos de um código-fonte [39]. Conforme visto na subseção anterior, os operandos são definidos como as variáveis e as constantes, enquanto que operadores são as vírgulas, parênteses, operadores aritméticos, etc.
de operadores (N1), o total de operandos (N2), a quantidade de operadores (µ1) e de operandos
(µ2) únicos. A partir dessas informações e de trabalhos ligados a psicologia, Halstead gerou um
conjunto de expressões aritméticas capazes de medir informações de software como: tamanho do programa, volume, nível do programa, dificuldade, esforço, tempo de programação, etc.
Halstead definiu que o tamanho (N) de um programa pode ser calculado em função do total de operadores e operandos [3], conforme exibido na Equação 2.4. O volume do software (V ) é outra métrica de Halstead. Segundo Fenton e Pfleeger [40], essa métrica está relacionada com número de “operações mentais” necessárias para se escrever um programa de tamanho N. O volume é calculado pela Equação 2.5. Um característica importante dessa métrica é que seu valor é dependente da linguagem de programação utilizada na codificação do projeto. Por exemplo, o número operadores e operandos de uma aplicação desenvolvida em linguagem C certamente é diferente da quantidade de operadores e operandos da mesma aplicação desenvol- vida em lingugens como JAVA, C#, Haskell, etc.
N = N1+ N2 (2.4)
V = (N1+ N2)log2(µ1+µ2) (2.5)
O nível de dificuldade (D) [47] é uma métrica cuja semântica indica o nível de propensão a falha de um programa. Halstead considerou que, se um mesmo operando for usado muitas vezes durante o mesmo programa, este programa seria mais propenso a falhas. Dessa forma, o cálculo da dificuldade é proporcional a quantidade de operadores únicos e proporcional à razão existente entre o número total de operandos e o número de operando únicos. O nível de dificuldade é calculado conforme a Equação 2.6. Outra métrica, chamada nível do programa (L) é definida como sendo o inverso do nível de dificuldade [40], conforme demonstrado na Equação 2.7. D =!µ1 2 " ×#Nµ2 2 $ (2.6) L = 1 D (2.7)
O esforço (E) é uma métrica que nasceu a partir de estudos sobre psicologia. Ela tem o objetivo de indicar a quantidade de esforço necessária para se gerar um software, sendo calculada através da Equação 2.8. Nessa métrica o esforço é medido em “unidades mentais”, as quais seriam necessárias para se entender um programa. O tempo de programação (T ) é uma métrica que foi baseada nos estudos do psicólogo John Stroud, onde se afirmava que a mente humana é capaz de realizar um número limitado (β) de operações por segundo. Halstead acreditava queβ= 18 e que o tempo estimado para se construir um programa seria dado através da Equação 2.9.
E =V
L (2.8)
T = E
Halstead também criou uma métrica chamada conteúdo inteligente (I) (também conhecida como volume potencial), que representa o volume mínimo que um dado programa poderia assumir. Outra métrica, a estimativa do erro (B), está relacionada a complexidade do software. Ela representa uma estimativa da quantidade de erros que um software deveria conter.
I = L ×V (2.10)
B = E
2 3
3000 (2.11)
Como muitas das proposições de Halstead foram definidas de forma empírica, através da junção de métricas de software e estudos ligados a psicologia, muitos pesquisadores questio- nam a validade dessas métricas. Segundo Fenton e Pfleeger[40], as métricas Halstead são um pouco confusas e vêm perdendo força nos últimos anos. Por sua vez, Pressman [3] afirma que muitos acreditam que a teoria de Halstead possui algumas falhas e que, apesar da controvérsia, o trabalho dele é passível de verificação experimental e vem sendo investigado por diversas pesquisas [48, 40, 49, 46, 50, 3]. Neste trabalho tais métricas não foram desconsideradas, pois (1) não existe um consenso sobre o tratamento adequado para elas e (2) foram coletadas pelo MDP e vêm sendo usadas com freqüência em diversas pesquisas passadas [12, 14, 15, 16, 11].
2.2.4 Métricas Derivadas
Diversas métricas derivadas podem ser calculadas usando como base as métricas McCabe (Sub- seção 2.2.1) e de tamanho de software (Subseção 2.2.2). Algumas dessas métricas são popu- larmente conhecidas, porém, em alguns casos podem representar o conhecimento específico de algum especialista. Neste trabalho, serão utilizadas as mesmas métricas derivadas que a equipe da NASA coletou e armazenou nos repositórios de métricas do MDP [13].
Com relação as métricas de tamanho de código, além das diversas modalidades de contagem de linhas, também é interessante a utilização de métricas que consolidem tais informações. Para está finalidade, neste trabalho, duas métricas serão utilizadas: o Total de linhas executáveis (LCTotalExec) e o Total de Linhas (LCTotal), que são representadas, respectivamente, pelas Equações 2.12 e 2.13.
LCTotalExec = LCExecutavel + LCodigoComentario (2.12)
LCTotal = LCExecutavel + LComentario + LBranco + LCodigoComentario (2.13) Outras métricas derivadas podem ser calculadas a partir das métricas McCabe. São elas: a Densidade Ciclomática (DensidadeCiclomatica), a Densidade de Projeto (DensidadePro jeto) e a Densidade Essencial (DensidadeEssencial), as quais podem ser calculadas através das Equações 2.14, 2.15 e 2.16, respectivamente.
DensidadeCiclomatica = v(G)
DensidadePro jeto =iv(G)