niedziela, 9 listopada 2014

... czyli testowanie kodu AOP, tkanego za pomocą post-kompilowalnego narzedzia Fody.

Celem tego wpisu jest zaprezentowanie tego jak testować kod zorientowany aspektowo, tkany podczas procesu kompilacji. Pokażę przykład jak przetestować to, czy ten kod aspektowy robi to co chcemy, niezależnie od kontekstu w którym został użyty.

Po wysłuchaniu wykładu Barbary Fusińskiej o programowaniu zorientowanym aspektowo (AOP) postanowiłem nabazgrać trochę kodu (branch: post-unit-tests-advice-aspect-oriented), w którym prezentuję przykład testowania aspektów tkanych podczas procesu kompilacji. Dokładnie to 47 minuta nagrania popchnęła mnie do napisania tego tekstu.


Muszę dodać, że wykład bardzo mi się podobał.

Pod koniec prezentacji Barbara Fusińska zastanawia się jak przetestować wynik działania aspektów, które zostały wplecione w kod w procesie kompilacji (43 minuta nagreania). Zgodnie z tym co wyniosłem z tej prezentacji, testować można to czy aspekty znajdują się tam gdzie chcieliśmy aby się znajdowały (testing the point) oraz to czy aspekty robią to co chcemy (testing the advice). Aspekty możemy wstawiać po przez DI kontenery i/lub po podczas procesu kompilacji (weaving). Biorąc pod uwagę co chcemy testować oraz jak chcemy wstawiać aspekty powstaje nam iloczyn kartezjański dający nam czteroelementowy zbiór zagadnień:

  • Testing the point & DI kontenery
  • Testing the point & Weaving
  • Testing the advice & DI kontenery
  • Testing the advice & Weaving
Poniżej opiszę tylko to jedno, ostatnie zagadnienie, czyli: "Testing the advice & Weaving".

Fody posiada ciekawy gotowy projekt "BasicFodyAddin", za pomocą którego w łatwy sposób możemy wyprodukować własną paczkę NuGet. To, że ten projekt robi nam paczkę NuGet to fajnie ale w kontekście tego postu ciekawszy jest fakt, że projekt ten posiada gotowy przykład testów jednostkowych.

Sklonowałem sobie ten kod i dodałem własny przykład aspektu. Kod dostępny jest tutaj (branch: post-unit-tests-advice-aspect-oriented). Poniżej opisuje newralgiczne miejsca projektu.

Wymyśliłem sobie aspekt, który będzie liczył ile razy dana metoda została wywołana. Wynik obliczeń będzie dostępny w klasie za pomocą pola o nazwie takiej samej jak metoda z dopiskiem "CallsCount".

Czyli mając na przykład klasę:
public class Student
{
    [CountCallsThem]
    public void PassTheExam()
    {
        // ...
    }

    [CountCallsThem]
    public int BuyBeer()
    {
        // ...
        return 1024;
    }
}
w wyniki weavingu otrzymamy klasę:
public class Student
{
    public int PassTheExamCallsCount;
    public int BuyBeerCallsCount;
  
    [CountCallsThem]
    public void PassTheExam()
    {
        this.PassTheExamCallsCount++;
    }
 
    [CountCallsThem]
    public int BuyBeer()
    {
        this.BuyBeerCallsCount++;
        return 1024;
    }
}
Przykład może mało wyszukany, ale na potrzeby zaprezentowania testowania kodu, tkanego podczas kompilacji, jest idealny.

Metoda FindAllMethods w klasie AttributeFinder wyszukuje wszystkie metody wraz z typami, które posiadają te metody.
public IEnumerable<methodintype> FindAllMethods(ModuleDefinition moduleDefinition, string attributeFullName)
{
    var mtCollection = new List<methodintype>();
    var types = moduleDefinition.GetTypes();
            
    foreach (var type in types)
    {
        foreach (var method in type.Methods)
        {
            foreach (var attribute in method.CustomAttributes)
            {
                if (attribute.AttributeType.FullName == attributeFullName)
                {
                    var typeMethodDefinition = new MethodInType { TypeDefinition = type, MethodDefinition = method };
                    mtCollection.Add(typeMethodDefinition);
                }
            }
        }
    }

    return mtCollection;
}
Przy pomocy tej metody będę teraz szukał metod, które posiadają atrybut CountCallsThem. Metoda AddMethodCallsCounter w klasie MethodCallsCounterWeaver wyszukuje i tka metody i klasy z tym atrybutem.
public class MethodCallsCounterWeaver
{
    //...
     private const string COUNT_CALLS_THEM_ATTRIBUTE_NAME = "TeoVincent.AssemblyToProcess.Attributes.CountCallsThemAttribute";
    //...

    public void AddMethodCallsCounter()
    {
        var mtCollection = atributeFinder.FindAllMethods(moduleDefinition, COUNT_CALLS_THEM_ATTRIBUTE_NAME);

        foreach (var mt in mtCollection)
        {
            var item = new FieldDefinition(mt.MethodDefinition.Name + PROPERTY_POSTFIX, FieldAttributes.Public, moduleDefinition.TypeSystem.Int32);
            mt.TypeDefinition.Fields.Add(item);

            var instructions = mt.MethodDefinition.Body.Instructions;

            instructions.Insert(0, Instruction.Create(OpCodes.Nop));
            instructions.Insert(1, Instruction.Create(OpCodes.Ldarg_0));
            instructions.Insert(2, Instruction.Create(OpCodes.Dup));
            instructions.Insert(3, Instruction.Create(OpCodes.Ldfld, item));
            instructions.Insert(4, Instruction.Create(OpCodes.Ldc_I4_1));
            instructions.Insert(5, Instruction.Create(OpCodes.Add));
            instructions.Insert(6, Instruction.Create(OpCodes.Stfld, item));
        }
    }
}
Metoda Execute w klasie HellowWorldWeaver wywołuje kolejne etapy tkania kodu. Oprócz tkania klasy Hello, wywołuje metodę AddMethodCallsCounter z klasy MethodCallsCounterWeaver. Tutaj odpalany jest weaving.
public void Execute()
{
    typeSystem = ModuleDefinition.TypeSystem;
    var newType = new TypeDefinition(null, "Hello", TypeAttributes.Public, typeSystem.Object);

    AddConstructor(newType);
    AddHelloWorld(newType);
    ModuleDefinition.Types.Add(newType);
    LogInfo("Added type 'Hello' with method 'World'.");

    AddMethodCallsCounter();
    LogInfo("Added aspect 'MethodCallsCounter'");
}
Metoda Execute wywoływana jest z metody Setup w klasie testującej WeaverTests. W metodzie tej kopiujemy wynik kompilacji do nowego assembly, które wczytujemy do zmiennej moduleDefinition i przekazujemy do klasy HellowWorldWeaver, na której wywołujemy metodę Execute, która to z kolei tka nam to assembly. Wynik przetkanego assembly pobieramy do zmiennej assembly i wystawiamy dla testów za pomocą właściwości Assembly.
public Assembly Assembly
{
 set { assembly = value; }
 get { return assembly; }
}
Teraz już pozostało napisać kilka testów jednostkowych. Test poniżej sprawdza, czy w wyniku wywołania metody PassTheExam z klasy Student, licznik wywołań tej metody będzie równy jeden.
[Test]
public void Validate_One_Time_Call_Of_Method()
{
    // 1) arrange
    var type = weaverTests.Assembly.GetType("TeoVincent.AssemblyToProcess.Student");
    dynamic instance = Activator.CreateInstance(type);

    // 2) act
    instance.PassTheExam();
    int expected = 1;
    int actual = instance.PassTheExamCallsCount;

    // 3) assert
    Assert.AreEqual(expected, actual);
}
Kolejny test sprawdza czy liczba wywołań metody PassTheExam jest równa zero, gdy nigdy jej nie wywołamy.
[Test]
public void Validate_No_Call_Of_Method()
{
    // 1) arrange
    var type = weaverTests.Assembly.GetType("TeoVincent.AssemblyToProcess.Student");
    dynamic instance = Activator.CreateInstance(type);

    // 2) act
    int expected = 0;
    int actual = instance.PassTheExamCallsCount;

    // 3) assert
    Assert.AreEqual(expected, actual);
}
Ostatni przygład testu to taki, gdzie sprawdzamy dwókrotne wywołanie metody PassTheExam.
[Test]
public void Validate_Two_Times_Call_Of_Method()
{
    // 1) arrange
    var type = weaverTests.Assembly.GetType("TeoVincent.AssemblyToProcess.Student");
    dynamic instance = Activator.CreateInstance(type);

    // 2) act
    instance.PassTheExam();
    instance.PassTheExam();
    int expected = 2;
    int actual = instance.PassTheExamCallsCount;

    // 3) assert
    Assert.AreEqual(expected, actual);
}
Aspekt napisany na potrzeby zademonstrowania testów jednostkowych jest kompletnie nieużyteczny w kodzie produkcyjnym. Aspekt ten, jednak nabierze dużego sensu jeśli rozbudujemy go do postaci takiej, że otrzymamy funkcjonalność logowania ilości przejść przez wybrane metody. Miejscem przeznacznia logowania mogą być pliki, baza, lub nawet wywołanie jakiejś metody serwisowej, cokolwiek. Dane w ten sposób zebrane mogą posłużyć do wygenerowania raportów.

W systemach telekomunikacyjnych, opartych o drzewa IVR, które na codzień rozwijam, takie raporty są bardzo ważnym źródłem informacji dla klienta. Inne przykłady zastosowania tego typu aspektu to raporty w systemach obiegu dokumentów lub w systemach zgłoszeń serwisowych.

Nie o przydatności aspektu miałem pisać, a o tym, jak można przetestować the advice aspektów napisanych przy pomocy post-kompilowalnych narzędzi. Z dużym prawdopodobieństwem przypuszczam, że analogiczne podejście zadziała w przypadku, każdego innego aspektu.

Ja swój przykład oparłem o Fody, na szablonie, który już taki test miał napisany. Pojawia się pytanie czy w Postsharpie też tak można,? Postsharpa jeszcze nigdy nie używałem, ale potrafię sobie wyobrazić, że analogiczne skonstruowanie mechanizmu do testowania aspektów w Postsharpie i w każdym innym narzędziu, również jest możliwe.

0 komentarze :

Prześlij komentarz