Могу ли я написать тесты для пользовательских атрибутов без определения классов 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 или подобное.

Другие вопросы по тегам