Desenvolvimento

2 out, 2018

Técnicas de teste para desenvolvedores

Publicidade

Por muitos anos, o processo de desenvolvimento de software segregou o papel do desenvolvedor e do testador como sendo dois profissionais com habilidades e objetivos diferentes. O cenário atual, entretanto, exige que testadores sejam capazes de desenvolver e que os desenvolvedores sejam capazes de testar.

Dessa forma, podemos entregar software com mais velocidade e com mais qualidade. Agora contextualizados, podemos então conhecer, numa abordagem prática, quais são as técnicas que podemos utilizar para validar os softwares que desenvolvemos de forma sistemática e mais apurada.

Além disso, utilizar scripts de teste automatizado para exercitar os testes provenientes do uso das técnicas sempre que o software sofrer modificações.

Definição do problema

Consideraremos o código a seguir como objeto dos nossos estudos. Ele atende a um requisito de negócio que determina que funcionários com salário a partir de R$ 3.000,00 e inferior a R$ 5.000,00, a partir de três anos desde a contratação, serão considerados incluídos na nova política de bonificação da companhia.

Método para validação do salário versus tempo de contratação (em JavaScript):

const validacaoSalario = (salario, tempo) => { if (salario >= 3000 && salario < 5000) {
return (tempo >= 3) ? true : false;
}
};

Cobertura de sentença

A cobertura de sentença é uma técnica utilizada para garantir a execução de todas as linhas da aplicação ao menos uma vez. Logo, escrevemos testes que utilizarão dados de entrada de forma que passem por todas as sentenças do método que estamos construindo.

Aplicação da cobertura de sentença (em JavaScript, com Jasmine):

describe(‘Testes para validação de salário’, () => { it(‘para cobertura de sentença’, () => {
expect(validacaoSalario(4000, 4)).toBeTruthy();
});
});

Perceba que esse único teste exercitou todos os comandos contidos dentro do método validacaoSalario, uma vez que o valor informado para o salário atendeu ao condicional, e a linha de dentro foi atingida. Dessa forma, com um único teste, temos 100% da cobertura de sentença. Essa técnica é utilizada como primeiro passo para times que desejam iniciar com a criação de testes para cobertura do código, pois requer menos esforço e traz ganhos rápidos.

Cobertura de decisão

A cobertura de decisão tem o objetivo de avaliar o verdadeiro e falso de cada condicional contido no código. Essa caraterística faz com que ela seja mais abrangente do que a cobertura de sentença, dado que, para o mesmo trecho de código, teremos muito mais testes a serem criados. Em contrapartida, precisamos de mais tempo para a criação dos testes.

Aplicação da cobertura de decisão (em JavaScript, com Jasmine):

describe(‘Testes para validação de salário’, () => {
it(‘pra cobertura decisão do salário 3k e com 10 anos’, () => { expect(validacaoSalario(3000, 10)).toBeTruthy();
});
it(‘pra cobertura decisão: salário 4.5k e menos de 3 anos’, () => { expect(validacaoSalario(4500, 2)).toBeFalsy();
});
});

O código 3 mostra o cenário no qual cobrimos a condição verdadeira da validação de salário e as condições verdadeira e falsa da condição relacionada ao tempo. Ainda falta a validação da condição falsa da condição que avalia o salário.

Bem, no caso em que esse método deveria retornar apenas true ou false, nosso próximo teste revelará um defeito que não havia sido identificado pela cobertura de sentença, que é a falta de um retorno específico para a condição falsa do salário, uma vez que não definimos explicitamente o retorno para esse cenário.

Aplicação da cobertura de decisão com teste que retorna falha (em JavaScript, com Jasmine):

describe(‘Testes para validação de salário’, () => {
it(‘pra cobertura decisão: salário fora do escopo e 7 anos’, () => { expect(validacaoSalario(4500, 7)).toBeFalsy();
});
});

Partição de equivalência

A partição de equivalência é uma técnica usada como base para a maioria das técnicas utilizadas por testadores para projetar seus testes. Basicamente, é a atividade formal de identificar quais as faixas de valores permitidos, sejam numéricos ou não.

Geralmente, é utilizada para validações funcionais, mas cabe muito bem para validações em qualquer nível de teste, seja em código, em API, em UI etc.

Para aplicá-la, vamos nos lembrar do requisito que dizia “funcionários com salário a partir de R$ 3.000,00 e inferior a R$ 5.000,00 a partir de três anos desde a contratação”. O primeiro passo é identificar as variáveis, que são salário e tempo.

Depois, identificar todas as possibilidades relacionadas a elas, essas seriam as partições. Para salário, são < 3k, >= 3k e <5k além de >=5k.

Para tempo, são apenas <3 e também >=3. Identificadas as partições, fazemos a permutação entre as partições de cada variável. Logo, teríamos 3 salários vezes 2 tempos, o que daria 6 combinações de entradas. Colocaríamos então o resultado esperado para cada uma delas:

  • Salário <3k e tempo <3 deve retornar false
  • Salário >= 3k e <5k e tempo <3 deve retornar false
  • Salário >=5k e tempo <3 deve retornar false
  • Salário <3k e tempo >=3 deve retornar false
  • Salário >= 3k e <5k e tempo >=3 deve retornar true
  • Salário >=5k e tempo >=3 deve retornar false

Teríamos que escrever um teste para cada combinação identificada. Um único valor seria escolhido de forma arbitrária para cada faixa de valores contido em cada combinação.

Aplicação da partição de equivalência com permutação (em JavaScript, com Jasmine):

describe(‘Testes para validação de salário’, () => { it(‘Salário <3k e tempo <3 deve retornar false’, () => {
expect(validacaoSalario(2499.99, 2)).toBeFalsy();
});
it(‘Salário >= 3k e <5k e tempo <3 deve retornar false’, () => { expect(validacaoSalario(3299.99, 2)).toBeFalsy();
});
it(‘Salário >=5k e tempo <3 deve retornar false’, () => { expect(validacaoSalario(6900, 1)).toBeFalsy();
});
it(‘Salário <3k e tempo >=3 deve retornar false’, () => { expect(validacaoSalario(1200, 5)).toBeFalsy();
});
it(‘Salário >= 3k e <5k e tempo >=3 deve retornar true’, () => { expect(validacaoSalario(4199.99, 6)).toBeTruthy();
});
it(‘Salário >=5k e tempo >=3 deve retornar false’, () => { expect(validacaoSalario(5379.99, 12)).toBeFalsy();
});
});

Podemos avaliar a cobertura da partição de equivalência de duas formas: 1) a cobertura por partição ou 2) a cobertura das combinações geradas pela permutação. Na opção 1, escreveríamos testes que exercitassem cada uma das cinco partições identificadas, ou seja, três para o salário e dois para o tempo. Na opção 2, criaríamos testes que exercitassem as seis combinações que criamos através da permutação.

Se estivéssemos executando testes em uma API, criaríamos os scripts de testes automatizados usando ferramentas capazes de enviar requisições HTTP com os valores de entrada que identificamos nas 6 combinações pelo uso da técnica de partição de equivalência.

Então, validaríamos se as respostas obtidas fossem as esperadas. Já se o caso fosse em que estivéssemos testando uma aplicação com interface gráfica web, desktop ou mobile, usaríamos um framework de manipulação da interface para abrir a aplicação, digitaríamos os valores referentes às 6 combinações que identificamos, submeteríamos o formulário e então avaliaríamos se os valores de resposta são os esperados.

Análise do valor limite

A técnica de análise do valor limite olha para os limites entre as partições de equivalência com o intuito de validar que os operadores condicionais foram usados apropriadamente. Por exemplo, no requisito “funcionários com salário a partir de R$ 3.000,00”, entende-se que o operador a ser usado aqui é “>=”, ou seja, inclusive 3000.

Mas o que aconteceria se eu, como desenvolvedor, colocasse “>”? Com certeza, ao usar 3000 com tempo de 5 anos, teria um retorno false ao invés de true. Pensando nisso, criam-se testes capazes de cobrir os limites entre as partições: para salário, seriam 2999.99,3000.00,4999.99 e 5000.00, e para tempo, seriam 2 e 3.

Logo, o menor e o maior valor dentro da partição, além de uma unidade de valor abaixo do menor valor da partição e uma unidade de valor acima do maior valor da partição.

Aplicação da análise do valor limite (em JavaScript, com Jasmine):

describe(‘Testes para validação de salário’, () => {
it(‘pra cobertura decisão do salário 2999.99 e com 2 anos’, () => { expect(validacaoSalario(2999.99, 2)).toBeFalsy();
});
it(‘pra cobertura decisão do salário 5000.00 e com 2 anos’, () => { expect(validacaoSalario(5000, 2)).toBeFalsy();
});
it(‘pra cobertura decisão do salário 3000 e com 3 anos’, () => { expect(validacaoSalario(3000, 3)).toBeTruthy();
});
it(‘pra cobertura decisão do salário 3000 e com 2 anos’, () => { expect(validacaoSalario(3000, 2)).toBeFalsy();
});
it(‘pra cobertura decisão do salário 4999.99 e com 3 anos’, () => { expect(validacaoSalario(4999.99, 3)).toBeTruthy();
});
it(‘pra cobertura decisão do salário 4999.99 e com 2 anos’, () => { expect(validacaoSalario(4999.99, 2)).toBeFalsy();
});
});

É importante mencionar que testes que envolvem os limites são diferentes dos testes que envolvem partições, visto que os primeiros olham apenas para a intercessão das partições, e os outros olham para a faixa de valores contidos dentro da partição. Por isso, são complementares.

Testes de mutação

Testes de mutação são utilizados para avaliar os testes escritos para uma determinada aplicação. Basicamente, eles alteram o código-fonte e executam os testes para verificar se eles foram capazes de validar que as alterações no código feriram o comportamento esperado. A versão alterada do código é chamada de “mutante”. Abaixo, o Código 1 que sofreu três mutações:

Mutação do método para validação do salário versus tempo de contratação (em JavaScript):

const validacaoSalario = (salario, tempo) => { if (salario > 3000 || salario <= 5000) {
return (tempo == 3) ? true : false;
}
};

Vemos que no Código 7 a mutação modificou alguns dos operadores condicionais que existiam no Código 1:

  • No salário, o operador >= foi substituído pelo >
  • No salário, o operador && foi substituído por ||
  • No salário, o operador < foi substituído por <=
  • No tempo, o operador >= foi substituído por ==

Então, os testes seriam executados novamente e aqueles que continuassem a passar seriam considerados “mutantes sobreviventes”, ou seja, código que possui validações falhas. Se houvesse validações suficientes, os scripts de teste deveriam ter falhado e informado que a mudança no código alterou o comportamento da aplicação.

As técnicas mencionadas neste artigo são passíveis de validação através do uso de scripts de testes automatizados. A utilização desses scripts é muitas vezes mal compreendida, por exemplo, ao tratar essa atividade como um esforço elevado e de pouco valor quando, na verdade, pode ser simples e trazer grande valor ao processo de desenvolvimento.

Numa abordagem manual, usamos depuração dinâmica ou da leitura de logs para entender se o resultado obtido é o esperado. Se esse processo leva três minutos, podemos considerar rápido, mas se temos que executar isso cinco vezes, talvez 15 minutos seja demais.

Se escrevemos um script de teste automatizado em cinco minutos, podemos executá-lo a qualquer momento, em questão de segundos. Temos então um ganho significativo e a possibilidade de validação do software com uso das técnicas sempre que ele for alterado.

A escolha das técnicas depende de diversos fatores, mas, com certeza, os principais são: tempo disponível para testes e nível de risco relacionado ao que estamos desenvolvendo. Por isso, devemos levar em consideração o contexto para dizer como iremos abordar a atividade de testes dentro do time em que atuamos.

Testadores nunca testam tudo, eles avaliam os riscos relacionados ao que se testa e, então, selecionam quais condições têm que ser executadas para evitar que os riscos se materializem em problemas que impactem o cliente ou o negócio. Logo, técnicas ajudam a entender o cenário completo, bem como a análise dos riscos ajuda a identificar quais condições devem ser testadas.

O contexto atual do processo de desenvolvimento de software exige desenvolvedores capazes de criar e validar que o produto se comporta como esperado. Com o uso de técnicas de teste, é possível validar sistematicamente o software sob a perspectiva do código e também da funcionalidade. O uso de scripts de testes automatizados favorece a execução dos testes provenientes do uso das técnicas sempre que o software sofrer modificações e, assim, manter vivas as validações.

Referências