UI Automation – czyli testy automatyczne w .NET

Biblioteka Microsoft UI Automation ujrzała światło dzienne wraz z premierą .NET 3.0 – jednakże pozostała ona w cieniu swoich większych braci WPF oraz WPF, które również zostały wprowadzone do Frameworka 3.0. Microsoft UI automation zapewnia nam dostęp do wszystkich elementów drzewa wizualnego aplikacji. Dzięki czemu mamy możliwość:

  • Znajdowania wybranych przez nas kontrolek
  • Interakcji z kontrolkami – wpisywanie tekstów do TextBox-ów, klikanie w przyciski itp
  • Wczytywania wartości już wprowadzonych do kontrolek

Każda kontrolka znajdująca się na widoku jest traktowana przez UI Automation jako AutomationElement. Rootem naszego drzewa wizualnego pulpit, dostęp do niego uzyskujemy poprzez statyczną właściwość

Mając dostęp do roota możemy następnie poruszać się po drzewie tak aby znaleźć interesujące nas elementy. Załóżmy, że napisaliśmy prostą aplikację WPF-ową, która wykonuje podstawowe obliczenia matematyczne (pomysł na prostą aplikację do testowania zaczerpnięty z tej strony – kod jednak pisałem po swojemu :D).
Wizualnie aplikacja ta przedstawia się w następujący sposób:

W celu poruszania się po drzewie wizualnym możemy skorzystać z dwóch metod

pierwsza z nich zwraca nam konkretny element, który spełnia podane przez nas kryterium. Pierwszy parametr funkcji TreeScope scope określa nam sposób przeszukiwania drzewa wizualnego. Dostępne opcje to:

  • Element – wyszukiwanie odbywa się tylko na danym elemencie
  • Children – wyszukiwanie odbywa się na wszystkich dzieciach danego elementu – nie zagłębiamy się rekurencyjnie,sprawdzamy tylko jeden poziom w dół od danego elementu
  • Descendants – wyszukiwanie odbywa się na wszystkich potomkach – przeszukiwanie rekurencyjne w dół
  • Subtree – wyszukiwanie odbywa się na wszystkich potomkach, oraz elemencie od którego zaczynamy
  • Ancestors – wyszukiwanie odbywa się na wszystkich rodzicach – rekurencyjnie w górę drzewa

Drugim z parametrów jakie musimy podać jest to UIAutomation.Condition. Parametr ten określa jaki warunek musi spełnić dany element aby został uwzględniony w wyniku wyszukiwania. Sama klasa Condition jest klasą abstrakcyjną, z której dziedziczą wyspecjalizowane klasy. Do określenia warunków najczęściej będziemy korzystać z potomka klasy Condition, mianowicie z

określa ona jaka właściwość kontrolki/aplikacji będzie uwzględniana przy wyszukiwaniu wyniku.

Pierwszym krokiem przy testowaniu aplikacji będzie oczywiście znalezienie głównego okna programu. Mając w solucji stworzony projekt z aplikacją WPF-ową, dodajmy drugi projekt – tym razem będzie to projekt typu ConsoleApplication.Następnie musimy dodać referencję do odpowiednich ddl-ek. Potrzebujemy następujących bibliotek:

  • UIAutomation.dll
  • UIAutomationClient.dll
  • UIAutomationType.dll

Następnie w naszym projekcie konsolowym w metodzie main odpalamy aplikację, którą chcemy testować. W moim przypadku wygląda to w następujący sposób:

Następnie musimy dostać się do głównego okna testowanej aplikacji. Możemy to zrobić używając funkcji FindFirst:

lub możemy uzyskać dostęp do okna przy pomocy funkcji

W pierwszym przypadku wykorzystując funkcję FindFirst przeszukujemy wszystkie dzieci (TreeScope.Children) roota (czyli pulpitu). Jako warunek wyszukiwania podaliśmy obiekt klasy PropertyCondition. Jako właściwość do porównywania podaliśmy id processu (AutomationElement.ProcessIdProperty). W drugim przypadku po prostu uzyskujemy dostęp do okna dzięki znajomości jego uchwytu (właściwość MainWindowHandle z klasy Process). Według mnie lepiej skorzystać z funkcji pierwszej, zwłaszcza gdy odpalamy aplikację, która się długo uruchamia. W pierwszym przypadku wystarczy zrobić odpowiednią funkcję oczekującą na odpalanie (gdyż funkcja zwróci nam null gdy okna jeszcze nie będzie), natomiast druga opcja rzuci nam wyjątek. Oczekiwania na załadowanie okna może wyglądać w następujący sposób

Mając dostęp do głównego okna możemy przeprowadzić testy – przetestujemy czy po wpisaniu danych do tekstboksów i przeprowadzeniu odpowiednich akcji (dodawanie,usuwanie,odejmowanie,dzielenie) w polu wynik pojawi się odpowiednia wartość. Zacznijmy od zlokalizowania textboxów. Posłużmy się tutaj przedstawioną wcześniej funkcją FindFirst.

Wyszukiwanie odbywa się po właściwości AutomationElement.AutomationIdProperty – właściwość ta jest zawsze taka sama jak nazwa kontrolki – chyba, że ktoś ją zmieni przy pomocy AttachedProperty AutomationProperties.AutomationId. Odpalając powyższy kod okazuje się, że nasza zmienna firstTextbox jest nullem – czyli framework nie był w stanie znaleźć naszego textboxa. Dlaczego tak się stało?? Odpowiedzi może nam dostarczyć aplikacja Snoop, dzięki której możemy podejrzeć drzewo wizualne naszego programu.
snoopResult
Na powyższym screenie widzimy, że szukany textbox nie jest bezpośrednim dzieckiem MainWindow, jest natomiast dzieckiem grida (tego można było się spodziewać zwłaszcza, że umieściliśmy textbox w gridzie). Zauważmy jednak, że MainWindow nie jest bezpośrednim rodzicem grida – znajduje się on dopiero na poziomie 4 w drzewie. Z racji dużego skomplikowania drzewa wizualnego aplikacji według mnie bezpiecznie jest używać funkcji Find z parametrem TreeScope.Descendants. Unikniemy dzięki temu przykrych niespodzianek z nieodnalezionymi kontrolkami. Nasza funkcja zatem powinna wyglądać w następujący sposób.

W analogiczny sposób znajdujemy pozostałe przyciski na naszej formie, a następnie postarajmy się wprowadzić do nich jakiś text. W celu interakcji z kontrolkami wykorzystamy tzw. Patterns
Patterns definiują określone funkcjonalności, które wspiera nasza kontrolka. Do najczęściej używanych patterns należą:

  • SelectionPattern – używany do manipulacjami kontrolkami wspierającymi zaznaczanie np.ListBox-ami
  • TextPattern – używany do manipulacjami kontrolkami wspierającymi edycję
  • ValuePattern – używany do pobierania i ustawiania wartości kontrolek nie wspierających wielokrotnych wartości
  • InvokePattern – używane do kontrolek wspierających wywołania – np. przyciski (wywołanie przyciśnięcia)
  • ScrollPattern – używane do kontrolek posiadających ScrolBar-y
  • RangeValuePattern – używany do kontrolek mogących posiadać jakiś zakres wartości np. ComboBox

Zatem w celu ustawienia wartości w textbox-ie posłużymy się następującym kodem

W analogiczny sposób ustawiamy zawartość drugiego textbox-a

Mając wprowadzone dane do textbox-ów wypadałoby teraz przeprowadzić obliczenia. Zatem musimy w jakiś sposób aby “nacisnąć” jeden z naszych czterech przycisków. Znajdźmy zatem nasze przyciski:

Następnie wykorzystując wspomniany wcześniej InvokePattern naciśnijmy przycisk “Dodaj”.

Ostatnim krokiem będzie zweryfikowanie czy wartość w textbox-ie txtResult jest zgodna z oczekiwaniami. W celu wyciągnięcia wartości z txtResult po raz kolejny posłużę się ValuePattern

Wynik otrzymany w textbox-ie porównujemy z oczekiwanym rezultatem

Na koniec najlepiej wrzucić nasz test w jakąś pętle, losowo generować wartości oraz porównywać je z wartościami oczekiwanymi. Dobrym pomysłem jest również stworzenie sobie jakiejś klasy pomocniczej, w której zostałyby umieszczone funkcje do wypełniania textbox-ow, klikania w buttony itp – gdyż jak widać dużo kodu jest powtarzalna. Ostatecznie aplikacja testowa może wyglądać w następujący sposób:

oraz klasa pomocnicza

result

UI Automation – czyli testy automatyczne w .NET