Jak testować kod z pozoru nietestowalny Udostępnij na: Facebook

Autor: Arkadiusz Benedykt

Opublikowano: 2011-12-16

W poprzednich częściach cyklu zapoznałeś się z podstawami TDD, zastosowanymi na dość rzeczywistym przykładzie. W codziennej pracy często może się zdarzyć, że ta wiedza nie będzie wystarczająca i będziesz musiał skorzystać z zewnętrznych bibliotek. Co wtedy? Z zasady nie powinno się testować zewnętrznej biblioteki – powinien to zrobić dostawca. Można jednak testować kod współpracujący z tą biblioteką. Pomocne wtedy stają się takie biblioteki jak stara i sprawdzona Rhino Mocks, nowsza i bardzo wygodna Moq, czy też prawdopodobnie jedna z najciekawszych – Pex and Moles.

Kod źródłowy zamieszczonych w tym artykule przykładów znajduje się tutaj

Zacznij od biblioteki Moq oraz wzorca Command. Spróbuj stworzyć przykładowy kod, wykonywujący komendy – command invoker. Nie będziesz się skupiać na implementacji konkretnych komend, ponieważ idea wzorca Command jest taka, że nie musisz znać wszystkich implementacji komend w momencie tworzenia invoker-a.

Chcesz, aby Twój kod działał w następujący sposób:

  • najpierw uruchamiana jest metoda Validate w celu sprawdzenia, czy w ogóle można wykonać komendę,
  • następnie jest uruchamiana metoda Execute – wykonująca rzeczywistą logikę komendy,
  • następnie uruchamiane są „podkomendy”, jeżeli komenda takowe posiada,
  • ponieważ chcesz uniezależnić się od implementacji komend, zdefiniuj odpowiedni interfejs.

Docelowo chcesz stworzyć klasę CommandExecutor, interfejs ICommand, wykonujący obiekty implementujące.

Interfejs ICommand:

 

public interface ICommand
    {
 
        bool Valid();
        void Execute();
        bool Executed { get; }
        bool Failed { get; }
        string ErrorMessage { get; }
        dynamic Result { get; }
 
    }

Napisz zatem pierwszy test, który upewni Cię, że tworzona klasa CommandExecutor uruchamia komendę Validate obiektu implementującego ICommand.

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Tests
{
    [TestClass]
    public class CommandExecutorTests
   {
        private CommandExecutor _executor;

       [TestInitialize]
        public void Setup()
        {
            ICommand command = ....
            _executor = new CommandExecutor();
            _executor.Execute(command);

        }

        [TestMethod]
       public void CommandExecutor_Execute_Wywoluje_Metode_Validate()
       {
            jakiś sposób sprwadzenia, że Valid rzeczywiście zostało wywołane
        }

    }

}

W tym momencie pojawiają się dwa problemy. Pierwszy to taki, że nie ma implementacji żadnej komendy, drugi to taki, że nie ma mechanizmu sprawdzenia, czy metoda została wykonana.

Wówczas z pomocą przychodzi biblioteka Moq (do pobrania na stronie http://code.google.com/p/moq/). Dodaj ją ręcznie do referencji lub posiłkuj się NuGet-em (Rys.1.):

Rys. 1. Instalacja biblioteki Moq.

Po dodaniu biblioteki Moq, możesz przystąpić do pracy. Zastąp metodę Setup na:

[TestInitialize]
        public void Setup()
        {
            _iCommandMock = new Mock<ICommand>();
            _iCommandMock.Setup(c=>c.Execute()).Verifiable();
            
            ICommand command = _iCommandMock.Object;
 
            _executor = new CommandExecutor();
            _executor.Execute(command);
        }

Biblioteka Moq pozwala stworzyć obiekt proxy, który implementuje interfejs ICommand. (New Mock<ICommand>). Kolejna linia definiuje sposób działania Twojego wirtualnego obiektu. Tutaj musisz określić, że metoda Execute ma być weryfikowalna –  musi zaistnieć możliwość zweryfikowania, czy metoda została uruchomiona – wywołana. Trzecia linia to przypisanie przygotowanego przez Ciebie obiektu do zmiennej typu ICommand. Reszta, to już standardowe użycie CommandExecutora. To tyle, jeśli chodzi o TestInitialize. Sam test wyglądać będzie tak:

[TestMethod]
        public void CommandExecutorExecuteWywolujeMetodeExecute()
        {
            _iCommandMock.Verify(c => c.Execute());
        }

Zamiast standardowej asercji, sprawdź, czy metoda Execute została wywołana. Oprócz prostego sprawdzenia, czy metoda została wywołana, biblioteka Moq pozwala na wiele więcej (Rys.2.):

Rys. 2. Kila możliwości, jakie daje biblioteka Moq.

Zgodnie z metodyką TDD, czas na implementację:

namespace TDD_Demo_3
{
    public class CommandExecutor
    {
        public void Execute(ICommand command)
        {
     

        }
    }
}

oraz uruchomienie testu (Rys.3.):

Rys. 3. Uruchomienie testu.

Powyższy zrzut ekranu pochodzi z dodatku do Visual Studio – ReSharper-a. Te same informacje otrzymasz w oknie Test Run, jednak ReSharper pokazuje te informacje w formie trochę bardziej czytelnej.

To co widzisz, to błąd informujący o tym, że metoda Execute nie została wywołana.

Popraw implementację:

namespace TDD_Demo_3
{
    public class CommandExecutor
    {
        public void Execute(ICommand command)
        {
            command.Execute();
        }
    }
}

i od tej pory test przechodzi pozytywnie.

Chcesz, aby przed wykonaniem komendy sprawdzać, czy spełnia ona założenia biznesowe – metoda Valid zwraca true. Wprowadź zatem potrzebne zmiany:

[TestInitialize]
        public void Setup()
        {
            _iCommandMock = new Mock<ICommand>();
            _iCommandMock.Setup(c => c.Valid()).Returns(true).Verifiable();
            _iCommandMock.Setup(c=>c.Execute()).Verifiable();
          

            ICommand command = _iCommandMock.Object;

 

           _executor = new CommandExecutor();
            _executor.Execute(command);
        }

Dodatkowa linia kodu mówi, że wywołanie metody Valid zwraca zawsze true i jest weryfikowalna.Dodaj również test:

[TestMethod]
        public void CommandExecutorExecuteWywolujeMetodeValidate()
        {
            _iCommandMock.Verify(c => c.Valid());
        }

który sprawdza, czy metoda Valid została wywołana. Ostatni krok – implementacja:

namespace TDD_Demo_3
{
    public class CommandExecutor
    {
        public void Execute(ICommand command)
        {
            if (command.Valid())
            {
                command.Execute();

            }
        }

    }

}

Stworzyłeś zatem prostą implementację wzorca Command, a dodatkowo implementacja ta została przetestowana w dodatku, bez tworzenia jakiegokolwiek obiektu Command – przetestowałeś samą implementację wzorca. Teraz masz już pewność, że jakakolwiek komenda, przekazana do CommandExecutora, zostanie wykonana zgodnie z Twoimi założeniami.

Po co w ogóle stosować ten wzorzec? Otóż pozwala on na stworzenie jednego centralnego obiektu, odpowiedzialnego za wykonywanie akcji. Dzięki temu, dodając w jednym miejscu logowanie zdarzeń, w aplikacji będziesz miał pewność, że każde wywołanie akcji/komendy zostanie zauważone. Nie ma potrzeby kopiowania kodu logowania do setek miejsc w kodzie – unikaj niepotrzebnej redundancji oraz miej pewność, że nie zapomnisz o jakiejś zagubionej akcji, której mógłbyś nie uzyskać w sytuacji, gdybyś nie zastosował tego wzorca.

Biblioteki, takie jak Moq, dają możliwość tworzenia testów dla kodu, który pozwala się testować. Niestety ciągle jeszcze nie wszystkie biblioteki i nie wszystkie kontrolki są na to przygotowane. W sytuacjach z pozoru beznadziejnych przychodzi biblioteka Pex and Moles, wydana przez zespół Microsoft Reaserch – do pobrania tutaj: https://research.microsoft.com/en-us/projects/pex/

Biblioteka ta składa się z dwóch części:

  • Pex odpowiedzialne jest za generowanie testów jednostkowych.
  • Moles pozwala na zastąpienie każdej metody .NET delegatem.

Zacznij od Moles – które pomogą Ci testować kod z pozoru nietestowany. W tym celu wróć do problemu obliczania wieku z drugiej części serii:

using System;

namespace TDD_Demo_3
{
    public class Osoba
    {
        public Osoba(string imie, string nazwisko, DateTime dataUrodzenia)
        {
            Imie = imie;
            Nazwisko = nazwisko;
            DataUrodzenia = dataUrodzenia;
        }

 

        public string Imie { get; private set; }

        public string Nazwisko { get; private set; }

        public DateTime DataUrodzenia { get; private set; }

        public int Wiek
        {
            get
            {
                return DateTime.Now.Year - DataUrodzenia.Year;
            }

        }

       public override string ToString()
        {
           return string.Format("{0} {1}", Imie, Nazwisko);

        }

 

    }

}

Od kodu, który pojawił się w części drugiej różni się tym, że nie ma sztucznego interfejsu IWiek. Dzięki bibliotece Moles, możesz taki kod przetestować, magicznie powodując, że DateTime.Now będzie zawsze zwracać konkretną, wybraną przez Ciebie datę.

Dokonaj analizy następującego testu:

[TestMethod]
       public void ObliczanieWieku()
        {
            var osoba = new Osoba("Banach", "Stefan", new DateTime(1892, 03, 30));

 

           Assert.AreEqual(53, osoba.Wiek);

        }

Właściwość Wiek będzie zwracać różne wyniki w różnych latach. Taki test nie ma wielkiej przydatności, ponieważ następnego dnia, po najbliższym Sylwestrze, test przestanie działać poprawnie. Możesz temu zaradzić, zaprzęgając Moles do pracy. Dodaj Moles do referencji (Rys. 4.).

Rys. 4. Dodanie Moles do projektu.

W pliku AssemblyInfo.cs dodaj:

[assembly: MoledType(typeof(System.DateTime))]

Pozwoli Ci to nadpisać DateTime, zgodnie z Twoimi potrzebami:

[TestMethod]
        [HostType("Moles")]
        public void ObliczanieWieku()
        {
            MDateTime.NowGet = () => new DateTime(1945, 12, 30, 23, 03, 24);

            var osoba = new Osoba("Banach", "Stefan", new DateTime(1892, 03, 30));
            Assert.AreEqual(53, osoba.Wiek);

        }

Magiczny DateTime będzie zwracał określoną przez Ciebie datę. To pozwoli na to, że dalszy kod – mimo, że nie został zmieniony – będzie zwracał zawsze tę samą datę (nawet rok później).

Moles, pomimo, że na początku wydaje się trudne do skonfigurowania, to pozwala przetestować kod z pozoru nietestowalny.

Druga część – Pex – daje (według mnie) dosyć kontrowersyjną możliwość – automatyczne generowanie testów jednostkowych. Kontrowersyjność polega na tym, że pokusa automatycznego wygenerowania testów może spowodować, że zamiast korzystać z możliwości TDD, można kilkoma kliknięciami „zaoszczędzić” cały trud.

Problem polega na tym, że Pex generuje testy do istniejącego kodu. Jeśli kod będzie błędny, wówczas i wygenerowane testy będą błędne. Dlatego NIE WOLNO stosować Pex do zastąpienia ręcznie tworzonych testów jednostkowych. Do czego zatem można wykorzystać to narzędzie? Mając napisany (zgodnie z TDD), a także kod i testy jednostkowe, warto zaprzęgnąć Pex do znalezienia tego, co mogłeś przeoczyć. W takim scenariuszu Pex staje się prawdziwym skarbem.

Patrząc na poniższy kod, proponuję wykonać prosty eksperyment:

namespace Matematyka
{

    public class OperacjeMatematyczne{

         public decimal Dzielenie(long a, long b, long c)
        {
                  return (a * b)/ c;

        }

    }

}

Proszę określić wszystkie problemy, jakie mogą mieć miejsce w powyższej implementacji.

Następnie zobacz, co otrzymasz podczas uruchamiania Pex (Rys. 5.).

Rys. 5. Uruchomienie biblioteki Pex.

Z menu podręcznego wybierz Run Pex , a następnie poczekaj na wyniki.

Rys. 6. Wynik działania Pex-a.

Pex wygenerował (Rys. 6.) 3 przypadki testowe, z których dwa powodują błąd. Pierwszy – DivideByZeroException jest bardzo łatwy do zauważenia – no cóż, podziel go przez zero. Drugi natomiast nie dla wszystkich jest oczywisty i nie rzuca się w oczy tak bardzo – OverflowException.

W tej chwili nie pozostaje Ci już nic innego, jak wygenerować odpowiedni (Rys. 7.) test jednostkowy:

Rys. 7. Wygenerowanie testu jednostkowego.

i poprawić kod.

Narzędzie to pokazuje dobitnie, jak trudne jest pisanie testów jednostkowych, które nie tylko pokryją 100% kodu, ale pokryją 100% przypadków lub raczej ten 1% przypadków, które mogą sprawiać problem. Maciej Aniserowicz, polski MVP, napisał: „…świetne testy chronią przed bugami przy zmianach w kodzie. Ale nauka pisania świetnych testów trwa latami…” i trudno się z tym nie zgodzić. Umiejętność pisania testów jednostkowych należy rozwijać tak samo jak umiejętność pisania dobrego kodu. Testy powinny dotyczyć wszystkich problematycznych przypadków oraz pomagać, a nie być tzw. kulą u nogi. Źle napisane testy będą dodatkowym ciężarem przy utrzymaniu kodu (czyli będą generowały koszty) i niekoniecznie będą testowały Twój kod prawidłowo. Dlatego narzędzia Pex należy używać ze świadomością i uwagą – wówczas będzie to genialny partner w zmaganiach z kodem – oczywiście pokazane tutaj wykorzystanie Pex nie jest jedynym i nie porusza całości zastosowań, jednak moim zdaniem dla metodyki TDD jest jednooko jednym z ciekawszych.

A mówiąc o Pex muszę przestrzec przed stroną http://www.pexforfun.com, gdzie na podstawie wyników testów jednostkowych, należy odtworzyć nieznany kod. Okazuje się, że tylko na podstawie tego, co podpowiada Ci Pex, możesz odgadnąć nieznaną Tobie implementację. Strona ta potrafi wciągnąć na bardzo długo.