如何告诉 Pex 不要存根具有具体实现的抽象类

Posted

技术标签:

【中文标题】如何告诉 Pex 不要存根具有具体实现的抽象类【英文标题】:How to tell Pex not to stub an abstract class that has concrete implementations 【发布时间】:2011-11-22 05:34:21 【问题描述】:

我正在尝试使用 Pex 来测试一些代码。我有一个具有四个具体实现的抽象类。我为四种具体类型中的每一种创建了工厂方法。我还为抽象类型创建了一个,除了 this nice thread 解释的那样,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 的方式编写测试本身是我在这里尝试做的。

例如,我希望我所做的与类型安全的枚举构造有何不同,这一点很明显。另外,我匿名发布了这里的对象(如你所知),并且我没有包括所有方法,所以如果你要评论告诉我 Implementation4.Equals(Implementation4) 坏了,别担心,我知道它在这里坏了,但我的实际代码解决了这个问题。

另一个修改:

这是其中一个工厂类的示例。它位于 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 参数控制要使用的构造函数。其他工厂方法具有类似的功能。我记得读过,虽然我无法立即找到链接,但这些工厂方法应该允许在每个可能的配置中创建对象。

【问题讨论】:

不确定您使用该实现解决了什么问题,但如果有人创建了从基类派生的另一种类型,那么听起来他们也会破坏您的实现。听起来它既可以破坏可扩展性,又可以让用户感到惊讶,这两者都是设计的味道。您可以改为向派生类添加一个属性(可能是internal),然后简单地搜索它吗?然后您不必关心 PEX 创建存根,因为您不必使用它,并且不会以导致代码中断的方式对其进行注释。它也不会破坏用户代码。 @MerlynMorgan-Graham 感谢您的意见。事实上,这个项目比 C# 更适合 F#,但未来的可维护性是一个问题。这种行为更接近于“有区别的联合”而不是真正的继承。也就是说,抽象基类的四个子类代表了我建立的计算结构中的一组封闭操作。没有人会扩展它,但是抽象基类和具体子类都需要在它们的程序集之外可见。如果您所说的 internal 还有其他含义,我不确定它是什么。 如果只有那些派生类有意义,那何必担心——它真的会破坏什么吗?如果是这样,您如何检测派生类的存在?我试图为您的检测机制提供替代方案。此外,您似乎有一个类似于类型安全枚举的模式。您可以完全遵循该模式并将所有实现内部化,并在基类上为四个实现创建静态工厂属性。正确命名它们,以便它们创建正确的类型,但将它们作为基本类型返回。 @MerlynMorgan-Graham 感谢您的评论。我想当我有几分钟的时间(不幸的是,今天不是)时,我需要为这个问题添加一些信息。存根类确实在测试期间破坏了一些东西,因为在每个类的某些方法(递归操作)中做出了一些假设,即四个派生类形成了完整的操作集。这不是一个类型安全的枚举,因为派生类实现了抽象方法。还有一点,但我想我需要再次编辑这个问题。 有人请发布一个答案,以某种方式帮助这个问题,以便我可以奖励赏金 【参考方案1】:

您是否尝试过使用 [PexUseType] 属性告诉 Pex 您的抽象类的非抽象子类型存在?如果 Pex 不知道任何非抽象子类型,则 Pex 的约束求解器将确定依赖于非抽象子类型存在的代码路径是不可行的。

【讨论】:

以上是关于如何告诉 Pex 不要存根具有具体实现的抽象类的主要内容,如果未能解决你的问题,请参考以下文章

python 抽象基类(abc)

C#:在所有派生类中具有*必需*实现的静态基类方法存根[重复]

设计原则-DIP依赖倒置原则

如何反射抽象类

抽象方法和抽象类 & 接口

面向对象 接口