Могу ли я написать тесты для пользовательских атрибутов без определения классов x^n?
Я строю библиотеку классов, которая включает в себя несколько пользовательских атрибутов, которые применяются к свойствам. Тогда у меня есть методы, которые делают определенные вещи, основанные на размещении атрибутов.
Теперь я хочу построить некоторые модульные тесты, но как мне сделать тесты, не создавая что-то порядка классов x^(количество атрибутов) только для целей тестирования? Могу ли я использовать классы метаданных или что-то еще?
По сути, я бы хотел, чтобы у меня был способ применять атрибуты к свойствам во время выполнения (т. Е. Внутри части "Упорядочить" моего метода тестирования), но я почти уверен, что это невозможно.
редактировать
Это код отражения, который я использую для проверки атрибутов, поскольку, очевидно, то, как я их читаю, может иметь значение:
bool skip = false, iip = false;
string lt = null;
SerializeAsOptions sa = SerializeAsOptions.Ids;
object[] attrs = prop.GetCustomAttributes(true);
foreach (object attr in attrs)
{
Type attrType = attr.GetType();
if (typeof(JsonIgnoreAttribute).IsAssignableFrom(attrType))
{
skip = true;
continue;
}
if (typeof(IncludeInPayload).IsAssignableFrom(attrType))
iip = ((IncludeInPayload)attr).Include;
if (typeof(SerializeAs).IsAssignableFrom(attrType))
sa = ((SerializeAs)attr).How;
if (typeof(LinkTemplate).IsAssignableFrom(attrType))
lt = ((LinkTemplate)attr).LinkTemplate;
}
if (skip) continue;
2 ответа
Я добавляю другой ответ, потому что, поскольку вы сейчас предоставили некоторый код, старый слишком широк. Теперь (в основном) очевидно, что:
- вы управляете кодом чтения атрибутов
- вы читаете код с помощью отражения (PropertyInfo.GetCustomAttributes)
Так. Поскольку вы используете Reflection, TypeDescriptors не поможет. Вам необходимо:
- либо читайте attrs по-другому, так что можно использовать TypeDescr
- динамически генерировать сборки во время выполнения для генерации классов со свойствами на лету во время тестов
Это может быть очень интересно / интересно, но это также может превратиться в приятный объем работы. Но, поскольку вы контролируете обе стороны кода, ни одна из этих двух на самом деле не нужна.
Во-первых, давайте обрежем код до значимых частей:
somemethod(PropertyInfo prop)
{
// ...
object[] attrs = prop.GetCustomAttributes(true); // read ATTRs from PROP
foreach (object attr in attrs) // scan the PROP's ATTRs
{
// check attr type, do something
}
// ...
}
Суть вашей проблемы не в:
добавление / удаление атрибутов во время части Arrange/Teardown
но
форсирование цикла по ATTR PROP для просмотра атрибутов, указанных в вашем тесте
Глядя на эту проблему, ответ почти очевиден: ваш цикл должен абстрагироваться от части "Чтение атрибутов".
object[] attributeReader(PropertyInfo prop)
{
return prop.GetCustomAttributes(true);
}
somemethod(PropertyInfo prop)
{
// ...
object[] attrs = attributeReader(prop); // read ATTRs from PROP
foreach (object attr in attrs) // scan the PROP's ATTRs
{
// check attr type, do something
}
// ...
}
Теперь ваш код обработки не зависит от способа чтения атрибутов. Конечно, в приведенном выше примере этот способ жестко закодирован. Но это не должно быть. В зависимости от того, как вы хотите / хотите организовать свои тесты, вы можете использовать множество способов заменить attributeReader
метод с другими механизмами.
Например, просто добавьте "виртуальный" к attributeReader
и используйте наследование для создания класса, который включит AttributeFaking:
// original class:
virtual object[] attributeReader(PropertyInfo prop)
{
return prop.GetCustomAttributes(true);
}
// derived class:
object[] AttributesOverrides {get;set;}
override object[] attributeReader(PropertyInfo prop)
{
if(prop.Name = "ShoeSize") return AttributesOverrides; // return what I say!
return base.attributeReader(prop);
}
// your test setup
var t = ... // that DERIVED object
t.AttributesOverrides = new [] { ... } ; // attributes to use
Например, используйте делегаты / лямбды, без наследования
// note the attributeReader is now a field, not function
Func<PropertyInfo, object[]> attributeReader = defaultAttributeReader;
static object[] defaultAttributeReader(PropertyInfo prop)
{
return prop.GetCustomAttributes(true);
}
// and your test setup
var t = ... // that ORIGNAL object
t.attributeReader = customReaderForTheTest; // change the reader on the fly
// that's the reader-function to use in THIS TEST setup
static object[] customReaderForTheTest(PropertyInfo prop)
{
if(prop.Name = "ShoeSize") return null; // crash when I say so! muhaHAHAhaa!
return prop.GetCustomAttributes(true);
}
Оба этих примера приводят к одному классу, который позволяет каким-то образом подделывать атрибуты, но это не единственный способ сделать это. Вы можете использовать IoC, чтобы ввести правильный attributeReader
, Вы можете делать это любым способом - вам просто нужно абстрагироваться от reading
часть и оставить его "открытым".
Невозможно действительно применить атрибут во время выполнения к существующему классу, но есть по крайней мере два способа сделать что-то похожее на него - это зависит от того, насколько точно вы читаете эти атрибуты позже.
Опции сосредоточены на части "действительно" и "существующий класс":
1) не делайте этого, просто фальшиво добавляя их
2) применить их к классу, который еще не существует!;)
Первый вариант - CustomTypeDescriptor. В его реализациях вы сможете динамически отвечать на любые запросы об атрибутах для некоторого класса, который его использует (-> см. Виртуальный метод GetAttributes).
Это приводит к первому пути:
- Создайте
AttributableTestObject
то есть наследует от вашегоClassCompatibleWithThatAttribute
так далее - Создать что-то вроде
DynamicallyAttributedClass : CustomTypeProvider
который выставляет статическое свойство, подобноеIEnumerable<Attribute>
- переопределить
GetAttributes
и вернуть все, что было предоставлено этим статическим свойством - на ваше
AttributableTestObject
класс установитьTypeDecriptorProvider
атрибут, указывающий на провайдера (вы должны снова его реализовать), который возвращаетDynamicallyAttributedClass
Теперь, используя это статическое свойство, вы можете изменить то, что GetAttributes
возвращает, и, следовательно, вы можете динамически изменять набор атрибутов , которые видны через дескриптор типа.
И здесь есть одна загвоздка: не все движки / наблюдатели / читатели /(..) на самом деле заботятся о TypeDescriptors. Некоторые просто читают метаданные сразу после Reflection. Reflection не будет проверять дескрипторы типов. Он просто вернет информацию о том, что AttributableTestObject
класс имеет TypeDecriptorProvider
имущество. Но при использовании механизмов ComponentModel пользовательский список атрибутов будет виден.
Это напоминает мне, что механизмы чтения просто сидят в классе TypeDescriptor, видят его методы. Есть также AddAttribute
так что, может быть, вы даже можете получить то, что вы хотите, не реализовав на самом деле то, что я сказал выше - попробуйте использовать AddAttribute, а затем GetAttribute для вашего целевого типа и проверьте результаты. Это может "просто работать". Но опять же, это не обманет отражение.
Итак, есть второй, более "хардкорный" подход - динамические классы. С System.Reflection.Emit
вы можете динамически генерировать сборку, которая будет содержать любой желаемый код IL, а затем загружать сборку в текущее время выполнения.
Это дает вам уникальную возможность просто генерировать новый свежий класс (ы) во время выполнения с любым содержимым и любыми атрибутами, которые вам нравятся. Вы можете использовать наследование, поэтому вы можете наследовать от любого ClassCompatibleWithThatAttributes
, Это не так просто сделать вручную, но есть некоторые библиотеки, которые облегчают использование IL-излучателя.
Обратите внимание, что сгенерированные типы будут сгенерированы во время выполнения. Это означает, что во время компиляции вы не будете проверять их, вы должны сгенерировать их идеально или столкнуться с некоторыми действительно редко встречающимися исключениями. Также обратите внимание, что, поскольку типы не известны во время компиляции, вы не можете просто new
объекты. Вы должны будете создавать объекты, основываясь на их недавно сгенерированном Типе через ie Activator.CreateInstance(Type)
,
Кроме того, даже если вы можете генерировать столько новых классов, сколько захотите - если вы переусердствуете с этим, вы, вероятно, в конечном итоге достигнете некоторого предела CLR или по крайней мере OutOfMemory
, поскольку сгенерированные и загруженные сборки фактически занимают место. Вы можете преодолеть это с помощью доменов приложений - вы можете создать несколько дополнительных доменов приложений, сгенерировать и загрузить сборки внутри них, а затем, наконец, вы можете выгрузить этот дополнительный домен, чтобы освободить всю память, а также выгрузить все сборки, которые были загружены там. Но, я полагаю, вы не будете генерировать так много типов, чтобы действительно нуждаться в этом. Я пишу об этом на всякий случай. Не волнуйтесь, пока вы не нажмете OutOfMemory или подобное.