如何告诉Pex不要为具有实现的抽象类进行存根处理

61

我正在尝试使用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的方式编写测试。

我希望很明显我正在做的事情与类型安全的枚举构造不同。同时,我为了发布而匿名化对象(你可以看出来),我也没有包含所有方法,所以如果你打算评论告诉我Implementation4.Equals(Implementation4)是错误的,别担心,我知道这里是错误的,但我的实际代码已经解决了这个问题。

另一个编辑:

这里是工厂类之一的示例。它位于由 Pex 生成的测试项目的 Factories 目录中。

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 参数控制要使用哪个构造函数。其他工厂方法也有类似的功能。我记得读过,尽管我无法立即找到链接,但这些工厂方法应该允许以每种可能的配置创建对象。


2
不确定您正在解决哪个问题,但如果有人创建了另一个派生自基类的类型,那么听起来他们也会破坏您的实现。这似乎可能会破坏可扩展性并使用户感到惊讶,这两者都是设计上的问题。您是否可以向派生类添加一个属性(可能是“internal”),然后简单地搜索该属性?然后您就不必关心PEX创建存根,因为您不必使用它,并且它不会被注释以导致代码中断。它也不会破坏用户代码。 - Merlyn Morgan-Graham
PexFactoryMethod会实例化一个存根类,并仅采用您对该函数/方法的实现(存根其余虚拟函数)。PexFactorClass覆盖它并给您控制权,应该帮助您阻止Pex添加额外的存根。 - namar0x0309
@namar0x0309 我有点困惑,我承认。你有这方面的文档链接吗?另外,如果我表述不清楚,我会编辑并发布一个代表性的工厂方法。 - Andrew
@Andrew:如果一个对象是“PexFactoryMethod”,Pex会禁用该对象的“PexFactoryClass”属性。我建议你注释掉其中一个,然后进行试错,直到你有一个小的测试类来证明Pex的内部工作原理。它们会在更新中修改这些属性(导致崩溃,因为Pex尝试从抽象中构建一个类,而编码人员想要添加自己的内容等)。 - namar0x0309
1
有人能否发布一个回答,以某种方式帮助解决问题,这样我就可以奖励赏金了。 - Gabriel
显示剩余5条评论
1个回答

1

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接