Este artigo foi traduzido por máquina.

Execução de teste

Teste de mutação supersimples

James McCaffrey

Baixar o exemplo de código

image: James McCaffreyA maioria dos testadores que sei tem ouvido falar dos testes de mutação, mas poucos têm realmente executada. Testes de mutação tem a reputação de ser ferramentas de software de terceiros cara difícil e que exige. No entanto, na coluna deste mês, mostrarei como criar uma mutação de super-simple (menos de duas páginas de código e quatro horas de tempo), teste o sistema usando o TRANSLATION FROM VPE FOR CSHARP e o Visual Studio. Mantendo o sistema de testes de mutação simples, você pode obter a maioria dos benefícios de um sistema completo de mutação por uma fração do tempo e esforço.

Testes de mutação são uma maneira de medir a eficácia de um conjunto de casos de teste. A idéia é simple. Suponha que você inicie com 100 casos de teste e seu sistema em teste (SUT) passa todos os testes de 100. Se você modifica o SUT — por exemplo, alterando uma ">" para um "<" ou alterando um "+" para um "-" — você presumivelmente de introduzir um bug no SUT. Agora se você executar novamente seus 100 casos de teste, você esperaria pelo menos uma falha no caso de teste, indicando que um de seus casos de teste pego o código defeituoso. Mas se você não vir nenhuma falha no teste, em seguida, é bastante provável que seu conjunto de casos de teste tenha perdido o código defeituoso e não ponham a SUT.

A melhor maneira para que você possa ver onde eu estou cabeças é olhando Figura 1.

Mutation Testing Demo Run

Figura 1 mutação testes executar Demo

SUT neste exemplo é uma biblioteca denominada MathLib.dll. A técnica que apresento aqui pode ser usada para testar a maioria dos Microsoft.Sistemas de NET Framework inclusive DLLs, aplicativos WinForms, ASP.NET e assim por diante. O sistema de mutação começa pela verificação de código-fonte original para SUT, procurando modifica um código candidato. Meu sistema supersimples procura somente "<" e ">" operadores. O sistema de teste é definido para criar e avaliar as duas mutants. Em um cenário de produção, você criaria provavelmente centenas ou milhares de mutants. A primeira mutante seleciona aleatoriamente um operador de muda, neste caso uma ">" operador no caractere posicionar 189 no código fonte SUT e sofre mutações token para "<". Em seguida, o código de origem DLL mutante baseia-se para criar uma biblioteca de MathLb.dll mutante. Em seguida, o sistema de mutação chama um conjunto de casos de teste a mutante SUT, resultados de log para um arquivo. A segunda iteração cria e testa uma segunda mutante da mesma maneira. O resultado do arquivo de log é:

=============
Número de falhas = 0
Número de falhas do caso de teste = 0 indica fraco possíveis suíte de teste!
=============
Número de falhas = 3
Isso é bom.
=============

A primeira mutante não gera quaisquer falhas no caso de teste, que significa que você deve examinar o código-fonte na posição 189 e determinar por que nenhum dos seus casos de teste exercício desse código.

O SUT

Minha demonstração de prova de mutação supersimples consiste em três projetos do Visual Studio. O primeiro projeto mantém o SUT e nesse caso, é uma biblioteca de classe TRANSLATION FROM VPE FOR CSHARP chamada MathLib. O segundo projeto é um conjunto de teste executável, neste caso um aplicativo de console TRANSLATION FROM VPE FOR CSHARP chamado TestMutation. O terceiro projeto cria e cria um aplicativo de console TRANSLATION FROM VPE FOR CSHARP chamado mutação de mutants, neste caso. Para sua conveniência, coloquei todos os três projetos em um único diretório chamado MutationTesting. Com testes de mutação há muitos arquivos e pastas para controlar e você não deve subestimar o desafio de mantê-los organizados. Para esta demonstração, usei o Visual Studio 2008 (mas qualquer versão do Visual Studio irá funcionar) para criar uma biblioteca de classe MathLib fictícia. O código-fonte inteiro para o manequim SUT é mostrado na Figura 2.

Figura 2 O código-fonte inteiro Dummy SUT

using System;
namespace MathLib
{
  public class Class1
  {
    public static double TriMin(double x, double y, double z)
    {
      if (x < y)
        return x;
      else if (z > y)
        return y;
      else
        return z;
    }
  }
}

Observe que eu mantido o nome da classe padrão de Class1. A classe contém um único método estático TriMin, que retorna a menor das três parâmetros de tipo double. Observe também que o SUT está deliberadamente incorreto. Por exemplo, se x = 2. 0, y = 3. 0 e z = 1. 0, o método TriMin retorna 2. 0 em vez do valor correto de 1. 0. No entanto, é importante observar que a mutação testes não nãomede diretamente a exactidão do SUT; mutação teste mede a eficácia de um conjunto de casos de teste. Após compilar o SUT, a próxima etapa é salvar uma cópia de linha de base do arquivo de origem, Class1. cs, no diretório raiz do sistema de testes de mutação. A idéia é que cada mutante é uma única modificação do código-fonte original do SUT, e então, uma cópia da origem SUT deve ser mantida. Neste exemplo salvei a fonte original como Class1 Original.cs no diretório C:\MutationTesting\Mutation.

O conjunto de teste

Em algumas situações de teste, você pode ter um conjunto existente de dados de caso de teste e, em algumas situações existe um utilitário de teste. Esta mutação supersimples sistema de teste, criei um TRANSLATION FROM VPE FOR CSHARP console application equipamento de teste chamado TestMutation. Após criar o projeto no Visual Studio, adicionei uma referência para o SUT: MathLib.dll localizado em C:\MutationTesting\MathLib\bin\Debug. O código-fonte inteiro para o projeto de conjunto de teste é apresentado na Figura 3.

Figura 3 O equipamento de teste e dados de teste

using System;
using System.IO;

namespace TestMutation
{
  class Program
  {
    static void Main(string[] args)
    {
      string[] testCaseData = new string[]
        { "1.0, 2.0, 3.0, 1.0",
          "4.0, 5.0, 6.0, 4.0",
          "7.0, 8.0, 9.0, 7.0"};

      int numFail = 0;

      for (int i = 0; i < testCaseData.Length; ++i) {
        string[] tokens = testCaseData[i].Split(',');
        double x = double.Parse(tokens[0]);
        double y = double.Parse(tokens[1]);
        double z = double.Parse(tokens[2]);
        double expected = double.Parse(tokens[3]);

        double actual = MathLib.Class1.TriMin(x, y, z);
        if (actual != expected) ++numFail;
      }

      FileStream ofs = new FileStream("..
\\..
\\logFile.txt",
        FileMode.Append);
      StreamWriter sw = new StreamWriter(ofs);
      sw.WriteLine("=============");
      sw.WriteLine("Number failures = " + numFail);
      if (numFail == 0)
        sw.WriteLine(
          "Number test case failures = " +
          "0 indicates possible weak test suite!");
      else if (numFail > 0)
        sw.WriteLine("This is good.");
      sw.Close(); ofs.Close();
    }
  }
}

Observe que o conjunto de teste possui três casos de teste codificado. Em um ambiente de produção, você provavelmente teria várias centenas de casos de teste, armazenados em um arquivo de texto e você poderia passar o nome do arquivo principal como args [0]. Primeiro caso de teste, "1. 0, 2. 0, 3. 0, 1. 0," representa o x, y e z parâmetros (1. 0, 2. 0 e 3. 0), seguido do resultado esperado (1. 0) para o método TriMin a SUT. É óbvio que o conjunto de teste é inadequado: cada um dos três casos de teste é basicamente equivalente e tem o menor valor como parâmetro x. Mas se você examinar o SUT original, você verá que na verdade passariam todos os três casos de teste. Nosso sistema de testes de mutação detectará o ponto fraco do conjunto de teste?

O conjunto de teste itera em cada caso de teste analisa os parâmetros de entrada e o valor de retorno esperado, chama o SUT com os parâmetros de entrada, busca o valor de retorno real, compara o retorno real com o retorno esperado para determinar um resultado de aprovação/reprovação do caso de testee então acumula o número total de falhas do caso de teste. Lembre-se de que nos testes de mutação, estamos interessados principalmente em se houver pelo menos uma falha de nova, em vez de testar quantas ocorrências passam. O conjunto de teste grava o arquivo de log na pasta raiz do programa de chamada.

O sistema de testes de mutação

Nesta seção, eu vou orientá-lo a mutação testes program uma linha por vez, mas omitir a maioria das instruções WriteLine usadas para produzir a saída mostrada na Figura 1. Eu criei um aplicativo de console TRANSLATION FROM VPE FOR CSHARP chamado mutação no diretório raiz do MutationTesting. O programa começa com:

using System;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Threading;

namespace Mutation
{
  class Program
  {
    static Random ran = new Random(2);
    static void Main(string[] args)
    {
      try
      {
        Console.WriteLine("\nBegin super-simple mutation testing demo\n");
...

A finalidade do objeto Random é gerar uma posição aleatória de mutação. Usei um valor de semente de 2, mas qualquer valor funcionará bem. Em seguida, configuro os locais de arquivo:

string originalSourceFile = "..
\\..
\\Class1-Original.cs"; 
string mutatedSourceFile = "..
\\..
\\..
\\MathLib\\Class1.cs";
string mutantProject = "..
\\..
\\..
\\MathLib\\MathLib.csproj";
string testProject = "..
\\..
\\..
\\TestMutation\\TestMutation.csproj";
string testExecutable = 
  "..
\\..
\\..
\\TestMutation\\bin\\Debug\\TestMutation.exe";
string devenv =
  "C:\\Program Files (x86)\\Microsoft Visual Studio 9.0\\Common7\\IDE\\
  devenv.exe"; 
...

Você verá como cada um desses arquivos é usada em breve. Observe que eu apontem para o programa de devenv. exe associado ao Visual Studio 2008. Em vez de codificar nesse local, eu poderia ter feito uma cópia do devenv. exe e o colocou dentro da pasta de raiz do sistema de mutação.

O programa continua:

List<int> positions = GetMutationPositions(originalSourceFile);
int numberMutants = 2;
...

Eu chamo um auxiliar método GetMutationPositions para examinar o arquivo de código-fonte original e armazenar as posições de caractere de todos os "<" e ">" caracteres em uma lista e definir o número de mutants para criar e testar a dois.

O loop de processamento principal é:

for (int i = 0; i < numberMutants; ++i) {
  Console.WriteLine("Mutant # " + i);
  int randomPosition = positions[ran.Next(0, positions.Count)];
  CreateMutantSource(originalSourceFile, randomPosition, mutatedSourceFile);

  try {
    BuildMutant(mutantProject, devenv);
    BuildTestProject(testProject, devenv);
    TestMutant(testExecutable);
  }
  catch {
    Console.WriteLine("Invalid mutant.
Aborting.");
    continue;
  }
}
...

Dentro do loop, o programa busca uma posição aleatória de um caractere de modifica da lista de posições possíveis e, em seguida, chama métodos auxiliares para gerar código de origem mutante Class1. cs, construir o mutante correspondente, MathLib.dll, recriar a estrutura de teste para que ele usa o novo mutante e, em seguida, testar o mutante DLL, na esperança de gerar um erro. Porque ele é bem possível que código-fonte que sofreram mutação pode não ser válido, concluir a tentativa de criar e testar em uma instrução try-catch, portanto, pode anular a testes de código não compilável.

O método Main conclui como:

...
Console.WriteLine("\nMutation test run complete");
  }
  catch (Exception ex) {
    Console.WriteLine("Fatal: " + ex.Message);
  }
} // Main()

Criando o código de origem mutante

O método auxiliar para obter uma lista das posições possíveis mutação é:

static List<int> GetMutationPositions(string originalSourceFile)
{
  StreamReader sr = File.OpenText(originalSourceFile);
  int ch = 0; int pos = 0;
  List<int> list = new List<int>();
  while ((ch = sr.Read()) != -1) {
    if ((char)ch == '>' || (char)ch == '<')
      list.Add(pos);
    ++pos;
  }
  sr.Close();
  return list;
}

O método vem por meio de um caractere de código fonte, cada vez maior procurando-que e menor-que operadores e adicionando a posição de caractere a uma coleção de lista. Observe que uma limitação deste sistema de mutação supersimples, conforme apresentado é que ele só pode modifica os tokens de caractere único, como ">" ou "+" e não pode lidar com tokens multicharacter como "> =". O método auxiliar para realmente mudar o código-fonte SUT está listado na Figura 4.

Figura 4 O método de CreateMutantSource.

static void CreateMutantSource(string originalSourceFile,
  int mutatePosition, string mutatedSourceFile)
{
  FileStream ifs = new FileStream(originalSourceFile, FileMode.Open);
  StreamReader sr = new StreamReader(ifs);
  FileStream ofs = new FileStream(mutatedSourceFile, FileMode.Create);
  StreamWriter sw = new StreamWriter(ofs);
  int currPos = 0;
  int currChar;
 
  while ((currChar = sr.Read()) != -1)
  {
    if (currPos == mutatePosition)
    {
      if ((char)currChar == '<') {
        sw.Write('>');
      }
      else if ((char)currChar == '>') {
        sw.Write('<');
      }
      else sw.Write((char)currChar);
    }
    else
       sw.Write((char)currChar);

    ++currPos;
   }
 
  sw.Close(); ofs.Close();
  sr.Close(); ifs.Close();
}

O método CreateMutantSource aceita o arquivo de código fonte original, que foi salvo anteriormente, sem uma posição de caractere muda e o nome e o local do arquivo resultante mutante para salvar. Aqui eu apenas procurar "<" e ">" caracteres, mas você pode querer considerar outras mutações. Em geral, ser mutações produzirão válido de origem, então, por exemplo, você não alterará ">" como "=". Além disso, a mutação em mais de um local não uma boa idéia, porque apenas um das mutações pode gerar uma falha de caso de teste de novo, sugerindo que o conjunto de teste é boa quando na verdade não poderá. Algumas mutações não terá nenhum efeito prático (por exemplo, a mutação de um caractere dentro de um comentário) e alguns mutações produzirá código inválido (como alterar o ">>" operador de deslocamento para "><").

Criar e testar o mutante

O método auxiliar do BuildMutant é:

static void BuildMutant(string mutantSolution, string devenv)
{
  ProcessStartInfo psi =
    new ProcessStartInfo(devenv, mutantSolution + " /rebuild");
  Process p = new Process();
      
  p.StartInfo = psi; p.Start();
  while (p.HasExited == false) {
    System.Threading.Thread.Sleep(400);
    Console.WriteLine("Waiting for mutant build to complete .
. "
);
  }
  p.Close();
}

Eu uso um objeto de processo para chamar o programa devenv. exe para recriar a solução do Visual Studio que abriga o código de origem de Class1. cs sofrendo mutação e produz o mutante MathLib.dll. Sem argumentos, devenv. exe inicia o Visual Studio IDE, mas quando passados argumentos, devenv pode ser usado para reconstruir soluções ou projetos. Observe que eu uso um loop de atraso, pausando a cada 400 milissegundos, para dar tempo devenv. exe para concluir a criação do mutante DLL; Caso contrário, o sistema de mutação poderá tentar testar o mutante SUT antes de ele é criado.

O método auxiliar para recriar a estrutura de teste é:

static void BuildTestProject(string testProject, string devenv)
{
  ProcessStartInfo psi =
    new ProcessStartInfo(devenv, testProject + " /rebuild");
  Process p = new Process();

  p.StartInfo = psi; p.Start();
  while (p.HasExited == false) {
    System.Threading.Thread.Sleep(500);
    Console.WriteLine("Waiting for test project build to complete .
. "
);
  }
  p.Close();
}

A principal idéia aqui é que, por reconstruir o projeto de teste, o novo mutante SUT será usado quando o equipamento de teste é executado em vez do mutante usado anteriormente SUT. Se seu código-fonte mutante for inválido, BuildTestProject lançará uma exceção.

A última parte da mutação supersimples sistema de teste é o método auxiliar para invocar o conjunto de teste:

...
static void TestMutant(string testExecutable)
    {
      ProcessStartInfo psi = new ProcessStartInfo(testExecutable);
      Process p = new Process(); p.StartInfo = psi;
      p.Start();
      while (p.HasExited == false)
        System.Threading.Thread.Sleep(200);

      p.Close();
    } 

  } // class Program
} // ns Mutation

Como mencionei anteriormente, a estrutura de teste usa um nome de arquivo de log codificados e local; Você pode parametrizar que passar informações como um parâmetro para TestMutant e colocando-o dentro do processo inicia info, onde ele será aceito pelo equipamento de teste de TestMutation.exe.

Um mundo Real, sistema de testes de mutação de trabalhar

Testes de mutação são simples, em princípio, mas os detalhes da criação de um sistema de testes de mutação completo são um desafio. No entanto, mantendo o sistema de mutação tão simples quanto possível e utilizando o Visual Studio e devenv. exe, você pode criar uma mutação surpreendentemente eficiente testes de sistema para.NET SUTs. Usando o exemplo apresentado aqui, você deve ser capaz de criar uma sistema de teste para suas própria SUTs de mutação. A principal limitação a mutação de amostra, sistema de teste é que, porque o sistema é baseado em alterações de caractere único, você não pode executar facilmente mutações dos operadores de multicharacter, como a alteração "> =" para seu "<" operador do complemento. Outra limitação é que o sistema só fornece a posição do caractere de mutação, para que ele não fornece uma maneira fácil de diagnosticar um mutante. Apesar dessas limitações, meu sistema de exemplo foi usado com êxito para medir a eficácia das suítes de teste para vários sistemas de software de empresas de médio porte.

Recuperação de desastresJames McCaffreytrabalha para a Volt Information Sciences Inc., onde gerencia o treinamento técnico para engenheiros de software, trabalhando com a Microsoft Redmond, Wash, campus. Ele trabalhou em vários produtos da Microsoft, incluindo o Internet Explorer e o MSN Busca. Dr. McCaffrey é o autor de ".NET Test Automation Recipes"(Apress, 2006) e pode ser alcançado em jammc@microsoft.com.

Obrigado aos seguintes especialistas técnicos da Microsoft para revisão deste artigo: Paul Koch, Dan Liebling e Shane Williams