Использование оболочек совместимости для изоляции приложения при модульном тестировании

Типы Shim, один из двух ключевых технологий, используемых Microsoft Fakes Framework, играют важную роль в изоляции компонентов приложения во время тестирования. Они работают путем перехвата и перенаправления вызовов к определенным методам, которые затем можно направлять к пользовательскому коду в рамках теста. Эта функция позволяет управлять результатом этих методов, обеспечивая согласованность и прогнозируемость результатов во время каждого вызова независимо от внешних условий. Этот уровень контроля упрощает процесс тестирования и помогает достичь более надежных и точных результатов.

При необходимости создавать границы между кодом и сборками, которые не являются частью решения, используются с помощью шима . Если цель состоит в изоляции компонентов решения друг от друга, рекомендуется использовать заглушки .

(Более подробное описание заглушки см. в разделе Используйте заглушки для изоляции частей приложения друг от друга для модульного тестирования.)

Ограничения для шимов

Важно отметить, что шимы имеют свои ограничения.

Шимы нельзя использовать для всех типов из определенных библиотек в базовом классе .NET, в частности mscorlib и System в платформа .NET Framework, а также в System.Runtime в .NET Core или .NET 5+. Это ограничение следует учитывать во время этапа планирования тестирования и проектирования, чтобы обеспечить успешную и эффективную стратегию тестирования.

Создание Shim: пошаговое руководство

Предположим, что компонент содержит вызовы System.IO.File.ReadAllLines.

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

Создание библиотеки классов

  1. Откройте Visual Studio и создайте Class Library проект

    Screenshot of NetFramework Class Library project in Visual Studio.

  2. Задание имени проекта HexFileReader

  3. Задайте имя ShimsTutorialрешения.

  4. Установка целевой платформы проекта на платформа .NET Framework 4.8

  5. Удаление файла по умолчанию Class1.cs

  6. Добавьте новый файл HexFile.cs и добавьте следующее определение класса:

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

Создание тестового проекта

  1. Щелкните правой кнопкой мыши решение и добавьте новый проект MSTest Test Project

  2. Задание имени проекта TestProject

  3. Установка целевой платформы проекта на платформа .NET Framework 4.8

    Screenshot of NetFramework Test project in Visual Studio.

Добавление сборки Fakes

  1. Добавление ссылки на проект HexFileReader

    Screenshot of the command Add Project Reference.

  2. Добавление сборки Fakes

    • В Обозревателе решений выполните следующие действия:

      • Для более старого проекта .NET Framework (не в стиле пакета SDK) разверните узел Ссылки проекта модульного теста.

      • Для проекта в стиле ПАКЕТА SDK, предназначенного для платформа .NET Framework, .NET Core или .NET 5+, разверните узел зависимостей, чтобы найти сборку, подделающуюся в сборках, проектах или пакетах.

      • При работе в Visual Basic на панели инструментов Обозревателя решений необходимо выбрать команду Показать все файлы, чтобы просмотреть узел Ссылки.

    • Выберите сборку System , содержащую определение System.IO.File.ReadAllLines.

    • В контекстном меню щелкните Добавить сборку Fakes.

    Screnshot of the command Add Fakes Assembly.

Так как сборка приводит к некоторым предупреждениям и ошибкам, так как не все типы можно использовать с шимами, вам придется изменить содержимое Fakes\mscorlib.fakes, чтобы исключить их.

<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>

Создание модульного теста

  1. Измените файл UnitTest1.cs по умолчанию, чтобы добавить следующее. 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);
        }
    }
    

    Ниже приведены Обозреватель решений, в котором отображаются все файлы

    Screenshot of Solution Explorer showing all files.

  2. Откройте Обозреватель теста и запустите тест.

Очень важно правильно ликвидировать каждый контекст оболочки. В общем случае следует вызывать ShimsContext.Create внутри оператора using, чтобы гарантировать правильный сброс зарегистрированных оболочек. Например, можно зарегистрировать оболочку для метода теста, заменяющего метод DateTime.Now делегатом, который всегда возвращает 1 января 2000 г. Если вы забудете сбросить зарегистрированную оболочку в методе теста, остальная часть тестового запуска всегда будет возвращать в качестве значения DateTime.Now первое января 2000 года. Это может быть неожиданно и неудобно.


Соглашения об именовании для классов Shim

Имена классов оболочки создаются путем добавления префикса Fakes.Shim к имени исходного типа. Имена параметров добавляются к имени метода. (Ссылки на сборки в System.Fakes можно не добавлять.)

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

Общие сведения о работе shims

Кодовые оболочки работают путем введения обходов в базу кода тестируемого приложения. Всякий раз, когда есть вызов исходного метода, система Fakes вмешивается в перенаправление этого вызова, что приводит к выполнению пользовательского кода схимы вместо исходного метода.

Важно отметить, что эти обходы создаются и удаляются динамически во время выполнения. Детуры всегда должны создаваться в течение срока существования ShimsContext. При удалении ShimsContext все активные шима, созданные в нем, также удаляются. Для эффективного управления этим рекомендуется инкапсулировать создание обходов в операторе using .


Оболочки для методов различных типов

Шимы поддерживают различные типы методов.

Статические методы

При использовании статических методов свойства, в которых хранятся шимы, размещаются в типе shim. Эти свойства имеют только метод задания, который используется для присоединения делегата к целевому методу. Например, если у нас есть класс, вызываемый MyClass статическим методом MyMethod:

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

Мы можем прикрепить схим, MyMethod чтобы он постоянно возвращал 5:

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

Методы экземпляра (для всех экземпляров)

Как и статические методы, методы экземпляра также могут быть очичены для всех экземпляров. Свойства, удерживающие эти шима, помещаются в вложенный тип AllInstances, чтобы предотвратить путаницу. Если у нас есть класс MyClass с методом MyMethodэкземпляра:

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

Мы можем подключить схим MyMethod , чтобы он последовательно возвращал 5, независимо от экземпляра:

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

Структура созданного ShimMyClass типа будет отображаться следующим образом:

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

В этом сценарии Fakes передает экземпляр среды выполнения в качестве первого аргумента делегата.

Методы экземпляра (один экземпляр среды выполнения)

Методы экземпляра также можно использовать с помощью разных делегатов в зависимости от приемника вызова. Это позволяет одному и тому же методу экземпляра проявлять различные поведения для каждого экземпляра типа. Свойства, удерживающие эти оболочки, являются методами экземпляра самого типа shim. Каждый экземпляр типа shim связан с необработанным экземпляром типа shimmed.

Например, имеется класс MyClass с методом экземпляра MyMethod.

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

Мы можем создать два типа схимов для MyMethod такого, чтобы первый последовательно возвращал 5, а второй последовательно возвращает 10:

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

Структура созданного ShimMyClass типа будет отображаться следующим образом:

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

Доступ к фактическому экземпляру типа с оболочкой совместимости можно получить с помощью свойства Instance.

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

Тип shim также включает неявное преобразование в тип shimmed, что позволяет напрямую использовать тип схима:

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

Конструкторы

Конструкторы не являются исключением для шимминга; они также могут быть шиты, чтобы присоединить типы схимов к объектам, которые будут созданы в будущем. Например, каждый конструктор представлен как статический метод, именованный Constructorв типе shim. Рассмотрим класс MyClass с конструктором, принимаюющим целое число:

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

Тип shim для конструктора можно настроить таким образом, чтобы независимо от значения, переданного конструктору, каждый будущий экземпляр вернет -5 при вызове метода получения значения:

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

Каждый тип схима предоставляет два типа конструкторов. Конструктор по умолчанию следует использовать при необходимости нового экземпляра, в то время как конструктор, принимающий шитый экземпляр в качестве аргумента, должен использоваться только в оболочках конструктора:

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

Структуру созданного типа ShimMyClass можно проиллюстрировать следующим образом:

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

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

Доступ к базовым элементам

Свойства Shim базовых элементов можно достичь, создав схим для базового типа и введите дочерний экземпляр в конструктор базового класса shim.

Например, рассмотрим класс MyBase с методом MyMethod экземпляра и подтипом MyChild:

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

public class MyChild : MyBase {
}

Схим может быть настроен путем инициирования нового ShimMyBase схимаMyBase:

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

Важно отметить, что при передаче в качестве параметра в конструктор базовой оболочки тип дочерней оболочки неявно преобразуется в дочерний экземпляр.

Структура созданного типа для ShimMyChild и ShimMyBase может быть похожа на следующий код:

// 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 { ... } }
}

Статические конструкторы

Типы оболочек совместимости предоставляют статический метод StaticConstructor, чтобы создать оболочку для статического конструктора типа. Так как статические конструкторы выполняются только раз, убедитесь, что оболочка настроена перед получением доступа к любому члену типа.

Методы завершения

Методы завершения не поддерживаются в Fakes.

Закрытые методы

Генератор кода Fakes создает свойства оболочки для закрытых методов, которые имеют только видимые типы в сигнатуре, например видимые типы параметров и типы возвращаемого значения.

Привязка интерфейсов

Когда тип с оболочкой совместимости реализует интерфейс, генератор кода создает метод, позволяющий ему привязать все члены из этого интерфейса за один раз.

Например, имеется класс MyClass, реализующий интерфейс IEnumerable<int>.

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

Вы можете создать оболочки для реализаций IEnumerable<int> в MyClass путем вызова метода привязки.

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

Структура созданного ShimMyClass типа похожа на следующий код:

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

Изменение поведения по умолчанию

Каждый созданный тип схима включает экземпляр интерфейса, доступный IShimBehavior через ShimBase<T>.InstanceBehavior свойство. Это поведение вызывается всякий раз, когда клиент вызывает член экземпляра, который не был явно устранен.

По умолчанию, если определенное поведение не задано, он использует экземпляр, возвращаемый статическим ShimBehaviors.Current свойством, который обычно вызывает NotImplementedException исключение.

Это поведение можно изменить в любое время, изменив InstanceBehavior свойство для любого экземпляра shim. Например, следующий фрагмент кода изменяет поведение, чтобы ничего не делать или возвращать значение по умолчанию возвращаемого типа, т. е. default(T):

// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimBehaviors.DefaultValue;

Вы также можете глобально изменить поведение для всех экземпляров, в которых InstanceBehavior свойство не было явно определено, задав статическое ShimBehaviors.Current свойство:

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

Определение взаимодействий с внешними зависимостями

Чтобы определить, когда код взаимодействует с внешними системами или зависимостями (называемыми environmentзависимостями), можно использовать шимы для назначения определенного поведения всем членам типа. Сюда входят статические методы. Задав ShimBehaviors.NotImplemented поведение для статического Behavior свойства типа shim, любой доступ к члену этого типа, который не был явно удален, вызовет NotImplementedExceptionисключение. Это может служить полезным сигналом во время тестирования, указывая, что код пытается получить доступ к внешней системе или зависимости.

Ниже приведен пример настройки в коде модульного теста:

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

Для удобства также предоставляется сокращенный метод для достижения того же эффекта:

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

Вызов исходных методов из методов Shim

В некоторых сценариях может потребоваться выполнить исходный метод во время выполнения метода shim. Например, может потребоваться написать текст в файловую систему после проверки имени файла, переданного методу.

Один из способов обработки этой ситуации заключается в том, чтобы инкапсулировать вызов исходного метода с помощью делегата и ShimsContext.ExecuteWithoutShims(), как показано в следующем коде:

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

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

Кроме того, можно nullify thehim, call the original method, and then restore the 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;

Обработка параллелизма с помощью типов Shim

Типы Shim работают во всех потоках в AppDomain и не обладают сходством потоков. Это свойство имеет решающее значение, если вы планируете использовать средство выполнения теста, поддерживающее параллелизм. Стоит отметить, что тесты, связанные с типами схимов, не могут выполняться параллельно, хотя это ограничение не применяется средой выполнения Fakes.

Shimming System.Environment

Если вы хотите похитить System.Environment класс, вам потребуется внести некоторые изменения в mscorlib.fakes файл. После элемента Assembly добавьте следующее содержимое:

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

После внесения этих изменений и перестроив решение, теперь доступны методы и свойства в System.Environment классе. Ниже приведен пример назначения поведения GetCommandLineArgsGet методу:

System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...

Сделав эти изменения, вы открыли возможность контролировать и тестировать взаимодействие кода с системными переменными среды, необходимым инструментом для комплексного модульного тестирования.