Use shims para isolar seu aplicativo para testes unitários

Shim types, uma das duas principais tecnologias utilizadas pelo Microsoft Fakes Framework, são fundamentais para isolar os componentes do aplicativo durante o teste. Eles funcionam interceptando e desviando chamadas para métodos específicos, que você pode direcionar para o código personalizado em seu teste. Esse recurso permite que você gerencie o resultado desses métodos para garantir que os resultados sejam consistentes e previsíveis durante cada chamada, independentemente das condições externas. Esse nível de controle simplifica o processo de teste e ajuda a obter resultados mais confiáveis e precisos.

Empregue shims quando precisar criar um limite entre seu código e assemblies que não fazem parte da sua solução. Quando o objetivo é isolar os componentes da sua solução uns dos outros, o uso de stubs é recomendado. Para obter uma descrição detalhada sobre stubs, consulte Usar stubs para isolar partes do aplicativo umas das outras para teste de unidade.

Este artigo fornece um guia passo a passo para usar tipos de shim para desviar chamadas para métodos específicos em seu código de teste de unidade.

Entender as limitações de tipo com shims

Há algumas limitações ao trabalhar com shims. Eles não podem ser usados em todos os tipos de determinadas bibliotecas na classe base .NET, especificamente:

  • mscorlib e System no .NET Framework
  • System.Runtime no .NET Core ou .NET 5+

Ao projetar sua estratégia de teste, planeje essa restrição para garantir um teste de unidade bem-sucedido e eficaz.

Criar um shim

Suponha que seu componente contenha chamadas para o System.IO.File.ReadAllLines método:

// Code under test:
this.Records = System.IO.File.ReadAllLines(path);

Para preparar o componente para teste de unidade, conclua as etapas nos procedimentos a seguir.

Criar uma biblioteca de classes

Crie uma nova solução e um projeto inicial para a biblioteca de classes.

  1. Na janela Visual Studio Start (File>Start Window), crie um projeto Class Library selecionando o modelo para C# ou Visual Basic.

    Captura de tela do modelo de projeto Biblioteca de Classes para .NET Framework e C# no Visual Studio.

    Captura de tela do modelo de projeto Biblioteca de Classes para .NET Framework e C# no Visual Studio 2022.

  2. Configure o novo projeto:

    • Defina a biblioteca de classes Project name como HexFileReader.
    • Defina o nome da solução como ShimsTutorial.
    • Defina a estrutura Target como .NET Framework 4.8.

    Selecione Criar.

  3. Depois que o projeto for aberto, localize o arquivo padrão Class1.cs em Gerenciador de Soluções e exclua o arquivo.

  4. Adicione um arquivo nomeado HexFile.cs e insira a seguinte definição de classe:

    // HexFile.cs
    public class HexFile
    {
        public string[] Records { get; private set; }
    
        public HexFile(string path)
        {
            this.Records = System.IO.File.ReadAllLines(path);
        }
    }
    

Adicionar um projeto de teste de unidade

Adicione outro projeto à sua solução para os testes de unidade.

  1. Em Gerenciador de Soluções, clique com o botão direito do mouse na solução ShimsTutorial e selecione Add>New Project.

  2. Na Janela Inicial, crie um Unit Test Project projeto selecionando o modelo.

    Screenshot do modelo de projeto Microsoft Unit Test para .NET Framework e C# em Visual Studio.

    Screenshot do modelo de projeto Microsoft Unit Test para .NET Framework e C# no Visual Studio 2022.

  3. Configure o novo projeto:

    • Defina o teste de unidade Project name como TestProject.
    • Defina a estrutura Target como .NET Framework 4.8.

    Selecione Criar.

Adicionar Assembly de Fakes

Adicione uma referência ao projeto HexFileReader.

  1. Em Gerenciador de Soluções, expanda o nó TestProject, clique com o botão direito do mouse no nó References e selecione Add Reference.

    Screenshot que mostra como adicionar uma referência do projeto de teste de unidade à classe em Visual Studio.

    Screenshot que mostra como adicionar uma referência do projeto de teste de unidade à classe no Visual Studio 2022.

    1. No painel esquerdo da janela do Gerenciador de Referência , selecione a seção Projetos .

    2. No painel do meio, marque a caixa de seleção referente ao projeto HexFileReader, e selecione OK.

  2. Adicione a biblioteca Fakes.

    1. Em Gerenciador de Soluções, localize o nó que contém o assembly:

      • Para um projeto mais antigo do .NET Framework (estilo não SDK), expanda o nó do projeto de teste de unidade e, em seguida, expanda o nó References.

      • Para um projeto no estilo SDK destinado ao .NET Framework, .NET Core ou .NET 5+, expanda o nó Dependencies para localizar o assembly a ser simulado em Assemblies, Projects ou Packages.

      • No Visual Basic, selecione Mostrar Todos os Arquivos na barra de ferramentas do Gerenciador de Soluções para ver o nó Referências.

    2. Selecione o System assembly que contém a definição de System.IO.File.ReadAllLines.

    3. Clique com o botão direito do mouse no nó System e selecione Adicionar assembly de simulação.

      Captura de tela que mostra como adicionar o assembly Fakes ao projeto no Visual Studio.

      Captura de tela que mostra como adicionar o assembly Fakes ao projeto no Visual Studio 2022.

    O processo de compilação gera avisos e erros para tipos que não têm suporte para uso com shims.

    Quando a compilação for concluída, o Gerenciador de Soluções é atualizado para exibir um nó Fakes no projeto de teste de unidade.

  3. Selecione o Fakes\mscorlib.fakes arquivo e substitua o XML para excluir os tipos sem suporte:

    <Fakes xmlns="http://schemas.microsoft.com/fakes/2011/" Diagnostic="true">
    <Assembly Name="mscorlib" Version="4.0.0.0"/>
    <StubGeneration>
         <Clear/>
    </StubGeneration>
    <ShimGeneration>
         <Clear/>
         <Add FullName="System.IO.File"/>
         <Remove FullName="System.IO.FileStreamAsyncResult"/>
         <Remove FullName="System.IO.FileSystemEnumerableFactory"/>
         <Remove FullName="System.IO.FileInfoResultHandler"/>
         <Remove FullName="System.IO.FileSystemInfoResultHandler"/>
         <Remove FullName="System.IO.FileStream+FileStreamReadWriteTask"/>
         <Remove FullName="System.IO.FileSystemEnumerableIterator"/>
    </ShimGeneration>
    </Fakes>
    

Criar um teste de unidade

Adicione um teste de unidade para seu projeto.

  1. Atualize o arquivo padrão TestProject\UnitTest1.cs fornecido pelo modelo de projeto.

    Localize a seguinte seção de código no arquivo e substitua-a pelo snippet de código fornecido:

    [TestMethod]
    public void TestMethod1()
    {
    }
    

    Snippet de substituição:

    [TestMethod]
    public void TestFileReadAllLine()
    {
       using (ShimsContext.Create())
       {
          // Arrange
          System.IO.Fakes.ShimFile.ReadAllLinesString = (s) => new string[] { "Hello", "World", "Shims" };
    
          // Act
          var target = new HexFile("this_file_doesnt_exist.txt");
    
          Assert.AreEqual(3, target.Records.Length);
       }
    }
    
  2. Para ver todos os assemblies do Fakes do teste de unidade, selecione Show All Files na barra de menus do Gerenciador de Soluções:

    Captura de tela do Gerenciador de Soluções no Visual Studio 2022 mostrando todos os arquivos, incluindo os assemblies Fakes.

  3. Abra o Gerenciador de Testes e execute o teste.

É fundamental descartar adequadamente cada contexto de shim. Como regra geral, chame o método ShimsContext.Create dentro de uma instrução using para garantir a limpeza correta dos shims registrados. Por exemplo, você pode registrar um shim para um método de teste que substitui o DateTime.Now método por um delegado que sempre retorna 1º de janeiro de 2000. Se você esquecer de limpar o shim registrado no método de teste, o restante da execução do teste sempre retornará 1º de janeiro de 2000 como valor de DateTime.Now. Esse resultado pode ser surpreendente e confuso.

Revisar convenções de nomenclatura para classes shim

Os nomes de classe Shim usam o Fakes.Shim prefixo seguido pelo nome de tipo original. Os nomes de parâmetro são acrescentados ao nome do método. (Você não precisa adicionar nenhuma referência de assembly a System.Fakes.)

System.IO.File.ReadAllLines(path);

System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };

Entender como funcionam os shims

Os shims funcionam introduzindo desvios de execução no código do aplicativo que está sendo testado. Sempre que há uma chamada para o método original, o sistema Fakes intervém para redirecionar a chamada, fazendo com que seu código shim personalizado seja executado em vez do método original.

É importante observar que esses desvios são criados e removidos dinamicamente no runtime. Os desvios devem sempre ser criados dentro do ciclo de vida de um ShimsContext. Quando o ShimsContext é descartado, todos os shims ativos criados dentro dele também são removidos. Para gerenciar essa estrutura de forma eficiente, recomenda-se encapsular a criação de desvios dentro de uma instrução using.

Explorar shims para diferentes tipos de métodos

Os shims dão suporte a vários tipos de métodos.

Métodos estáticos

Quando você aplica shims a métodos estáticos, as propriedades que armazenam os shims ficam contidas em um tipo de shim. Essas propriedades possuem apenas um setter, que é usado para anexar um delegado ao método de destino.

Por exemplo, para uma classe chamada MyClass com um método MyMethodestático:

//Code under test
public static class MyClass {
    public static int MyMethod() {
        ...
    }
}

Você pode anexar um shim a MyMethod de modo que ele retorne constantemente 5:

// Unit test code
ShimMyClass.MyMethod = () => 5;

Métodos de instância (para todas as instâncias)

Assim como os métodos estáticos, os métodos de instância também podem ser shimmed para todas as instâncias. As propriedades que agrupam esses shims são colocadas em um tipo aninhado chamado AllInstances para evitar confusão.

Para a classe MyClass com um método de instância MyMethod:

// Code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

Você pode anexar um shim a MyMethod para que ele sempre retorne 5, independentemente da instância:

// Unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;

O snippet a seguir mostra a estrutura de tipo gerada de ShimMyClass:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
   public static class AllInstances {
      public static Func<MyClass, int>MyMethod {
         set {
            ...
         }
      }
   }
}

Nesse cenário, o Fakes passa a instância de runtime como o primeiro argumento do delegado.

Métodos de instância (uma única instância em tempo de execução)

Os métodos de instância também podem ser shimmed usando delegados diferentes, dependendo do receptor da chamada. Essa abordagem permite que o mesmo método de instância apresente comportamentos diferentes em cada instância do tipo. As propriedades que mantêm esses shims são métodos de instância do próprio tipo de shim. Cada tipo de shim instanciado está vinculado a uma instância bruta de um tipo shimmed.

Por exemplo, dada uma classe MyClass com um método MyMethodde instância:

// Code under test
public class MyClass {
   public int MyMethod() {
      ...
   }
}

Você pode criar dois tipos de shim para MyMethod, de modo que o primeiro retorne consistentemente 5 e o segundo retorne consistentemente 10:

// Unit test code
var myClass1 = new ShimMyClass()
{
   MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };

O snippet a seguir mostra a estrutura de tipo gerada de ShimMyClass:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
   public Func<int> MyMethod {
      set {
         ...
      }
   }
   public MyClass Instance {
      get {
         ...
      }
   }
}

A instância de tipo shimmed real pode ser acessada por meio da Instance propriedade:

// Unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;

O tipo de shim também inclui uma conversão implícita para o tipo shimmed, que permite que você use o tipo de shim diretamente:

// Unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // Implicit cast retrieves the runtime instance

Explorar construtores shim

Construtores também não são exceção à aplicação de shims. Eles podem ser adaptados para associar tipos de shim a objetos que venham a ser criados no futuro. Por exemplo, cada construtor é representado como um método estático nomeado Constructor dentro do tipo shim.

Considere uma classe MyClass com um construtor que aceita um inteiro:

public class MyClass {
   public MyClass(int value) {
      this.Value = value;
   }
   ...
}

Um tipo de shim para esse construtor pode ser configurado de modo que, independentemente do valor passado para o construtor, cada instância futura retorna -5 quando o Value getter é invocado:

// Unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
   var shim = new ShimMyClass(@this) {
      ValueGet = () => -5
   };
};

Cada tipo de shim expõe dois tipos de construtores:

  • Quando precisar de uma nova instância, use o construtor padrão.
  • Quando você tiver um shim de construtor, use o construtor que usa uma instância shimmed como um argumento.
// Unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

O código a seguir ilustra o tipo gerado para :ShimMyClass

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
   public static Action<MyClass, int> ConstructorInt32 {
      set {
         ...
      }
   }

   public ShimMyClass() { }
   public ShimMyClass(MyClass instance) : base(instance) { }
   ...
}

Acessar membros da classe base

Você pode acessar as propriedades de shim dos membros da classe base ao criar um shim para o tipo base. Em seguida, passe a instância derivada para o construtor da classe base shim.

Por exemplo, considere uma classe MyBase com um método MyMethod de instância e um subtipo MyChild:

public abstract class MyBase {
   public int MyMethod() {
       ...
   }
}

public class MyChild : MyBase {
}

Um shim de MyBase pode ser configurado ao iniciar um novo shim de ShimMyBase:

// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };

É importante observar que, quando você passa o shim filho como parâmetro para o construtor do shim base, o tipo do shim filho é convertido implicitamente na instância derivada.

A estrutura do tipo gerado para ShimMyChild e ShimMyBase é semelhante ao seguinte código:

// Fakes generated code
public class ShimMyChild : ShimBase<MyChild> {
   public ShimMyChild() { }
   public ShimMyChild(Child child)
       : base(child) { }
}
public class ShimMyBase : ShimBase<MyBase> {
   public ShimMyBase(Base target) { }
   public Func<int> MyMethod
   { set { ... } }
}

Construtores estáticos

Os tipos de shim expõem um método estático StaticConstructor para fazer o shim do construtor estático de um tipo. Como os construtores estáticos são executados apenas uma vez, você precisa garantir que o shim esteja configurado antes que qualquer membro do tipo seja acessado.

Finalizadores

Não há suporte para finalizadores em Fakes.

Métodos privados

O gerador de código Fakes cria propriedades shim para métodos privados cuja assinatura contém apenas tipos visíveis, isto é, em que os tipos dos parâmetros e o tipo de retorno são visíveis.

Interfaces de associação

Quando um tipo shimmed implementa uma interface, o gerador de código emite um método que permite associar todos os membros dessa interface ao mesmo tempo.

Por exemplo, dada uma classe MyClass que implementa IEnumerable<int>:

public class MyClass : IEnumerable<int> {
   public IEnumerator<int> GetEnumerator() {
       ...
   }
   ...
}

Você pode adaptar as implementações de IEnumerable<int> em MyClass chamando o método Bind:

// Unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });

A estrutura de tipo gerada de ShimMyClass é semelhante ao código a seguir:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
   public ShimMyClass Bind(IEnumerable<int> target) {
       ...
   }
}

Alterar o comportamento padrão

Cada tipo de shim gerado contém uma instância da interface IShimBehavior, acessível pela propriedade ShimBase<T>.InstanceBehavior. Esse comportamento é acionado sempre que um cliente chama um membro de instância para o qual não foi definido explicitamente um shim.

Por padrão, se nenhum comportamento específico for definido, o design usará a instância retornada pela propriedade estática ShimBehaviors.Current , que normalmente gera uma NotImplementedException exceção.

Você pode modificar esse comportamento a qualquer momento ajustando a propriedade InstanceBehavior de qualquer instância de shim.

Por exemplo, você pode alterar o comportamento para não fazer nada ou retornar o valor padrão do tipo de retorno: default(T)

// Unit test code
var shim = new ShimMyClass();

//Return default(T) or do nothing
shim.InstanceBehavior = ShimBehaviors.DefaultValue;

Você também pode alterar globalmente o comportamento de todas as instâncias shimmed, em que a InstanceBehavior propriedade não está definida explicitamente, definindo a propriedade estática ShimBehaviors.Current :

// Unit test code
// Change default shim for all shim instances where the behavior isn't set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;

Identificar interações com dependências externas

Para ajudar a identificar quando seu código interage com sistemas externos ou dependências (referidos como environment), você pode utilizar shims para atribuir um comportamento específico a todos os membros de um tipo (incluindo métodos estáticos). Ao definir o ShimBehaviors.NotImplemented comportamento na propriedade estática Behavior do tipo shim, qualquer acesso a um membro desse tipo que não seja explicitamente shimmed gerará um NotImplementedException. Esse resultado pode servir como um sinal útil durante o teste, indicando que seu código está tentando acessar um sistema ou dependência externa.

Aqui está um exemplo de como configurar essa forma de shim no código de teste de unidade:

// Unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;

Para conveniência, um método abreviado também é fornecido para obter o mesmo efeito:

// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();

Invocar métodos originais a partir de métodos shim

Em alguns cenários, talvez seja necessário executar o método original durante a execução do método shim. Por exemplo, talvez você queira gravar texto no sistema de arquivos depois de validar o nome do arquivo passado para o método.

Uma abordagem para lidar com essa situação é encapsular uma chamada ao método original usando um delegado e o método ShimsContext.ExecuteWithoutShims():

// Unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
  ShimsContext.ExecuteWithoutShims(() => {

     Console.WriteLine("enter");
     File.WriteAllText(fileName, content);
     Console.WriteLine("leave");
  });
};

Como alternativa, você pode anular o shim, chamar o método original e restaurar o shim:

// Unit test code
ShimsDelegates.Action<string, string> shim = null;

shim = (fileName, content) => {
   try {
      Console.WriteLine("enter");
      // Remove shim in order to call original method
      ShimFile.WriteAllTextStringString = null;
      File.WriteAllText(fileName, content);
   }
   finally
   {
      // Restore shim
      ShimFile.WriteAllTextStringString = shim;
      Console.WriteLine("leave");
   }
};

// Initialize the shim
ShimFile.WriteAllTextStringString = shim;

Manipular simultaneidade com tipos de shim

Os tipos de shim operam em todos os threads no AppDomain e não possuem afinidade de thread. Essa característica é crucial para ter em mente se você planeja utilizar um executor de teste que dê suporte à simultaneidade. Vale a pena observar que testes que envolvem tipos de shim não podem ser executados simultaneamente, embora o runtime do Fakes não imponha essa restrição.

Usar shims com System.Environment

Para criar um shim para a classe System.Environment, você precisa modificar o arquivo mscorlib.fakes.

Localize o \<Assembly> elemento e adicione o seguinte conteúdo após a definição do elemento:

<ShimGeneration>
   <Add FullName="System.Environment"/>
</ShimGeneration>

Depois de fazer alterações e recompilar a solução, os métodos e as propriedades da System.Environment classe agora estão disponíveis para shimming.

Aqui está um exemplo de como você pode atribuir um comportamento ao GetCommandLineArgsGet método:

System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...

Ao fazer essas modificações, você obtém a capacidade de controlar e testar como seu código interage com variáveis de ambiente do sistema, que é uma ferramenta essencial para testes de unidade abrangentes.