Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Типы 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);
Создание библиотеки классов
Откройте Visual Studio и создайте
Class Library
проектУстановить имя проекта
HexFileReader
Задайте имя решения
ShimsTutorial
.Установка целевой платформы проекта на .NET Framework 4.8
Удаление файла по умолчанию
Class1.cs
Добавьте новый файл
HexFile.cs
и добавьте следующее определение класса:
Создание тестового проекта
Щелкните правой кнопкой мыши решение и добавьте новый проект
MSTest Test Project
Установить имя проекта
TestProject
Установка целевой платформы проекта на .NET Framework 4.8
Добавление сборки Fakes
Добавьте ссылку на проект
HexFileReader
Добавление сборки Fakes
В обозревателе решений
Для более старого проекта .NET Framework (не SDK-стиля) разверните элемент Ссылки проекта модульного теста.
Для проекта в стиле SDK, предназначенного для .NET Framework, .NET Core или .NET 5+, разверните узел зависимостей, чтобы найти сборку, которую вы хотите сделать подложной в сборках, проектах или пакетах.
Если вы работаете в Visual Basic, выберите "Показать все файлы " на панели инструментов обозревателя решений , чтобы просмотреть узел "Ссылки ".
Выберите сборку
System
, содержащую определениеSystem.IO.File.ReadAllLines
.В контекстном меню выберите "Добавить фиктивную сборку".
Так как сборка приводит к некоторым предупреждениям и ошибкам, так как не все типы могут использоваться с шимами, необходимо изменить содержимое 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>
Создание модульного теста
Измените файл
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); } }
В обозревателе решений отображаются все файлы.
Откройте обозреватель тестов и запустите тест.
Важно правильно удалить каждый шим-контекст. Как правило, вызовите ShimsContext.Create
внутри using
инструкции, чтобы обеспечить правильную очистку зарегистрированных шима. Например, можно зарегистрировать схим для метода тестирования, заменяющего метод делегатом, который DateTime.Now
всегда возвращает первое января 2000 года. Если вы забыли очистить зарегистрированный шим в методе теста, остальные тесты всегда будут возвращать первое января 2000 года в качестве значения DateTime.Now
. Это может быть удивительно и запутано.
Соглашения об именовании для классов Shim
Имена классов Shim составляются путем добавления префикса Fakes.Shim
к имени исходного типа. Имена параметров добавляются к имени метода. (Вам не нужно добавлять ссылку на сборку для System.Fakes.)
System.IO.File.ReadAllLines(path);
System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };
Понимание работы шимов
Шимы работают путем введения обходов в кодовую базу тестируемого приложения. Всякий раз, когда происходит вызов исходного метода, система Fakes вмешивается, чтобы перенаправить этот вызов, что приводит к выполнению пользовательского кода-шим вместо исходного метода.
Важно отметить, что эти обходы создаются и удаляются динамически во время выполнения. Детуры всегда должны создаваться в течение срока существования ShimsContext
. При завершении ShimsContext все активные шимы, созданные в нем, также удаляются. Для эффективного управления процессом рекомендуется инкапсулировать создание обходов в инструкции using
.
Шимы для различных видов методов
Шимы поддерживают различные типы методов.
Статические методы
При замещении статических методов свойства, содержащие эти замещения, размещаются в соответствующем типе замещения. Эти свойства имеют только сеттер, который используется для присоединения делегата к целевому методу. Например, если у нас есть класс 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 связан с необработанным экземпляром типа 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 {
...
}
}
}
Доступ к фактическому экземпляру типа shimmed можно получить с помощью свойства Instance:
// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;
Тип shim также включает неявное преобразование в shimmed-тип, что позволяет напрямую использовать shim-тип.
// 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 для базового типа и введя дочерний экземпляр в конструктор класса базовой shim.
Например, рассмотрим класс MyBase
с экземплярным методом MyMethod
и подтипом MyChild
:
public abstract class MyBase {
public int MyMethod() {
...
}
}
public class MyChild : MyBase {
}
Шим MyBase
может быть настроен путем инициирования нового ShimMyBase
шима:
// 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 { ... } }
}
Статические конструкторы
Типы Shim предоставляют статический метод StaticConstructor
для замещения статического конструктора типа. Так как статические конструкторы выполняются только один раз, необходимо убедиться, что схим настроен перед доступом к любому члену типа.
Финализаторы
Финализаторы не поддерживаются в Fakes.
Частные методы
Генератор кода Fakes создает свойства схима для частных методов, которые имеют только видимые типы в сигнатуре, то есть типы параметров и видимый тип возвращаемого типа.
Интерфейсы привязки
Когда подставной тип реализует интерфейс, генератор кода создает метод, позволяющий привязать все члены этого интерфейса одновременно.
Например, учитывая класс MyClass
, реализующий IEnumerable<int>
:
public class MyClass : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
...
}
...
}
В MyClass можно вставить реализации IEnumerable<int>
, вызвав метод Bind.
// 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");
});
};
Кроме того, можно аннулировать shim, вызвать оригинальный метод, а затем восстановить 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.
Подмена System.Environment
Если вы хотите модифицировать класс System.Environment, вам потребуется внести некоторые изменения в файл mscorlib.fakes
. После элемента Assembly добавьте следующее содержимое:
<ShimGeneration>
<Add FullName="System.Environment"/>
</ShimGeneration>
После внесения этих изменений и перестроив решение, теперь доступны методы и свойства в System.Environment
классе. Ниже приведен пример назначения поведения GetCommandLineArgsGet
методу:
System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...
Сделав эти изменения, вы открыли возможность контролировать и тестировать взаимодействие кода с системными переменными среды, необходимым инструментом для комплексного модульного тестирования.