Saturday, February 26, 2011

Visual Studio CodedUI Tests Search Criteria

CodedUI Test allows developers/testers to perform UI automated Tests over Windows desktop applications.

Theses kind of features are well documented over the web and several blogs.

In this post I’ll show how to modify generated CodedUI tests to overwrite UI elements search criteria.

This kind of test modifications are very frequent in some type of applications.

Some background..

CodedUI technology works over MSAA API (Microsoft Active Accesibility) and in order to automate user interaction with Windows, buttons, tabs, etc. (any interactive UI element), it needs first to find it and then perform one or more actions over it (this is Windows implementation for Assistive Technologies).

The initial finding or search process is a necessary step. The same way we search an element by seeing the screen, MSAA allows automatized applications to find elements in different ways.

Searching an UI element involves matching specific attributes against expected values. When all attributes or properties meet this “search criteria” a control is found and referenced from CodedUI test for its later used.

Sometimes, finding an UI element using an automated algorithm could be a non trivial task, since its properties or attributes change dynamically during the application lifecycle or between different executions (name, position, text, etc.).

Creating a CodedUI tests is done through a Visual Studio Wizard. It allows us to record our actions and then generating the proper source code (in VB.NET, C#, etc.). The generated source code is implemented using partial classes, thus allowing us to add custom behavior without modifying the auto generated file.

Example, finding a WPF button:

this.mUIInnerButton1Button.SearchProperties[WpfButton.PropertyNames.Name] = "Inner Button 1";

The SearchProperties collection contains an standard set of attributes used for searching a certain kind of control (in this case, a WPF Button).

We can always change and pass the values we decide to this property bag or collection (overwriting the generated default behavior). However, finding a control sometimes require us to perform more elaborated procedures.

Overwriting Control Definition and Search Criteria..

This means, modifying the generated code to change default search criteria logic.

We do not modify .Designer.cs file. We add our new search criteria in the .cs file associated with the UIMap visual studio element:

image

Let´s work over the next sample. Suppose we have defined a WPF button with dynamic text (current DateTime for instance, only for demo purposes). The following XAML is the static content of our main window application.

Code Snippet
<Grid>
    <TabControl>
        <TabItem Header="Tab 1">
            <Button>
                <ContentControl>
                    <StackPanel x:Name="ButtonStackPanel">
                        <TextBox>Text 1</TextBox>
                        <TextBox>Text 2</TextBox>
                        <Button Click="Button1_Click">Inner Button 1</Button>
                        <Button Click="Button2_Click">Inner Button 2</Button>
                        <ComboBox>
                            <ComboBoxItem Content="Item 1" IsSelected="True" />
                            <ComboBoxItem Content="Item 2" />
                            <ComboBoxItem Content="Item 3" />
                        </ComboBox>
                    </StackPanel>
                </ContentControl>
            </Button>
        </TabItem>
        <TabItem Header="Tab 2" />
        <TabItem Header="Tab 3" />
    </TabControl>
</Grid>

Then, by code we create a third button with dynamic text on it:

Code Snippet
public MainWindow()
{
    InitializeComponent();
    this.Loaded += (s, e) =>
    {
        var dynamicButton =
            new Button()
                {
                    Content = "Dynamic Button: " + DateTime.Now.ToString(),
                };
        dynamicButton.Click += (sender, args) => MessageBox.Show("Dynamic Button Click");
        this.ButtonStackPanel.Children.Add(dynamicButton);
    };
}

If your record actions using CodedUI wizard you will get something similar to the following search criteria:

this.mUIDynamicButton2602201Button = new WpfButton(this);
#region Search Criteria
this.mUIDynamicButton2602201Button.SearchProperties[WpfButton.PropertyNames.Name] = "Dynamic Button: 26/02/2011 06:22:29 p.m.";
this.mUIDynamicButton2602201Button.WindowTitles.Add("MainWindow");
#endregion

Try running the test and you’ll end up with an error on the button click (note the name attribute contains the current DateTime, thus every run will generate a different button name). This was expected of course.

So, lets modify the generated UI test code to find the control. In the UIMap.cs (non designer file), add the following code:

Code Snippet
public partial class UIMap
{
    public UIMap()
    {
        this.UIMainWindowWindow.UIItemTabList.UITab1TabPage.Find();
        var tabItemStackPanel = this.UIMainWindowWindow.UIItemTabList.UITab1TabPage.GetChildren()[0];
        var dynamicButton =
            tabItemStackPanel.GetChildren().FirstOrDefault(c => c.Name.Contains("Dynamic"));
        this.UIMainWindowWindow.UIItemTabList.UITab1TabPage.UIDynamicButton2602201Button.CopyFrom(dynamicButton);
    }
}
  1. The custom code is added in the constructor of the UIMap, but you can easily create an Init() method (recommended) and then call it before invoking the test.
  2. We first need to find the control by searching the children of the parent StackPanel (note in the XAML that the StackPanel is the first and only child of the TabItem).
  3. Once we’ve found the StackPanel we enumerate its children, by filtering the results by control name (we are looking for something that begins with the word “Dynamic”).
  4. Finally, we need to clear the recorded search properties values and add the correct name filter value. You can do this by copying the button’s definition (with the CopyFrom() method). This copies all properties from the just found button to the one used by the test.

Final notes..

  • This is a simple example to show how you can manually search and iterate through the entire UI structure of an application to find elements. You can do this by position, name, display text, control type, etc.
  • You can implement the same behavior to find any control in the hierarchy.
  • This API could be used to automate applications for any purpose (not only for tests).

Tuesday, February 22, 2011

Webcast MSDN: Mejores Prácticas en la Gestión de Requisitos con Visual Studio 2010

I am going to participate in this MSDN WebCast (spanish only) with Maximiliano Déboli from Lagash today.

See you there..

Saturday, February 19, 2011

Overwriting TFS Web Services

In this blog I will share a technique I used to intercept TFS Web Services calls.

This technique is a very invasive one and requires you to overwrite default TFS Web Services behavior. I only recommend taking such an approach when other means of TFS extensibility fail to provide the same functionality (this is not a supported TFS extensibility point).

For instance, intercepting and aborting a Work Item change operation could be implemented using this approach (consider TFS Subscribers functionality before taking this approach, check Martin’s post about subscribers).

So let’s get started.

The technique consists in versioning TFS Web Services .asmx service classes. If you look into TFS’s ASMX services you will notice that versioning is supported by creating a class hierarchy between different product versions.

For instance, let’s take the Work Item management service .asmx. Check the following .asmx file located at:

%Program Files%\Microsoft Team Foundation Server 2010\Application Tier\Web Services\_tfs_resources\WorkItemTracking\v3.0\ClientService.asmx

The .asmx references the class Microsoft.TeamFoundation.WorkItemTracking.Server.ClientService3:

Code Snippet
  1. <%-- Copyright (c) Microsoft Corporation.  All rights reserved. --%>
  2. <%@ webservice language="C#" Class="Microsoft.TeamFoundation.WorkItemTracking.Server.ClientService3" %>

The inheritance hierarchy for this service class follows:

image

Note the naming convention used for service versioning (ClientService3, ClientService2, ClientService).

We will need to overwrite the latest service version provided by the product (in this case ClientService3 for TFS 2010).

The following example intercepts and analyzes WorkItem fields. Suppose we need to validate state changes with more advanced logic other than the provided validations/constraints of the process template.

Important: Backup the original .asmx file and create one of your own.

  1. Create a Visual Studio Web App Project and include a new ASMX Web Service in the project
  2. Add the following references to the project (check the folder %Program Files%\Microsoft Team Foundation Server 2010\Application Tier\Web Services\bin\):
    • Microsoft.TeamFoundation.Framework.Server.dll
    • Microsoft.TeamFoundation.Server.dll Microsoft.TeamFoundation.Server.dll
    • Microsoft.TeamFoundation.WorkItemTracking.Client.QueryLanguage.dll
    • Microsoft.TeamFoundation.WorkItemTracking.Server.DataAccessLayer.dll
    • Microsoft.TeamFoundation.WorkItemTracking.Server.DataServices.dll
  3. Replace the default service implementation with the something similar to the following code:
Code Snippet
  1. /// <summary>
  2.     /// Inherit from ClientService3 to overwrite default Implementation
  3.     /// </summary>
  4.     [WebService(Namespace = "http://schemas.microsoft.com/TeamFoundation/2005/06/WorkItemTracking/ClientServices/03", Description = "Custom Team Foundation WorkItemTracking ClientService Web Service")]
  5.     public class CustomTfsClientService : ClientService3
  6.     {
  7.         [WebMethod, SoapHeader("requestHeader", Direction = SoapHeaderDirection.In)]
  8.         public override bool BulkUpdate(
  9.             XmlElement package,
  10.             out XmlElement result,
  11.             MetadataTableHaveEntry[] metadataHave,
  12.             out string dbStamp,
  13.             out Payload metadata)
  14.         {
  15.             var xe = XElement.Parse(package.OuterXml);
  16.             
  17.             // We only intercept WorkItems Updates (we can easily extend this sample to capture any operation).
  18.             var wit = xe.Element("UpdateWorkItem");
  19.             if (wit != null)
  20.             {
  21.                 if (wit.Attribute("WorkItemID") != null)
  22.                 {
  23.                     int witId = (int)wit.Attribute("WorkItemID");
  24.                     // With this Id. I can query TFS for more detailed information, using TFS Client API (assuming the WIT already exists).
  25.                     var stateChanged =
  26.                         wit.Element("Columns").Elements("Column").FirstOrDefault(c => (string)c.Attribute("Column") == "System.State");
  27.  
  28.                     if (stateChanged != null)
  29.                     {
  30.                         var newStateName = stateChanged.Element("Value").Value;
  31.                         if (newStateName == "Resolved")
  32.                         {
  33.                             throw new Exception("Cannot change state to Resolved!");
  34.                         }
  35.                     }
  36.                 }
  37.             }
  38.  
  39.             // Finally, we call base method implementation
  40.             return base.BulkUpdate(package, out result, metadataHave, out dbStamp, out metadata);
  41.         }
  42.     }

4. Build your solution and overwrite the original .asmx with the new implementation referencing our new service version (don’t forget to backup it up first).

5. Copy your project’s .dll into the following path:

%Program Files%\Microsoft Team Foundation Server 2010\Application Tier\Web Services\bin

6. Try saving a WorkItem into the Resolved state.

Enjoy!