2011-09-21 6 views
59

Próbuję użyć Pex do przetestowania kodu. Mam klasę abstrakcyjną z czterema konkretnymi implementacjami. Stworzyłem metody fabryczne dla każdego z czterech rodzajów betonu. Stworzyłem także jeden dla typu abstrakcyjnego, z wyjątkiem, jak wyjaśnił this nice thread, Pex nie użyje abstrakcyjnej metody fabrycznej, ani nie powinien.Jak powiedzieć Peksowi, aby nie był stubem klasy abstrakcyjnej z konkretnymi implementacjami

Problem polega na tym, że niektóre z moich kodów zależą od tego, że istnieją cztery konkretne typy (ponieważ jest bardzo mało prawdopodobne, że zostaną utworzone więcej podklas), ale Pex łamie kod przy użyciu kresek do utworzenia stub.

Jak zmusić Pex do korzystania z jednej z metod fabrycznych (nieważne, czy nie) do tworzenia wystąpień klasy abstrakcyjnej bez tworzenia skrótów Moles dla tej klasy abstrakcyjnej? Czy istnieje dyrektywa PexAssume, która to osiągnie? Zauważ, że niektóre typy betonu tworzą rodzaj struktury drzewa, więc powiedzmy, że ConcreteImplementation pochodzi od AbstractClass, a ConcreteImplementation ma dwie właściwości typu AbstractClass. Muszę się upewnić, że w drzewie nie ma żadnych skrótów. (Nie wszystkie konkretne implementacje mają AbstractClass właściwości.)

Edycja: Wydaje

że trzeba dodać trochę więcej informacji o tym, jak sama struktura klasa działa, choć należy pamiętać, że celem jest nadal jak dostać Pex nie do klas pośrednich.

Oto uproszczone wersje abstrakcyjnej klasy bazowej i czterech konkretnych jej implementacji.

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); 
    } 
} 

Sercem co próbuję przetestować to metoda Distill, który jak widać ma potencjał, aby uruchomić rekursywnie. Ponieważ skrótowy kod AbstractClass jest bez znaczenia w tym paradygmacie, łamie on logikę algorytmu. Nawet próba sprawdzenia, czy jest zajęta, jest nieco bezużyteczna, ponieważ niewiele mogę z tym zrobić, poza wyrzuceniem wyjątku lub udawaniem, że jest to instancja o numerze Implementation1. Wolałbym nie przepisywać testowanego kodu, aby w ten sposób dostosować się do konkretnych ram testowania, ale staram się napisać test w taki sposób, aby nigdy nie wypełniać kodu AbstractClass.

Mam nadzieję, że jest oczywiste, jak to, co robię, różni się na przykład od konstrukcji bezpiecznej dla typu. Ponadto, anonimizowałem obiekty do zamieszczenia tutaj (jak można powiedzieć) i nie uwzględniłem wszystkich metod, więc jeśli masz zamiar skomentować, aby powiedzieć, że Implementation4.Equals(Implementation4) jest zepsuty, nie martw się, jestem świadomy, że jest tutaj zepsuty, ale mój aktualny kod zajmuje się problemem.

Kolejny edit:

Oto przykład jednej z klas fabrycznych. Znajduje się w katalogu Factories w projekcie testowym generowanym przez 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; 
    } 
} 

W moich metodach fabrycznych dla tych konkretnych realizacji, możliwe jest użycie dowolnego konstruktora do stworzenia konkretnej implementacji. W tym przykładzie parametr useEmptyConstructor określa, którego konstruktora użyć. Inne metody fabryczne mają podobne cechy. Pamiętam, że czytam, choć nie mogę od razu znaleźć linku, że te metody fabryczne powinny umożliwić stworzenie obiektu w każdej możliwej konfiguracji.

+2

Nie jestem pewien, jaki problem rozwiązujesz z tą implementacją, ale jeśli ktoś kiedykolwiek tworzy inny typ wywodzący się z klasy bazowej, to brzmi tak, jakby złamał twoją implementację. Wygląda na to, że może to złamać zarówno rozszerzalność, jak i zaskoczyć użytkownika, z których oba są zapachem projektowym. Czy zamiast tego możesz dodać atrybut (prawdopodobnie "wewnętrzny") do swoich klas pochodnych i po prostu go wyszukać? Wtedy nie musisz przejmować się tym, że PEX tworzy stub, ponieważ nie musisz go używać i nie zostanie on zanotowany w sposób powodujący złamanie twojego kodu. Nie złamie również kodu użytkownika. –

+0

@ MerlynMorgan-Graham Dzięki za twój wkład. W rzeczywistości ten projekt jest bardziej odpowiedni dla F # niż C#, ale przyszłe możliwości konserwacji są problemem. Zachowanie jest bliższe "dyskryminowanej unii" niż prawdziwemu dziedziczeniu. To powiedziawszy, cztery podklasy abstrakcyjnej klasy bazowej reprezentują zamknięty zbiór operacji w ramach utworzonej przeze mnie struktury obliczeniowej. Nikt tego nie rozszerzy, ale zarówno abstrakcyjna klasa bazowa, jak i konkretne podklasy muszą być widoczne poza ich montażem. Jeśli jest coś, co masz na myśli przez "wewnętrzny", nie jestem pewien co to jest. – Andrew

+0

Jeśli tylko dla klas pochodnych ma sens, to po co się martwić - Czy rzeczywiście * coś złamie? Jeśli tak, to w jaki sposób można wykryć klasy pochodne? Próbowałem zapewnić alternatywę dla twojego mechanizmu wykrywania. Wydaje się, że macie schemat podobny do enumu bezpiecznego dla typu. Możesz wykonać ten schemat całkowicie i wykonać wszystkie swoje wewnętrzne implementacje i po prostu dokonać statycznych właściwości fabrycznych w klasie bazowej dla czterech implementacji. Nazwij je poprawnie, aby utworzyć właściwy typ, ale zwróć je jako typ podstawowy. –

Odpowiedz

1

Czy próbowałeś powiedzieć Pexowi używając atrybutu [PexUseType], że istnieją nie abstrakcyjne podtypy dla twojej abstrakcyjnej klasy? Jeśli Pex nie zna żadnych nieistniejących podtypów, to narzędzie rozstrzygające Pex wyznaczy, że ścieżka kodu, która zależy od istnienia nietypowego podtypu, jest nieosiągalna.

Powiązane problemy