Diretriz: Idéias de Teste para Chamadas de Método
As idéias de teste são baseadas em defeitos de software plausíveis e em como esses defeitos podem ser melhor resolvidos. Esta diretriz descreve um método para detectar casos em que o código não manipula o resultado da chamada de um método.
Relacionamentos
Elementos Relacionados
Descrição Principal

Introdução

Um exemplo de código com defeito é:

File file = new File(stringName);
file.delete();

O defeito é que File.delete pode falhar, mas o código não verifica isso. A correção requer a adição do código em itálico mostrado aqui:

File file = new File(stringName);



if (file.delete()


== false) {...}

Esta diretriz descreve um método para detectar casos em que o código não manipula o resultado da chamada de um método. (Observe que pressupõe-se que o método chamado produza o resultado correto para quaisquer que sejam as entradas fornecidas. Isso é algo que deve ser testado, mas a criação de idéias de teste para o método chamado é uma tarefa distinta. Ou seja, não é função testar File.delete.)

A noção fundamental é que você deve criar uma idéia de teste para cada resultado relevante distinto não manipulado de uma chamada de método. Para definir esse termo, primeiro vamos observar o resultado. Quando um método é executado, ele altera o estado de tudo. A seguir são apresentados alguns exemplos:

  • Ele pode alocar os valores de retorno para a pilha em tempo de execução.
  • Ele pode retornar a execução de uma exceção.
  • Ele pode alterar uma variável global.
  • Ele pode atualizar um registro de um banco de dados.
  • Ele pode enviar dados pela rede.
  • Ele pode imprimir uma mensagem para a saída padrão.

Agora vamos observar relevante mais uma vez, utilizando alguns exemplos.

  • Suponha que o método chamado imprima uma mensagem para a saída padrão. Isso "altera o estado do mundo", mas não pode afetar o processamento adicional deste programa. Nada do que for impresso, mesmo que nada seja impresso, afetará a execução do código.
  • Se o método retornar verdadeiro para êxito e falso para falha, é muito provável que o programa se ramifique com base no resultado. Assim, esse valor de retorno é relevante.
  • Se o método chamado atualizar um registro do banco de dados que o código for ler e usar posteriormente, o resultado (atualização do registro) será relevante.

(Não há uma linha absoluta entre relevante e irrelevante. Chamando print, o método pode causar a alocação de buffers e essa alocação pode ser relevante após o retorno de print. É concebível que um defeito dependa se um buffer foi alocado e do que foi alocado em um buffer. É concebível, mas é plausível?)

Geralmente, um método pode ter um grande número de resultados, mas apenas alguns deles serão distintos. Por exemplo, considere um método que grava bytes em disco. Ele pode retornar um número menor que zero para indicar uma falha; caso contrário, retorna o número de bytes gravados (que pode ser um número menor do que o solicitado). As inúmeras possibilidades podem ser agrupadas em três resultados distintos:

  • um número menor que zero.
  • o número gravado é igual ao número solicitado.
  • alguns bytes foram gravados, mas menos do que o número solicitado.

Todos os valores menores que zero são agrupados em um resultado porque nenhum programa razoável fará distinção entre eles. Todos eles (se, de fato, for possível haver mais de um) devem ser tratados como erro. De modo semelhante, se o código solicitar a gravação de 500 bytes, não importará se forem gravados efetivamente 34 ou 340: o mesmo provavelmente ocorrerá com os bytes não gravados. (Se for necessário fazer algo diferente para algum valor, como 0, isso formará um novo resultado distinto.)

Resta explicar uma última palavra do termo de definição. Essa técnica de teste específica não está interessada nos resultados distintos que já foram manipulados. Considere, uma vez mais, este código:

File file = new File(stringName);
if (file.delete() == false) {...}

Há dois resultados distintos (verdadeiro e falso). O código os manipula. Ele poderá manipulá-los incorretamente, mas as idéias de teste em Diretriz do Produto de Trabalho: Idéias de Teste para Expressões Booleanas e Limites verificarão isso. Essa técnica de teste está interessada nos resultados distintos que não são manipulados especificamente por código distinto. Isso pode ocorrer por duas razões: você achou que a distinção era irrelevante ou simplesmente a negligenciou. Um exemplo do primeiro caso é:

result = m.method();
switch (result) {
    case FAIL:
    case CRASH:
       ...
       break;
    case DEFER:
       ...
       break;
    default:
       ...
       break;
}

FAIL CRASH são manipulados pelos mesmo código. Convém verificar se isso é realmente apropriado. Um exemplo de uma distinção negligenciada é:

result = s.shutdown();
if (result == PANIC) {
   ...
} else {
   // success! Shut down the reactor.
   ...
} 

Deduz-se que o encerramento pode retornar um resultado distinto adicional: RETRY. O código escrito trata esse caso da mesma maneira que o caso de êxito, o que provavelmente está errado.

Localizando Idéias de Teste

Assim, a sua meta é considerar os resultados relevantes distintos que negligenciou anteriormente. Isso parece impossível: por que você chegaria à conclusão de que são relevantes agora se não achou que fossem anteriormente?

A resposta é que um novo exame sistemático do código, do ponto de vista de teste e não de programação, às vezes pode gerar uma nova forma de raciocínio. Você pode questionar as próprias suposições através de uma análise metódica das etapas do código, observando os métodos chamados, verificando novamente a respectiva documentação e pensando. A seguir estão alguns casos a serem considerados.

Casos "Impossíveis"

Geralmente, retornos de erro parecem impossíveis. Reavalie suas suposições.

Este exemplo mostra uma implementação em Java de um sabor Unix comum para a manipulação de arquivos temporários.

File file = new File("tempfile");
FileOutputStream s;
try {
    // open the temp file.
    s = new FileOutputStream(file);
} catch (IOException e) {...}
// Make sure temp file will be deleted
file.delete();

A meta é certificar-se de que os arquivos temporários sejam sempre excluídos, independentemente da forma de saída do programa. Para fazer isso, crie o arquivo temporário e, depois, exclua-o imediatamente. No Unix, você pode continuar a trabalhar com o arquivo excluído e o sistema operacional se encarregará da limpeza ao término do processo. Uma programadora de Unix pode não gravar o código para verificar uma exclusão que falhou. Como acaba de criar o arquivo com êxito, ela deverá conseguir exclui-lo.

Esse truque não funciona no Windows. A exclusão falhará porque o arquivo está aberto. Descobrindo que o fato é difícil: até agosto de 2000, a documentação Java não enumerava as situações em que delete poderia falhar; apenas informava que podia falhar. Mas, talvez, quando estiver no "modo de teste", o programador possa questionar sua suposição. Como supõe-se que o código seja "gravado uma vez e executado em qualquer lugar", pode-se perguntar a um programador para ambientes Windows quando File.delete pode falhar no Windows e, assim, descobrir a verdade.

Casos "Irrelevantes"

Outro ponto que impede a detecção de um valor relevante distinto é o fato de já estar convencido de que ele não é importante. Um método Java Comparator de comparação retorna um número <0, 0 ou um número >0. Estes são três casos distintos que podem ser tentados. Este código agrupa dois deles:

void allCheck(Comparator c) {
   ...
   if (c.compare(o1, o2) <= 0) {
      ...
   } else {
      ...
   }

Talvez isso esteja incorreto. A maneira de descobrir se é menor ou igual a 0 é testar os dois casos separadamente, mesmo que você acredite realmente que não faz diferença. Suas convicções são realmente o que você está testando. Note que você pode estar executando o caso then da instrução if mais de uma vez por outras razões. Por que não tentar uma delas com o resultado menor que 0 e uma com o resultado igual a zero?

Exceções Não Capturadas

As exceções são um tipo de resultado distinto. Como referência, considere este código:

void process(Reader r) {
   ...
   try {
      ...
      int c = r.read();
      ...
   } catch (IOException e) {
      ...
   }
}

Você esperaria verificar se o código adota realmente o procedimento correto em caso de falha de leitura. Mas suponha que uma exceção não seja manipulada explicitamente. Em vez disso, é possível que ela se propague para cima no código em teste. Em Java, isso poderia ter esta aparência:

void process(Reader r) 


throws IOException {
    ...
    int c = r.read();
    ...
}

Esta técnica solicita a você para testar esse caso ainda que o código não o manipule explicitamente. Por quê? Devido a este tipo de erro:

void process(Reader r) throws IOException {
    ...
    


Tracker.hold(this);
    ...
    int c = r.read();
    ...
    


Tracker.release(this);
    ...
}

Aqui, o código afeta o estado global (por meio de Tracker.hold). Se a exceção for emitida, Tracker.release nunca será chamado.

(Observe que a falha na liberação do método release provavelmente não terá conseqüências imediatas óbvias. O problema provavelmente não estará mais visível até que o processo seja chamado novamente, em que a tentativa suspender o objetivo por uma segunda vez falhará. Um bom artigo sobre esses defeitos é "Testing for Exceptions" de Keith Stobie.   (Obtenha o Adobe Reader))

Falhas Não Descobertas

Esta técnica específica não considera todos os defeitos associados a chamadas de método. A seguir estão dois tipos que provavelmente não serão detectados.

Argumentos Incorretos

Considere estas duas linhas de código C, onde a primeira está correta e a segunda está incorreta.

... strncmp(s1, s2, strlen(s1)) ...
... strncmp(s1, s2, strlen(


s2)) ...

strncmp compara duas cadeias e retorna um número menor que 0 se a primeira for lexicograficamente menor que a segunda (seria apresentada primeiro em um dicionário). Ele retorna um "0" se forem iguais. Ele retorna um número maior que 0 se a primeira for lexicograficamente maior. Entretanto, ele apenas compara o número de caracteres fornecidos pelo terceiro argumento. O problema é que o comprimento da primeira seqüência de caracteres é usado para limitar a comparação, enquanto o comprimento da segunda é que deveria impor essa limitação.

Essa técnica exigiria três testes, um para cada valor de retorno distinto. A seguir estão três que podem ser usados:

s1 s2 resultado esperado resultado real
"a" "bbb" <0 <0
"bbb" "a" >0 >0
"foo" "foo" =0 =0

O defeito não é descoberto porque nada nessa técnica força o terceiro argumento a ter qualquer valor específico. É necessário um caso de teste como este:

s1 s2 resultado esperado resultado real
"foo" "food" <0 =0

Ainda que existam técnicas adequadas para detectar esses defeitos, elas raramente são usadas na prática. Provavelmente, será melhor empregar o esforço de teste em um conjunto abrangente de testes para detectar vários tipos de defeitos (e que detecte esse tipo como um efeito colateral).

Resultados Indistintos

Os processos de codificação e de teste, método por método, apresenta um perigo. Eis um exemplo. Há dois métodos. O primeiro, connect, deseja estabelecer uma conexão de rede:

void connect() {
   ...
   Integer portNumber = serverPortFromUser();
   if (portNumber == null) {
      // pop up message about invalid port number
      return;
   }

Quando ele precisa de um número de porta, ela chama serverPortFromUser. Esse método retorna dois valores distintos. Ele retornará um número de porta escolhido pelo usuário se o número escolhido for válido (1000 ou mais). Caso contrário, retornará null (nulo). Se nulo for retornado, o código em teste exibirá uma mensagem de erro e terminará.

Quando connect foi testado, ele funcionou conforme planejado: um número de porta válido estabeleceu uma conexão e um número inválido conduziu a um pop-up.

O código para serverPortFromUser é um bit mais complicado. Primeiro ele exibe uma janela que solicita uma seqüência de caracteres e possui os botões OK e CANCEL padrão. Há quatro casos baseados na ação do usuário:

  1. Se o usuário digitar um número válido, esse número será retornado.
  2. Se o número for muito pequeno (menor que 1000), será retornado nulo (assim, será exibida a mensagem sobre o número de porta inválido).
  3. Se o número estiver formatado de maneira incorreta, também será retornado nulo (e a mesma mensagem será exibida).
  4. Se o usuário clicar em CANCEL, será retornado nulo.

Esse código também funciona conforme o esperado.

A combinação dos dois fragmentos de código tem uma conseqüência ruim: o usuário pressiona CANCEL e recebe uma mensagem sobre um número de porta inválido. Todo o código funciona conforme o esperado, mas o efeito geral ainda está incorreto. Ele foi testado de maneira razoável, mas um defeito não foi detectado.

O problema aqui é que nulo é um resultado que representa dois significados distintos ("valor inválido" e "cancelado pelo usuário"). Nada nessa técnica força você a observar esse problema no design de serverPortFromUser.

Contudo, os testes podem ajudar. Quando serverPortFromUser é testado isoladamente - apenas para verificar se retorna o valor desejado em cada um dos quatro casos - o contexto de uso se perde. Em vez disso, suponha que fosse testado utilizando connect. Há quatro testes que experimentariam ambos os métodos simultaneamente:

entrada resultado esperado processo de raciocínio
"1000" tipos de usuário é aberta a conexão com a porta 1000 serverPortFromUser retorna um número, que é utilizado.

"999" tipos de usuário

mensagem sobre número de porta inválido

serverPortFromUser retorna nulo, que conduz a um pop-up

"i99" tipos de usuário

mensagem sobre número de porta inválido serverPortFromUser retorna nulo, que conduz a um pop-up
o usuário clica em CANCEL todo o processo de conexão deve ser cancelado serverPortFromUser retorna nulo, aguarda um minuto, o que não faz sentido...

Como ocorre com freqüência, a realização de testes em um contexto mais amplo revela problemas de integração que não ocorrem em testes de pequena escala. E, como também ocorre freqüentemente, um raciocínio criterioso durante o design do teste revelará o problema antes da sua execução. (Mas se o defeito não for detectado nesse momento, ele será durante a execução do teste.)