Compartilhar via


Use "shims" para isolar a sua aplicação para testes unitários.

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

Empregue calços quando precisar criar um limite entre seu código e assemblies que não fazem parte de sua solução. Quando o objetivo é isolar os componentes da sua solução uns dos outros, recomenda-se o uso de stubs .

(Para obter uma descrição mais detalhada dos stubs, consulte Usar stubs para isolar partes do seu aplicativo umas das outras para testes de unidade.)

Limitações de Shims

É importante notar que os calços têm suas limitações.

Shims não podem ser usados em todos os tipos de determinadas bibliotecas na classe base .NET, especificamente mscorlib e System no .NET Framework e em System.Runtime no .NET Core ou .NET 5+. Esta restrição deve ser tida em conta durante a fase de planeamento e conceção do ensaio para garantir uma estratégia de ensaio bem-sucedida e eficaz.

Criando um Shim: Um Guia Passo a Passo

Suponha que seu componente contenha chamadas para System.IO.File.ReadAllLines:

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

Criar uma biblioteca de classes

  1. Abra o Visual Studio e crie um Class Library projeto

    Captura de tela do projeto NetFramework Class Library no Visual Studio.

  2. Definir nome do projeto HexFileReader

  3. Defina o nome ShimsTutorialda solução .

  4. Defina a estrutura de destino do projeto como .NET Framework 4.8

  5. Excluir o arquivo padrão Class1.cs

  6. Adicione um novo arquivo HexFile.cs e adicione 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);
        }
    }
    

Criar um projeto de teste

  1. Clique com o botão direito do mouse na solução e adicione um novo projeto MSTest Test Project

  2. Definir nome do projeto TestProject

  3. Defina a estrutura de destino do projeto como .NET Framework 4.8

    Captura de tela do projeto NetFramework Test no Visual Studio.

Adicionar Fakes Assembly

  1. Adicionar uma referência de projeto a HexFileReader

    Captura de ecrã do comando Adicionar Referência de Projeto.

  2. Adicionar Fakes Assembly

    • No Gerenciador de Soluções,

      • Para um projeto mais antigo do .NET Framework (estilo não-SDK), expanda o nó Referências do seu projeto de teste de unidade.

      • Para um projeto no estilo SDK destinado ao .NET Framework, .NET Core ou .NET 5+, expanda o nó Dependências para localizar o assembly que você gostaria de falsificar em Assemblies, Projetos ou Pacotes.

      • Se você estiver trabalhando no Visual Basic, selecione Mostrar todos os arquivos na barra de ferramentas Gerenciador de Soluções para ver o nó Referências .

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

    • No menu de atalho, selecione Adicionar montagem de falsificações.

    Captura de ecrã do comando Add Fakes Assembly.

Uma vez que a construção resulta em alguns avisos e erros, porque nem todos os tipos podem ser usados com calços, você terá que modificar o conteúdo do Fakes\mscorlib.fakes para excluí-los.

<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

  1. Modifique o arquivo UnitTest1.cs padrão para adicionar o seguinte TestMethod

    [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);
        }
    }
    

    Aqui está o Gerenciador de Soluções mostrando todos os arquivos

    Captura de ecrã do Solution Explorer a mostrar todos os ficheiros.

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

É fundamental descartar adequadamente cada contexto de calço. Como regra geral, chame o ShimsContext.Create dentro de uma using instrução para garantir a limpeza adequada dos calços registados. 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 o primeiro de janeiro de 2000. Se se esquecer de limpar o calço registado no método de teste, o resto da execução do teste devolverá sempre o primeiro de janeiro de 2000 como o valor DateTime.Now. Isso pode ser surpreendente e confuso.


Convenções de nomenclatura para classes Shim

Os nomes das classes Shim são constituídos pelo prefixo Fakes.Shim ao nome do tipo original. Os nomes dos parâmetros são acrescentados ao nome do método. Não é necessário 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" };

Entendendo como os calços funcionam

Os shims operam introduzindo desvios na base de código da aplicação que está a ser testada. Sempre que há uma chamada para o método original, o sistema Fakes intervém para redirecionar essa chamada, fazendo com que seu código de shim personalizado seja executado em vez do método original.

É importante notar que esses desvios são criados e removidos dinamicamente em tempo de execução. Os desvios devem sempre ser criados dentro do ciclo de vida de um ShimsContext. Quando o ShimsContext é descartado, todos os shims ativos que foram criados dentro dele também são removidos. Para gerir isto de maneira eficiente, é recomendável encapsular a criação de desvios em uma using instrução.


Adaptadores para diferentes tipos de métodos

Os interceptores suportam vários tipos de métodos.

Métodos estáticos

Ao utilizar shims em métodos estáticos, as propriedades que mantêm os shims são alojadas dentro de um tipo de shim. Essas propriedades possuem apenas um setter, que é usado para anexar um delegado ao método de destino. Por exemplo, se tivermos uma classe chamada MyClass com um método MyMethodestático:

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

Podemos anexar um calço a MyMethod de tal forma que ele constantemente retorne 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 evitados para todas as instâncias. As propriedades que contêm esses shims são colocadas num tipo aninhado chamado AllInstances para evitar confusão. Se tivermos uma classe MyClass com um método MyMethodde instância:

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

Podemos ligar um shim a MyMethod para que ele retorne consistentemente 5, independentemente da instância.

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

A estrutura de tipo gerada de ShimMyClass apareceria da seguinte forma:

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

Nesse cenário, Fakes passa a instância de tempo de execução como o primeiro argumento do delegado.

Métodos de instância (Instância Única de Tempo de Execução)

Os métodos de instância também podem ser ajustados/implementados usando diferentes delegados, dependendo do receptor da chamada. Isso permite que o mesmo método de instância exiba comportamentos diferentes para cada instância do tipo. As propriedades que contêm esses calços são métodos de instância do próprio tipo de calço. Cada tipo de calço instanciado está ligado 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() {
        ...
    }
}

Podemos criar dois tipos de calços para MyMethod de forma 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 };

A estrutura de tipo gerada de ShimMyClass apareceria da seguinte forma:

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

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

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

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

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

Construtores

Os construtores não são exceção ao uso de 'shims'; eles também podem ser colocados com shims para anexar tipos de shims a objetos que serão criados no futuro. Por exemplo, cada construtor é representado como um método estático, chamado Constructor, dentro do tipo shim. Vamos considerar 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 o construtor pode ser configurado de tal forma que, independentemente do valor passado para o construtor, cada instância futura retornará -5 quando o Value getter for invocado:

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

Cada tipo de calço expõe dois tipos de construtores. O construtor padrão deve ser usado quando uma nova instância é necessária, enquanto o construtor que usa uma instância shimmed como um argumento só deve ser usado em shims do construtor:

// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

A estrutura do tipo gerado para ShimMyClass pode ser ilustrada da seguinte forma:

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

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

Aceder a membros da base

As propriedades shim dos membros base podem ser alcançadas criando um shim para o tipo base e inserindo a instância filho no 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 {
}

Pode-se configurar um calço de MyBase ao iniciar um novo calço de ShimMyBase.

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

É importante notar que, quando passado como um parâmetro para o construtor de shim base, o tipo de shim filho é implicitamente convertido para a instância filho.

A estrutura do tipo gerado para ShimMyChild e ShimMyBase pode ser comparada 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

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

Finalizadores

Os finalizadores não são suportados em Fakes.

Métodos privados

O gerador de código Fakes cria propriedades de shim para métodos privados que só têm tipos visíveis na assinatura, ou seja, tipos de parâmetro e tipo de retorno visível.

Interfaces de ligação

Quando um tipo shimmed implementa uma interface, o gerador de código emite um método que permite vincular todos os membros dessa interface de uma só vez.

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

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

Você pode usar shim nas implementações de IEnumerable<int> na classe 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 seguinte código:

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

Alterar comportamento padrão

Cada tipo de shim gerado inclui uma instância da interface IShimBehavior, acessível através da propriedade ShimBase<T>.InstanceBehavior. Esse comportamento é invocado sempre que um cliente chama um membro da instância que não foi explicitamente corrigido.

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

Você pode modificar esse comportamento a qualquer momento, ajustando a propriedade InstanceBehavior para qualquer instância de shim. Por exemplo, o trecho de código a seguir altera o comportamento para não fazer nada ou retornar o valor padrão do tipo de retorno, ou seja, 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 — onde a propriedade InstanceBehavior não foi explicitamente definida — definindo a propriedade estática ShimBehaviors.Current:

// unit test code
// change default shim for all shim instances where the behavior has not been set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;

Identificando interações com dependências externas

Para ajudar a identificar quando seu environmentcódigo está interagindo com sistemas externos ou dependências (conhecido como ), você pode utilizar shims para atribuir um comportamento específico a todos os membros de um tipo. Isso inclui métodos estáticos. Ao definir o comportamento ShimBehaviors.NotImplemented na propriedade estática Behavior do tipo shim, qualquer acesso a um membro desse tipo que não tenha sido explicitamente adaptado (shimmed) lançará uma NotImplementedException. Isso pode servir como um sinal útil durante o teste, indicando que seu código está tentando acessar um sistema externo ou dependência.

Aqui está um exemplo de como configurar isso em seu código de teste de unidade:

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

Por conveniência, um método abreviado também é disponibilizado para alcançar o mesmo efeito.

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

Invocando métodos originais a partir de métodos de Shim

Pode haver cenários em que você pode precisar executar o método original durante a execução do método shim. Por exemplo, você pode querer 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 para o método original usando um delegado e ShimsContext.ExecuteWithoutShims(), conforme demonstrado no código a seguir:

// 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, em seguida, 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;

Manipulando concorrência com tipos de shim

Os tipos de shim operam em todos os threads dentro do AppDomain e não possuem afinidade de thread. Essa propriedade é crucial para ter em mente se você planeja utilizar um executor de teste que suporte simultaneidade. Vale a pena notar que os testes envolvendo tipos de shim não podem ser executados concomitantemente, embora essa restrição não seja imposta pelo tempo de execução do Fakes.

Implementação de Shimming para System.Environment

Se quiser ajustar a classe System.Environment, terá de fazer algumas modificações no ficheiro mscorlib.fakes. Após o elemento Assembly, adicione o seguinte conteúdo:

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

Depois de fazer essas alterações e reconstruir a solução, os métodos e propriedades na System.Environment classe agora estão disponíveis para serem corrigidos. 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ê abriu a possibilidade de controlar e testar como seu código interage com variáveis de ambiente do sistema, uma ferramenta essencial para testes de unidade abrangentes.