Как сказать Pex не заглушать абстрактный класс, имеющий конкретные реализации

Я пытаюсь использовать Pex для проверки кода. У меня есть абстрактный класс с четырьмя конкретными реализациями. Я создал фабричные методы для каждого из четырех конкретных типов. Я также создал один для абстрактного типа, кроме как эта замечательная ветка объясняет, что Pex не будет использовать абстрактный фабричный метод и не должен.

Проблема в том, что часть моего кода зависит от всех четырех конкретных типов (поскольку очень, очень маловероятно, что будут созданы дополнительные подклассы), но Pex нарушает код, используя Moles для создания заглушки.

Как я могу заставить Pex использовать один из фабричных методов (любой, мне все равно) для создания экземпляров абстрактного класса без создания заглушек Moles для этого абстрактного класса? Есть ли директива PexAssume, которая сделает это? Обратите внимание, что некоторые из конкретных типов образуют тип древовидной структуры, например, ConcreteImplementation происходит от AbstractClass, а ConcreteImplementation имеет два свойства типа AbstractClass. Мне нужно убедиться, что в дереве вообще не используются заглушки. (Не все конкретные реализации имеют AbstractClass свойств.)

Изменить:

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

Вот упрощенные версии абстрактного базового класса и четыре его конкретные реализации.

public abstract class AbstractClass
{
    public abstract AbstractClass Distill();

    public static bool operator ==(AbstractClass left, AbstractClass right)
    {
         // some logic that returns a bool
    }

    public static bool operator !=(AbstractClass left, AbstractClass right)
    {
         // some logic that basically returns !(operator ==)
    }

    public static Implementation1 Implementation1
    {
        get
        {
            return Implementation1.GetInstance;
        }
    }
}

public class Implementation1 : AbstractClass, IEquatable<Implementation1>
{
    private static Implementation1 _implementation1 = new Implementation1();

    private Implementation1()
    {
    }

    public override AbstractClass Distill()
    {
        return this;
    }

    internal static Implementation1 GetInstance
    {
        get
        {
            return _implementation1;
        }
    }

    public bool Equals(Implementation1 other)
    {
        return true;
    }
}

public class Implementation2 : AbstractClass, IEquatable<Implementation2>
{
    public string Name { get; private set; }
    public string NamePlural { get; private set; }

    public Implementation2(string name)
    {
        // initializes, including
        Name = name;
        // and sets NamePlural to a default
    }

    public Implementation2(string name, string plural)
    {
        // initializes, including
        Name = name;
        NamePlural = plural;
    }

    public override AbstractClass Distill()
    {
        if (String.IsNullOrEmpty(Name))
        {
            return AbstractClass.Implementation1;
        }
        return this;
    }

    public bool Equals(Implementation2 other)
    {
        if (other == null)
        {
            return false;
        }

        return other.Name == this.Name;
    }
}

public class Implementation3 : AbstractClass, IEquatable<Implementation3>
{
    public IEnumerable<AbstractClass> Instances { get; private set; }

    public Implementation3()
        : base()
    {
        Instances = new List<AbstractClass>();
    }

    public Implementation3(IEnumerable<AbstractClass> instances)
        : base()
    {
        if (instances == null)
        {
            throw new ArgumentNullException("instances", "error msg");
        }

        if (instances.Any<AbstractClass>(c => c == null))
        {
            thrown new ArgumentNullException("instances", "some other error msg");
        }

        Instances = instances;
    }

    public override AbstractClass Distill()
    {
        IEnumerable<AbstractClass> newInstances = new List<AbstractClass>(Instances);

        // "Flatten" the collection by removing nested Implementation3 instances
        while (newInstances.OfType<Implementation3>().Any<Implementation3>())
        {
            newInstances = newInstances.Where<AbstractClass>(c => c.GetType() != typeof(Implementation3))
                                       .Concat<AbstractClass>(newInstances.OfType<Implementation3>().SelectMany<Implementation3, AbstractUnit>(i => i.Instances));
        }

        if (newInstances.OfType<Implementation4>().Any<Implementation4>())
        {
            List<AbstractClass> denominator = new List<AbstractClass>();

            while (newInstances.OfType<Implementation4>().Any<Implementation4>())
            {
                denominator.AddRange(newInstances.OfType<Implementation4>().Select<Implementation4, AbstractClass>(c => c.Denominator));
                newInstances = newInstances.Where<AbstractClass>(c => c.GetType() != typeof(Implementation4))
                                           .Concat<AbstractClass>(newInstances.OfType<Implementation4>().Select<Implementation4, AbstractClass>(c => c.Numerator));
            }

            return (new Implementation4(new Implementation3(newInstances), new Implementation3(denominator))).Distill();
        }

        // There should only be Implementation1 and/or Implementation2 instances
        // left.  Return only the Implementation2 instances, if there are any.
        IEnumerable<Implementation2> i2s = newInstances.Select<AbstractClass, AbstractClass>(c => c.Distill()).OfType<Implementation2>();
        switch (i2s.Count<Implementation2>())
        {
            case 0:
                return AbstractClass.Implementation1;
            case 1:
                return i2s.First<Implementation2>();
            default:
                return new Implementation3(i2s.OrderBy<Implementation2, string>(c => c.Name).Select<Implementation2, AbstractClass>(c => c));
        }
    }

    public bool Equals(Implementation3 other)
    {
        // omitted for brevity
        return false;
    }
}

public class Implementation4 : AbstractClass, IEquatable<Implementation4>
{
    private AbstractClass _numerator;
    private AbstractClass _denominator;

    public AbstractClass Numerator
    {
        get
        {
            return _numerator;
        }

        set
        {
            if (value == null)
            {
                throw new ArgumentNullException("value", "error msg");
            }

            _numerator = value;
        }
    }

    public AbstractClass Denominator
    {
        get
        {
            return _denominator;
        }

        set
        {
            if (value == null)
            {
                throw new ArgumentNullException("value", "error msg");
            }
            _denominator = value;
        }
    }

    public Implementation4(AbstractClass numerator, AbstractClass denominator)
        : base()
    {
        if (numerator == null || denominator == null)
        {
            throw new ArgumentNullException("whichever", "error msg");
        }

        Numerator = numerator;
        Denominator = denominator;
    }

    public override AbstractClass Distill()
    {
        AbstractClass numDistilled = Numerator.Distill();
        AbstractClass denDistilled = Denominator.Distill();

        if (denDistilled.GetType() == typeof(Implementation1))
        {
            return numDistilled;
        }
        if (denDistilled.GetType() == typeof(Implementation4))
        {
            Implementation3 newInstance = new Implementation3(new List<AbstractClass>(2) { numDistilled, new Implementation4(((Implementation4)denDistilled).Denominator, ((Implementation4)denDistilled).Numerator) });
            return newInstance.Distill();
        }
        if (numDistilled.GetType() == typeof(Implementation4))
        {
            Implementation4 newImp4 = new Implementation4(((Implementation4)numReduced).Numerator, new Implementation3(new List<AbstractClass>(2) { ((Implementation4)numDistilled).Denominator, denDistilled }));
            return newImp4.Distill();
        }

        if (numDistilled.GetType() == typeof(Implementation1))
        {
            return new Implementation4(numDistilled, denDistilled);
        }

        if (numDistilled.GetType() == typeof(Implementation2) && denDistilled.GetType() == typeof(Implementation2))
        {
            if (((Implementation2)numDistilled).Name == (((Implementation2)denDistilled).Name)
            {
                return AbstractClass.Implementation1;
            }
            return new Implementation4(numDistilled, denDistilled);
        }

        // At this point, one or both of numerator and denominator are Implementation3
        // instances, and the other (if any) is Implementation2.  Because both
        // numerator and denominator are distilled, all the instances within either
        // Implementation3 are going to be Implementation2.  So, the following should
        // work.
        List<Implementation2> numList =
            numDistilled.GetType() == typeof(Implementation2) ? new List<Implementation2>(1) { ((Implementation2)numDistilled) } : new List<Implementation2>(((Implementation3)numDistilled).Instances.OfType<Implementation2>());

        List<Implementation2> denList =
            denDistilled.GetType() == typeof(Implementation2) ? new List<Implementation2>(1) { ((Implementation2)denDistilled) } : new List<Implementation2>(((Implementation3)denDistilled).Instances.OfType<Implementation2>());

        Stack<int> numIndexesToRemove = new Stack<int>();
        for (int i = 0; i < numList.Count; i++)
        {
            if (denList.Remove(numList[i]))
            {
                numIndexesToRemove.Push(i);
            }
        }

        while (numIndexesToRemove.Count > 0)
        {
            numList.RemoveAt(numIndexesToRemove.Pop());
        }

        switch (denList.Count)
        {
            case 0:
                switch (numList.Count)
                {
                    case 0:
                        return AbstractClass.Implementation1;
                    case 1:
                        return numList.First<Implementation2>();
                    default:
                        return new Implementation3(numList.OfType<AbstractClass>());
                }
            case 1:
                switch (numList.Count)
                {
                    case 0:
                        return new Implementation4(AbstractClass.Implementation1, denList.First<Implementation2>());
                    case 1:
                        return new Implementation4(numList.First<Implementation2>(), denList.First<Implementation2>());
                    default:
                        return new Implementation4(new Implementation3(numList.OfType<AbstractClass>()), denList.First<Implementation2>());
                }
            default:
                switch (numList.Count)
                {
                    case 0:
                        return new Implementation4(AbstractClass.Implementation1, new Implementation3(denList.OfType<AbstractClass>()));
                    case 1:
                        return new Implementation4(numList.First<Implementation2>(), new Implementation3(denList.OfType<AbstractClass>()));
                    default:
                        return new Implementation4(new Implementation3(numList.OfType<AbstractClass>()), new Implementation3(denList.OfType<AbstractClass>()));
                }
        }
    }

    public bool Equals(Implementation4 other)
    {
        return Numerator.Equals(other.Numerator) && Denominator.Equals(other.Denominator);
    }
}

Суть того, что я пытаюсь протестировать, - это метод Distill, который, как вы можете видеть, может выполняться рекурсивно. Поскольку заглушка AbstractClass бессмысленна в этой парадигме, она нарушает логику алгоритма. Даже попытка проверить наличие заглушенного класса в некоторой степени бесполезна, поскольку я мало что могу с этим поделать, кроме как выбросить исключение или притвориться, что это экземпляр Implementation1. Я бы предпочел не переписывать тестируемый код таким образом, чтобы приспособить его к конкретной среде тестирования, но я хочу написать сам тест таким образом, чтобы никогда не заглушать AbstractClass.

Я надеюсь, что очевидно, чем, например, то, что я делаю, отличается от типобезопасной конструкции enum. Кроме того, я анонимизировал объекты для публикации здесь (как вы можете сказать), и я не включил все методы, поэтому, если вы собираетесь комментировать, чтобы сказать мне, что Implementation4.Equals(Implementation4) сломан, не волнуйтесь, я знаю, что это здесь не работает, но мой код решает эту проблему.

Другое изменение:

Вот пример одного из фабричных классов. Он находится в каталоге Factories тестового проекта, созданного Pex.

public static partial class Implementation3Factory
{
    [PexFactoryMethod(typeof(Implementation3))]
    public static Implementation3 Create(IEnumerable<AbstractClass> instances, bool useEmptyConstructor)
    {
        Implementation3 i3 = null;
        if (useEmptyConstructor)
        {
            i3 = new Implementation3();
        }
        else
        {
            i3 = new Implementation3(instances);
        }

        return i3;
    }
}

В моих фабричных методах для этих конкретных реализаций можно использовать любой конструктор для создания конкретной реализации. В этом примере параметр useEmptyConstructor определяет, какой конструктор использовать. Другие фабричные методы имеют аналогичные особенности. Я помню, как читал, хотя не могу сразу найти ссылку, что эти фабричные методы должны позволять создавать объект во всех возможных конфигурациях.


person Andrew    schedule 21.09.2011    source источник
comment
Не уверен, какую проблему вы решаете с помощью этой реализации, но если кто-нибудь когда-нибудь создаст другой тип, производный от базового класса, то похоже, что они также нарушат вашу реализацию. Звучит так, как будто это может нарушить расширяемость и удивить пользователя, и то и другое является запахом дизайна. Можете ли вы вместо этого добавить атрибут (возможно internal) к производным классам и просто искать его? Тогда вам не нужно заботиться о том, что PEX создает заглушку, поскольку вам не нужно ее использовать, и она не будет аннотирована таким образом, чтобы ваш код сломался. Это также не повредит пользовательский код.   -  person Merlyn Morgan-Graham    schedule 30.10.2011
comment
@ MerlynMorgan-Graham Спасибо за ваш вклад. По правде говоря, этот проект больше подходит для F #, чем для C #, но возможность сопровождения в будущем вызывает беспокойство. Поведение ближе к размеченному союзу, чем к истинному наследованию. Тем не менее, четыре подкласса абстрактного базового класса представляют собой замкнутый набор операций в структуре вычислений, которую я установил. Никто не собирается расширять это, но и абстрактный базовый класс, и конкретные подклассы должны быть видимыми вне их сборки. Если есть что-то еще, что вы подразумеваете под внутренним, я не уверен, что это такое.   -  person Andrew    schedule 31.10.2011
comment
Если смысл имеют только те, которые относятся к производным классам, то зачем беспокоиться - действительно ли это что-то сломает? Если да, то как определить, существуют ли производные классы? Я пытался предложить альтернативу вашему механизму обнаружения. Кроме того, похоже, что у вас есть шаблон, похожий на типобезопасное перечисление. Вы можете полностью следовать этому шаблону и сделать все свои реализации внутренними и просто создать статические фабричные свойства в базовом классе для четырех реализаций. Назовите их правильно, чтобы они создавали правильный тип, но возвращали их как базовый тип.   -  person Merlyn Morgan-Graham    schedule 31.10.2011
comment
@ MerlynMorgan-Graham Спасибо за ваш комментарий. Думаю, мне нужно будет добавить некоторую информацию к вопросу, когда у меня будет несколько минут для этого (то есть, к сожалению, не сегодня). Класс-заглушка действительно ломает вещи во время тестирования, потому что в определенных методах каждого класса (которые работают рекурсивно) делаются некоторые предположения о том, что четыре производных класса образуют полный набор операций. Это не совсем типобезопасное перечисление, потому что производные классы реализуют абстрактные методы. Есть еще кое-что, но опять же, я думаю, что мне нужно отредактировать вопрос.   -  person Andrew    schedule 03.11.2011
comment
@Andrew: Думали ли вы о создании фабрики классов [PexClassFactory] и должны ли это быть классом, реализующим абстрактный класс любым способом, который вы хотите протестировать.   -  person namar0x0309    schedule 23.11.2011
comment
@ namar0x0309 Я вижу очень мало документации (одна страница в Google), в которой упоминается PexClassFactoryAttribute, и, похоже, она украшает нестатический класс статическим фабричным методом, украшенным PexFactoryMethodAttribute. Обеспечивает ли это функциональность, которой нет в одиночку PexFactoryMethodAttribute? Я уже использую [PexFactoryMethod] в фабричных классах для реализаций, и, как описывает ссылка, которую я дал в моем вопросе, [PexFactoryMethod(typeof(AbstractClass))] не будет работать (я все равно попробовал и подтвердил, что Pex не будет его использовать).   -  person Andrew    schedule 23.11.2011
comment
PexFactoryMethod создаст экземпляр класса-заглушки и возьмет только вашу реализацию указанной функции / метода (заглушите остальные виртуальные функции). PexFactorClass переопределяет это, дает вам контроль и должен помочь вам остановить Pex от добавления дополнительных заглушек.   -  person namar0x0309    schedule 23.11.2011
comment
@ namar0x0309 Я здесь немного запуталась, признаюсь. У вас есть ссылка на документацию по этому поводу? Кроме того, я отредактирую, чтобы опубликовать репрезентативный заводской метод, если я не понимаю.   -  person Andrew    schedule 23.11.2011
comment
@Andrew: Pex отключает свойство PexFactoryClass объекта, если последний является PexFactoryMethod. Я предлагаю вам прокомментировать либо или и пойти методом проб и ошибок, пока у вас не будет небольшого тестового класса, чтобы доказать внутреннюю работу Pex. Они изменяют эти свойства от обновления к обновлению (вызывает сбои, поскольку Pex пытается построить класс из абстрактного, а кодер хочет добавить свой собственный и так далее).   -  person namar0x0309    schedule 23.11.2011
comment
Кто-нибудь, пожалуйста, отправьте ответ, который как-то помогает в вопросе, чтобы я мог вознаградить награду   -  person Gabriel    schedule 28.11.2011


Ответы (1)


Вы пробовали сообщить Pex, используя атрибут [PexUseType], что существуют не абстрактные подтипы для вашего абстрактного класса? Если Pex не знает каких-либо неабстрактных подтипов, то решатель ограничений Pex определит, что путь кода, который зависит от существования неабстрактного подтипа, недопустим.

person esskar    schedule 28.11.2011