Pex i Moles – narzędzia do automatycznego generowania testów jednostkowych  

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2011-02-07

Pobierz kod źródłowy

Wprowadzenie

Testy jednostkowe pomagają zweryfikować poprawność kodu. Najtrudniejsze jest jednak podczas ich implementacji odpowiednie dobranie zestawu danych testowych. Należy dążyć do pokrycia na poziomie 80%. Jeżeli nie znamy kodu źródłowego, to jest to szczególnie trudne, ponieważ tester nie wie, jakie dane spowodują odpowiednie wykonanie instrukcji sterujących. Dysponując kodem źródłowym (nie będą już to zatem testy czarnej skrzynki), łatwiej można osiągnąć wysokie pokrycie, wymaga to jednak analizy kodu źródłowego, co nie należy już do kompetencji testera. Oczywiście programista może doradzić testerowi zestaw danych testujących, jednak lepszym rozwiązaniem jest użycie narzędzia do automatycznego generowania testów – Pex. Narzędzie to analizuje kod źródłowy (testy białej skrzynki) i dobiera dane wejściowe tak, aby każda instrukcja została wykonana. Pex generuje testy oraz dane wejściowe do momentu, w którym zostanie uzyskane całkowite pokrycie kodu.

Istotną cechą testów jednostkowych jest niezależność od zasobów zewnętrznych. Załóżmy, że tester chce zweryfikować poprawność klasy o nazwie AddressManager. AddressManager zawiera metody typu ValidateAddress lub GetAddress, i wykorzystuje do tego celu zewnętrzne zasoby, np. relacyjną bazę danych. W testach jednostkowych jest to niedopuszczalne i należy dążyć do całkowitej izolacji testowanego kodu. Można to zrealizować za pomocą Moles. Moles umożliwia odizolowanie zasobów za pomocą metod symulujących rzeczywiste klasy (stubs) i robi to za pomocą delegat, a nie dodatkowych metod jak to ma miejsce np. we frameworku nMock.

Instalacja

Narzędzie Pex and Moles można ściągnąć ze strony Microsoft Research.

Eksploracja kodu

Po instalacji narzędzie powinno zintegrować się z Visual Studio. Spróbujmy wygenerować testy jednostkowe dla prostej funkcji, składające się z kilku instrukcji sterujących:

public class Math

{
        public int Compute(int value)
        {

            if (value > 10)

                return 10;

            else if (value <= 10 && value >= 0)

                return value;

            else

                return -value;
        }
 }

Powyższy kod realizuje następującą funkcję nieliniową:

W celu eksploracji kodu należy kliknąć na metodzie i z menu kontekstowego wybrać RunPex.

Rysunek 1. Aby rozpocząć eksplorację kodu, należy wybrać Run Pex.

W następnym oknie należy wybrać framework dla testów jednostkowych. Na potrzeby artykułu został użyty standardowy framework wbudowany  w Visual Studio:

Rysunek 2. Pex umożliwia wykorzystanie różnych frameworków do testowania.

Po chwili zostanie wyświetlony raport:

Rysunek 3. Raport Pex.

Jak widać, raport zawiera:

  • wartości wejściowe (value),
  • wartości zwrócone przez metodę (result),
  • ewentualną nazwę wyrzuconego wyjątku (Summary/Exception),
  • ewentualny opis błędu (Error Message).

Pex generował dane wejściowe do momentu, w którym zostały osiągnięte wszystkie gałęzie kodu. Dla badanej funkcji wystarczyło dobrać trzy zestawy testujące. Podstawiając wartości do metody, łatwo sprawdzić, że są one wystarczające do pokrycia całego kodu (trzy instrukcje sterujące wymagają trzech zestawów danych).

Zapisywanie testów

W poprzednich krokach kod został przeanalizowany przez Pex. Teraz należy zapisać rzeczywiste testy jednostkowe. W tym celu należy zaznaczyć wszystkie pozycje z raportu i kliknąć w przycisk Save.

Rysunek 4. Zapis testów jednostkowych.

Następnie należy zaakceptować zmiany w projekcie:

Rysunek 5. Domyślnie Pex wymaga zaakceptowania zmian w projekcie.

Ostatnim krokiem jest wskazanie ścieżki docelowej projektu zawierającego testy jednostkowe.

Rysunek 6. Należy ustawić ścieżkę docelową, zawierającą testy jednostkowe.

Po  ustawieniu ścieżki projekt zostanie wygenerowany i dodany do solucji.

Definiowanie warunków wstępnych

Pex umożliwia automatyczne definiowanie warunków wstępnych. Rozważmy następującą funkcję sumującą wartości kodów ASCI podanego w parametrze wejściowym tekstu:

public int ToNumber(string text)
{
            int sum = 0;

            for (int i = 0; i < text.Length; i++)
            {

                sum += (int)text[i];

            }

            return sum;
}

Następnie rozpocznijmy eksplorację kodu (menu kontekstowe -> Run Pex). Po chwili pojawi się raport:

Rysunek 7. Z raportu wynika, że badana metoda nie obsługuje wartości NULL.

Łatwo zauważyć, że po przekazaniu wartości NULL funkcja wyrzuca wyjątek. Należy zawsze walidować parametry wejściowe i odpowiednio reagować. Pex umożliwia w łatwy sposób dodanie warunków wstępnych. Wystarczy kliknąć na odpowiedniej pozycji w raporcie i z menu kontekstowego wybrać Add Precondition:

Rysunek 8. Wygenerowanie warunku wstępnego.

Po kliknięciu pojawi się okno sugerujące warunek sprawdzający:

Rysunek 9. Zasugerowany warunek wstępny można zmodyfikować według własnych potrzeb.

Jeśli zasugerowany warunek nie jest wystarczający, można go zmodyfikować przed zatwierdzeniem. Po kliknięciu w przycisk Apply zostanie zaktualizowany kod:

public int ToNumber(string text)

{

      // <pex>
      if (text == (string)null)

            throw new ArgumentNullException("text");

      // </pex>

      int sum = 0;

      for (int i = 0; i < text.Length; i++)
     {

             sum += (int)text[i];

      }

      return sum;

}

Warunek wstępny został oznaczony odpowiednim komentarzem (<pex>). Po ponownej eksploracji kodu nie zostanie już wyrzucony wyjątek NullReferenceException, a ArgumentNullException, co jest prawidłową reakcją.

Moles oraz izolacja zasobów

Testy jednostkowe muszą cechować się atomowością. Niedopuszczalne jest, aby testowana klasa wywoływała zdalne zasoby. Odizolowanie zasobów pozwala łatwiej zidentyfikować źródło błędu. Gdybyśmy próbowali testować kilka klas naraz, to w przypadku błędu ciężko byłoby zlokalizować przyczynę, ponieważ należałoby przeszukać wszystkie testowane klasy. Moles pozwala odizolować jedną klasę od drugiej za pomocą obiektów, zwanych makietami. Makieta to zwykła klasa, która zwraca z góry określone wartości. Załóżmy, że chcemy przetestować następujący menedżer:

public class AddressManager

    {

        private IProvider m_Provider = null;

        public AddressManager(IProvider provider)

        {

            if (provider == null)

                throw new ArgumentNullException();

            m_Provider = provider;

        }

        public string GetAddress(int idUser)

        {

                 return string.Format("{0}\n{1} {2}",   

                m_Provider.GetTownNameOfUser(idUser),

                m_Provider.GetStreetNameOfUser(idUser),

                m_Provider.GetStreetNumberOfUser(idUser));

        }    

    }

Obiekt m_Provider umożliwia zwrócenie danych, np. z bazy danych, i oparty jest na następującym interfejsie:

public interface IProvider

    {

        string GetTownNameOfUser(int idUser);

        string GetStreetNameOfUser(int idUser);

        string GetStreetNumberOfUser(int idUser);

    }

Przykładowy provider wykorzystujący relacyjną bazę danych:

class SqlProvider : IProvider

    {

        #region IProvider Members

        public string GetTownNameOfUser(int idUser)

        {

            System.Data.SqlClient.SqlConnection connection = new System.Data.SqlClient.SqlConnection("jakiś connection string, nie ma to znaczenia dla tego artykułu");

            connection.Open();

            System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient.SqlCommand("select TownName From Address where ID_USER=@ID_USER", connection);

            cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@ID_USER",idUser));

            string townName= cmd.ExecuteScalar().ToString();

            connection.Close();

            return townName;

        }

        public string GetStreetNameOfUser(int idUser)

        {

            throw new NotImplementedException();

        }

        public string GetStreetNumberOfUser(int idUser)

        {

            throw new NotImplementedException();

        }

        #endregion

    }

Jak widać SqlProvider odwołuje się do zewnętrznych zasobów (bazy SQL Server). Aby przetestować taką klasę, należy użyć makiety, która zamiast łączyć się z bazą danych zwracałaby predefiniowane dane. Do tego służy właśnie narzędzie Moles. W celu wygenerowania makiety wystarczy:

  1. W Solution Explorer klikamy na References oraz z menu kontekstowego wybieramy Add Moles assembly from mscorlib.


    Rysunek 10. Dodawanie wymaganych bibliotek.

  2. Klikamy na metodzie GetAddress oraz standardowo wybieramy z menu kontekstowego Run Pex. Następnie zapisujemy testy oraz przechodzimy do wygenerowanego pliku zawierającego testy jednostkowe.

  3. Przyglądamy się wygenerowanej metodzie testującej:

[TestMethod]

[PexGeneratedBy(typeof(AddressManagerTest))]

        public void GetAddress825()

        {

            using (PexChooseBehavedBehavior.Setup())

            {

                SIProvider sIProvider;

                AddressManager addressManager;

                string s;

                sIProvider = new SIProvider();

                addressManager = new AddressManager((IProvider)sIProvider);

                s = this.GetAddress(addressManager, 0);

                Assert.AreEqual<string>("\n ", s);

                Assert.IsNotNull((object)addressManager);

            }

        }

Visual Studio automatycznie stworzył makietę o nazwie SIProvider. Jeśli przejdziemy do definicji, to zobaczymy, że makieta oparta jest na delegatach:

[StubClass(typeof(IProvider))]

[DebuggerNonUserCode]

[DebuggerDisplay("Stub = IProvider")]

    public class SIProvider : StubBase, IProvider

    {

        // Summary:

        //     Sets the stub of ClassLibrary.IProvider.GetStreetNameOfUser(T:System.Int32)

        public MolesDelegates.Func<int, string> GetStreetNameOfUserInt32;

        //

        // Summary:

        //     Sets the stub of ClassLibrary.IProvider.GetStreetNumberOfUser(T:System.Int32)

        public MolesDelegates.Func<int, string> GetStreetNumberOfUserInt32;

        //

        // Summary:

        //     Sets the stub of ClassLibrary.IProvider.GetTownNameOfUser(T:System.Int32)

        public MolesDelegates.Func<int, string> GetTownNameOfUserInt32;

        // Summary:

        //     Initializes a new instance of type SIProvider

        public SIProvider();

    }
  1. Do delegat należy zatem podłączyć kod symulujący zachowanie providera:
[TestMethod]

[PexGeneratedBy(typeof(AddressManagerTest))]

        public void GetAddress825()

        {

            using (PexChooseBehavedBehavior.Setup())

            {

                SIProvider sIProvider;

                AddressManager addressManager;

                string s;

                sIProvider = new SIProvider();

                sIProvider.GetStreetNameOfUserInt32=new MolesDelegates.Func<int,string>((idUser)=>"ulica");

                sIProvider.GetStreetNumberOfUserInt32 = new MolesDelegates.Func<int, string>((idUser) => "56");

                sIProvider.GetTownNameOfUserInt32 = new MolesDelegates.Func<int, string>((idUser) => "miasto");

                addressManager = new AddressManager((IProvider)sIProvider);

                s = this.GetAddress(addressManager, 0);

                Assert.AreEqual<string>("miasto\nulica 56", s);

                Assert.IsNotNull((object)addressManager);

            }

        }

Dzięki Moles udało się odizolować bazę danych od testowanego kodu. Co więcej, większość pracy zostało wykonana automatycznie. Nie musieliśmy tworzyć samodzielnie obiektu makiety. Nawet test jednostkowy został automatycznie wygenerowany z wykorzystaniem makiety. Podczas eksploracji kodu narzędzie Pex&Moles samo „domyśliło się”, że w konstruktorze prawdopodobnie będzie potrzebna makieta.

Należy jednak pamiętać, że testowanie kodu jest możliwe tylko wtedy, gdy architektura została prawidłowo opracowana. Gdyby konstruktor AddressManagera nie przyjmował klasy implementującej IProvider, nie byłoby możliwe wstrzyknięcie makiety. Dobry kod zatem powinien być zgodny z zasadą IoC (inversion of control) oraz wykorzystywać interfejsy, a nie wyłącznie konkretne klasy.

Zakończenie

Framework Pex&Moles znacząco ułatwia testowanie aplikacji. Tester nie musi więc martwić się o podstawowe testy jednostkowe, gdyż zostaną one z pewnością wygenerowane automatycznie. Należy jednak pamiętać, że całkowite pokrycie kodu nie gwarantuje, że aplikacja została prawidłowo przetestowana. Ponadto źle napisany kod może okazać się bardzo trudny lub nawet niemożliwy do przetestowania. Dokładną dokumentację frameworka można znaleźć na stronach Microsoft Research.


          

Piotr Zieliński

Absolwent informatyki o specjalizacji inżynieria oprogramowania Uniwersytetu Zielonogórskiego. Posiada szereg certyfikatów z technologii Microsoft (MCP, MCTS, MCPD). W 2011 roku wyróżniony nagrodą MVP w kategorii Visual C#. Aktualnie pracuje w General Electric pisząc oprogramowanie wykorzystywane w monitorowaniu transformatorów . Platformę .NET zna od wersji 1.1 – wcześniej wykorzystywał głównie MFC oraz C++ Builder. Interesuje się wieloma technologiami m.in. ASP.NET MVC, WPF, PRISM, WCF, WCF Data Services, WWF, Azure, Silverlight, WCF RIA Services, XNA, Entity Framework, nHibernate. Oprócz czystych technologii zajmuje się również wzorcami projektowymi, bezpieczeństwem aplikacji webowych i testowaniem oprogramowania od strony programisty. W wolnych chwilach prowadzi blog o .NET i tzw. patterns & practices.