UI Automation

In acest articol voi prezenta cum se poate dezvolta un instrument de test pentru o aplicatie care prezinta o interfata grafica.

Microsoft a facut eforturi considerabile in aceasta directie si a dezvoltat in intreg framework cunoscut sub numele de UI Automation si se ragaseste in namespace-ul System.Windows.Automation. Majoritatea claselor de automatizare se regasesc in modulele (assembly) UIAutomationClient.dll si UIAutomationTypes.dll.

Pentru a putea controla o aplicatie ce prezinta o interfata grafica cu utilizatorul trebuie sa putem accesa elementele de control (butoane, casute de editare, combobox-uri, etc.) ale respectivei aplicatii. In acest sens, Microsoft a introdus o proprietate atasata (attached property), AutomationId, implementata in clasa AutomationProperties. Aceasta proprietate atasata poate fi setata direct in codul XAML astfel:

<Element AutomationProperties.AutomationId='identificator_element' ... >

In aplicatia tinta, adaugam atributul AutomationProperties.AutomationId tuturor elementelor vizuale pe care dorim sa le monitorizam. Astfel, aplicatia de test va putea identifica elementele respective.

Pentru exemplificare vom porni tot de la aplicatia prezentata in articolul "Prima mea aplicatie". Astfel, fisierul XAML modificat va arata ca mai jos.

<Window x:Class="SampleWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns
:viewModel="clr-namespace:SampleWpf"
        Title="Prima mea aplicatie" SizeToContent="WidthAndHeight" Background="SkyBlue"
        AutomationProperties.AutomationId="AppWnd" >

    <Window.DataContext>
        <viewModel:MainWindowViewModel/>
    </Window.DataContext>
    <StackPanel>
        <TextBlock Margin="4" Text
="{Binding UltimulMesaj}" />
        <ListBox Height="200" Width="300" Margin="4" ItemsSource="{Binding Mesaje}" AutomationProperties.AutomationId="Lst" />
        <StackPanel Orientation="Horizontal">
            <TextBox x:Name="TxtMesaj" Width="200" Margin="4"
 Text="{Binding Mesaj}" AutomationProperties.AutomationId="MsgEdit" />
            <Button Content="Adauga mesaj" Margin="4" AutomationProperties.AutomationId="Btn"
                         Command="{Binding AdaugaMesajCommand}"
                         CommandParameter="{Binding Text, ElementName=TxtMesaj}" />
        </StackPanel>
    </StackPanel>
</Window>

Intr-o aplicatie WPF, daca nu este setata explicit proprietatea atasata AutomationProperties.AutomationId, infrastructura WPF ii va aloca automat o valoare bazata pe proprietatea Name. Cu ajutorul aplicatiilor UISpy.exe sau Snoop putem inspecta valorile proprietatii atasate AutomationProperties.AutomationId.

Sa trecem acum la dezvoltarea aplicatiei de test. Atasam solutiei existente un nou proiect de tip consola (din solutie, meniul context Add => New Project => Console Application) pe care-l numim UiAutomation. Proiectului nou creat ii atasam doua noi referinte la cele doua module UIAutomationClient.dll si UIAutomationType.dll, astfel incat solutia si proiectul nou creat va arata ca mai jos.

Sa trecem la cod. In prima faza trebuie sa startam aplicatia tinta, care urmeaza a fi testata. In cazul de fata, aplicatia SampleWpf.exe. Acest lucru se realizeaza apeland metoda Start din clasa Process astfel:

if ((Process.Start(@"..\..\..\SampleWpf\bin\Debug\SampleWpf.exe") == null)
    Console.WriteLine("Nu a fost gasit procesul");

Elementul principal din framework-ul UI Automation il constituie clasa AutomationElement. Prin intermediul acestei clase obtinem controlul obiectelor grafice din aplicatia tinta. Motorul de automatizare vede fiecare element grafic, fie el o fereastra sau un control, ca pe o instanta a clasei AutomationElement.

Pentru motorul de automatizare, intreaga suprafata de lucru a sistemului este constituita dintr-o ierarhie de obiecte AutomationElement, pornind de la desktop, ferestre si pana la ultimul control grafic de pe suprafata unei aplicatii. Astfel, pentru a identifica un control grafic pe care dorim sa-l controlam, plecam de la desktop si coboram in ierarhie pe fereastra aplicatiei tinta si mai jos pana la obiectul tinta.

Transpunand cele afirmate mai sus in cod, vom crea o metoda, care ne furnizeaza obiectul AutomationElement al ferestrei principale a aplicatiei tinta. Metoda e prezentata mai jos.

private static AutomationElement GetWindowElement()
{
    var desktop = AutomationElement.RootElement;
    if (desktop == null)
        throw new Exception("Nu a fost gasit elementul Desktop");
    return desktop.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "AppWnd"));
}

Avand elementul fereastra putem obtine restul obiectelor grafice si efectua operatii de control asupra lor. Aplicatia noastra de test va introduce un mesaj in casuta de editare dupa care va activa butonul de adaugare de mesaj si va repeta aceasta operatie de trei ori. In final, va extrage din controlul lista al doilea mesaj introdus si-l va afisa la consola. Pentru a realiza cele de mai sus, vom introduce trei noi metode astfel: metoda EnterMessage insereaza un mesaj in casuta de editare, metoda ClickButton executa o accesare a butonului de adaugare de mesaje iar metoda GetSecondElementFromListbox extrage din controlul lista al doilea mesaj introdus si-l afiseaza la consola. Metodele sunt prezentate mai jos.

private static void EnterMessage(AutomationElement window, string message)
{
    // Obtine casuta de editare
    const string id = "MsgEdit";
    var textBox = window.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, id));
    if (textBox == null)
    {
        Console.WriteLine("Nu a fost gasit obiectul cu ID-ul '{0}'", id);
        return;
    }

    // Introduce textul in casuta de editare
    var pattern = (ValuePattern)textBox.GetCurrentPattern(ValuePattern.Pattern);
    pattern.SetValue(message);
    Thread.Sleep(1000);
}

private static void ClickButton(AutomationElement window)
{
    // Obtine butonul
    const string id = "Btn";
    var button = window.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, id));
    if (button == null)
    {
        Console.WriteLine("Nu a fost gasit obiectul cu ID-ul '{0}'", id);
        return;
    }

    // Apasa butonul
    var pattern = (InvokePattern)button.GetCurrentPattern(InvokePattern.Pattern);
    pattern.Invoke();
    Thread.Sleep(1000);
}

private static string GetSecondElementFromListbox(AutomationElement window)
{
    // Obtine elementul lista
    const string id = "Lst";
    var listbox = window.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, id));
    if (listbox == null)
    {
        Console.WriteLine("Nu a fost gasit obiectul cu ID-ul '{0}'", id);
        return string.Empty;
    }

    // Returneaza al doilea element din lista
    var pattern = (ItemContainerPattern)listbox.GetCurrentPattern(ItemContainerPattern.Pattern);
    var item1 = pattern.FindItemByProperty(null, AutomationElement.ControlTypeProperty, ControlType.ListItem);
    var item2 = pattern.FindItemByProperty(item1, AutomationElement.ControlTypeProperty, ControlType.ListItem);
    return item2.Current.Name;
}

In metodele EnterMessage si ClickButton am introdus o intarziere de 1 secunda ( Thread.Sleep(1000) ) pentru a putea vizualiza efectele realizate de cele doua metode.

Urmeaza acum sa apelam metodele tocmai prezentate dupa cum urmeaza:

static void Main(string[] args)
{
    try
    {
        if (Process.Start(@"..\..\..\SampleWpf\bin\Debug\SampleWpf.exe") == null)
        {
            Console.WriteLine("Nu a fost gasit procesul");
            return;
        }

        var window = GetWindowElement();
        if (window == null)
        {
            Console.WriteLine("Nu a fost gasita fereastra aplicatiei");
            return;
        }

        EnterMessage(window, "Mesaj de test 1");
        ClickButton(window);
        EnterMessage(window, "Mesaj de test 2");
        ClickButton(window);
        EnterMessage(window, "Mesaj de test 3");
        ClickButton(window);
        Console.WriteLine("Al doilea element este: {0}", GetSecondElementFromListbox(window));
    }
    catch (Exception e)
    {
        Console.WriteLine("ERROR: {0}", e.InnerException != null ? e.Message + ": " + e.InnerException.Message : e.Message);
    }

    Console.WriteLine("Apasa o tasta pentru a termina aplicatia");
    Console.ReadKey();
}

Executand codul de mai sus, vom observa cum se starteaza aplicatia SampleWpf.exe dupa care este inserat un mesaj in casuta de editare, urmand accesarea automata a butonului de adaugare de mesaje si aparitia mesajului in controlul lista, dupa care casuta de editare este golita. Acestea se repeta de trei ori, in final in fereastra consola a aplicatiei de test afisandu-se cel de-al doilea mesaj introdus in lista.

Cu ajutorul tehnicii descrise se pot dezvolta adevarati robotii de test, care sa ne testeze aplicatiile. Se pot dezvolta comenzi care pot si citite din fisiere text si executate in bucla, astfel testandu-se comportamentul aplicatiei la rularea pe termen lung.

Cod sursa

Niciun comentariu:

Trimiteți un comentariu