Usar shims para isolar seu aplicativo para teste de unidade
Os tipos Shim, 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, garantindo 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 mais detalhada para stubs, confira Usar stubs para isolar partes do aplicativo para teste de unidade.)
Limitações de shims
É importante observar que os shims têm suas limitações.
Os shims não podem ser usados em todos os tipos de determinadas bibliotecas na classe base, especificamente mscorlib e System do .NET no .NET Framework e em System.Runtime no .NET Core ou no .NET 5+. Essa restrição deve ser levada em conta durante a fase de planejamento e design de teste para garantir uma estratégia de teste bem-sucedida e eficaz.
Criando um Shim: Um Guia Passo a Passo
Suponhamos 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
Abra o Visual Studio e crie um projeto
Class Library
Definir o nome do projeto
HexFileReader
Defina o nome da solução
ShimsTutorial
.Definir a estrutura de destino do projeto como .NET Framework 4.8
Excluir o arquivo padrão
Class1.cs
Adicione um novo arquivo
HexFile.cs
e adicione a seguinte definição de classe:
Criar um projeto de teste
Clique com o botão direito do mouse na solução e adicione um novo projeto
MSTest Test Project
Definir o nome do projeto
TestProject
Definir a estrutura de destino do projeto como .NET Framework 4.8
Adicionar assembly do Fakes
Adicionar uma referência de projeto para
HexFileReader
Adicionar assembly do Fakes
No Gerenciador de Soluções,
Para um projeto de .NET Framework mais antigo (estilo não SDK), expanda o nó Referências do projeto de teste de unidade.
Para um projeto no estilo SDK direcionado ao .NET Framework, .NET Core ou .NET 5+, expanda o nó Dependências para localizar o assembly que você deseja simular em Assemblies, Projetos ou Pacotes.
Caso esteja trabalhando no Visual Basic, selecione Mostrar Todos os Arquivos na barra de ferramentas do Gerenciador de Soluções para ver o nó Referências.
Selecione o assembly
System
que contém a definição deSystem.IO.File.ReadAllLines
.No menu de atalhos, selecione Adicionar Assembly do Fakes.
Como a criação resulta em alguns avisos e erros porque nem todos os tipos podem ser usados com shims, você precisará modificar o conteúdo de 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
Modifique o arquivo padrão
UnitTest1.cs
para adicionar o seguinteTestMethod
[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
Abra Explorador de Teste e execute o teste.
É fundamental dispor cada contexto de shim adequadamente. Como regra geral, chame o ShimsContext.Create
dentro de uma instrução using
para garantir a limpeza apropriada dos shims registrados. Por exemplo, você pode registrar um shim para um método de teste que substitui o método DateTime.Now
por um representante 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 o valor DateTime.Now
. Isso pode ser surpreendente e pode causar confusão.
Convenções de nomenclatura para classes Shim
Os nomes das classes de shims são compostos pelo prefixo Fakes.Shim
adicionado ao nome do tipo original. Os nomes dos parâmetros são acrescentados ao nome do método. (Você não precisa adicionar nenhuma referência ao assembly ao System.Fakes.)
System.IO.File.ReadAllLines(path);
System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };
Noções básicas sobre como os shims funcionam
Os shims operam introduzindo desvios na base de 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 essa 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 em runtime. Os desvios sempre devem ser criados dentro do tempo de vida de um ShimsContext
. Quando o ShimsContext é descartado, todos os shims ativos que foram criados dentro dele também são removidos. Para gerenciar isso com eficiência, é recomendável encapsular a criação de desvios em uma instrução using
.
Shims para tipos de métodos diferentes
Os shims dão suporte a vários tipos de métodos.
Métodos estáticos
Ao realizar shim de métodos estáticos, as propriedades que contêm shims são alojadas dentro de um tipo 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 estático MyMethod
:
//code under test
public static class MyClass {
public static int MyMethod() {
...
}
}
Podemos 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 ocorre com os métodos estáticos, os métodos de instância podem fazer shim em todas as instâncias. As propriedades que contêm esses shims são colocadas em um tipo aninhado chamado AllInstances para impedir confusão. Se tivermos uma classe MyClass
com um método de instância MyMethod
:
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
Podemos anexar um shim a MyMethod
para retornar 5 consistentemente, não importa a instância:
// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;
A estrutura de tipo gerado de ShimMyClass
seria exibida da seguinte maneira:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public static class AllInstances {
public static Func<MyClass, int>MyMethod {
set {
...
}
}
}
}
Neste cenário, o Fakes passa a instância de runtime como o primeiro argumento do representante.
Métodos de instância (instância única de runtime)
Os métodos de instância também podem sofrer shim usando delegados diferentes, dependendo do receptor da chamada. Isso permite que o mesmo método de instância exiba comportamentos diferentes por instância do tipo. As propriedades que contêm esses shims são métodos de instância do próprio tipo shim. Cada tipo de shim instanciado está vinculado a uma instância bruta de um tipo com shim.
Por exemplo, dada a classe MyClass
com um método de instância MyMethod
:
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
Podemos 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 };
A estrutura de tipo gerado de ShimMyClass
seria exibida da seguinte maneira:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public Func<int> MyMethod {
set {
...
}
}
public MyClass Instance {
get {
...
}
}
}
A instância de tipo realmente com shim pode ser acessada por meio da propriedade Instance:
// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;
O tipo de shim também inclui uma conversão implícita no tipo com shim, permitindo usar o tipo de shim diretamente:
// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime instance
Construtores
Construtores não são exceção ao shimming; eles também podem sofrer shim para anexar tipos de shim 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 modo que, independentemente do valor passado para o construtor, cada instância futura retornará -5 quando o getter Value for 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. O construtor padrão deve ser usado quando uma nova instância é necessária; enquanto que o construtor que usa uma instância com shim como argumento deve ser usado apenas em shims de construtor:
// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }
A estrutura do tipo gerado para ShimMyClass
pode ser ilustrada da seguinte maneira:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
public static Action<MyClass, int> ConstructorInt32 {
set {
...
}
}
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }
...
}
Acessando membros base
As propriedades de shim dos membros base podem ser acessadas com a criação de uma correção para o tipo base e a passagem da instância filha para o construtor da classe de shim base.
Por exemplo, considere a classe MyBase
com um método de instância MyMethod
e um subtipo MyChild
:
public abstract class MyBase {
public int MyMethod() {
...
}
}
public class MyChild : MyBase {
}
Um shim de MyBase
pode ser configurado iniciando um novo shim ShimMyBase
:
// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };
É importante observar que, quando passado como um parâmetro para o construtor shim base, o tipo de shim filho é implicitamente convertido para a instância filha.
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
Os tipos de shim expõem um método estático StaticConstructor
para fazer shim do construtor estático de um tipo. Como os construtores estáticos são executados somente uma vez, você precisa fazer com que a correção seja configurada antes de qualquer membro do tipo ser acessado.
Finalizadores
Não há suporte para os finalizadores no Fakes.
Métodos privados
O gerador de código do Fakes cria propriedades de shim para métodos particulares que têm apenas tipos visíveis na assinatura, ou seja, tipos de parâmetro e tipo de retorno visíveis.
Interfaces de associação
Quando um tipo com shim implementa uma interface, o gerador de código emite um método que permite associar todos os membros da interface de uma vez.
Por exemplo, dada a classe MyClass
que implementa IEnumerable<int>
:
public class MyClass : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
...
}
...
}
É possível usar shim nas 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 do tipo gerado de ShimMyClass
lembra o seguinte código:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public ShimMyClass Bind(IEnumerable<int> target) {
...
}
}
Alterar o comportamento padrão
Cada tipo de shim gerado inclui uma instância da interface IShimBehavior
acessível pela propriedade ShimBase<T>.InstanceBehavior
. Esse comportamento é invocado sempre que um cliente chama um membro de instância que não sofreu shim explicitamente.
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 gera uma exceção NotImplementedException
.
Você pode modificar esse comportamento a qualquer momento ajustando a propriedade InstanceBehavior
para qualquer instância de shim. Por exemplo, o snippet 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 com shim, em que a propriedade InstanceBehavior
não tenha sido definida explicitamente, 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 código está interagindo com sistemas externos ou dependências (chamados de environment
), 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 sofrido explicitamente shim gerará um NotImplementedException
. Isso pode servir como um sinal útil durante o teste, indicando que seu código está tentando acessar um sistema externo ou uma 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;
Para sua conveniência, um método abreviado também é fornecido para obter o mesmo efeito:
// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();
Invocando métodos originais de dentro dos métodos shim
Pode haver cenários em que talvez seja necessário executar o método original durante a execução do método shim. Por exemplo, você pode escrever o 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 representante 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 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 simultaneidade 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 dê suporte à simultaneidade. Vale a pena observar que os testes envolvendo tipos de shim não podem ser executados simultaneamente, embora essa restrição não seja imposta pelo runtime do Fakes.
Shimming System.Environment
Se você quiser fazer shim em uma classe System.Environment, precisará fazer algumas modificações no arquivo mscorlib.fakes
. Seguindo o elemento Assembly, adicione o seguinte conteúdo:
<ShimGeneration>
<Add FullName="System.Environment"/>
</ShimGeneration>
Depois de fazer essas alterações e recompilar a solução, os métodos e as propriedades na classe System.Environment
agora estarão disponíveis para sofrerem shim. Aqui está um exemplo de como você pode atribuir um comportamento ao método GetCommandLineArgsGet
:
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.