Enable Coded UI Testing of Your Custom Controls
Your control can be more easily tested if you implement support for the coded UI testing framework. You can add increasing levels of support incrementally. You can start by supporting record and playback and property validation. You can build on that to allow the coded UI test builder to recognize your control’s custom properties, and provide custom classes to access those properties from generated code. You can also help the coded UI test builder capture actions in a way that is closer to the intent of the action being recorded.
In this topic:
Support record and playback and property validation by implementing accessibility
Add custom property validation by implementing a property provider
Support code generation by implementing a class to access custom properties
Support intent-aware actions by implementing an action filter
Support record and playback and property validation by implementing accessibility
The coded UI test builder captures information about the controls that it encounters during a recording and then generates code to replay that session. If your control doesn't support accessibility, then the coded UI test builder will capture actions, like mouse clicks, using screen coordinates. When you play the test back, the generated code will issue those mouse clicks in the same screen coordinates. If your control appears in a different place on the screen when you play the test back—for example, if the test is played back on different screen configurations, in different environments, or after the UI layout has changed—the generated code will fail to perform the recorded actions on your control. .
If you implement accessibility, though, the coded UI test builder will use that to capture information about your control when it records a test and generates code. Then, when you run the test, the generated code will replay those events against your control, even if it's somewhere else in the user interface. Test authors will also be able to create asserts using the basic properties of your control.
To support record and playback and property validation by implementing accessibility
Implement accessibility for your control as outlined in the following procedure, and explained in detail in System.Windows.Forms.AccessibleObject.
Implement a class that derives from Control.ControlAccessibleObject, and override the AccessibilityObject property to return an object of your class.
public partial class ChartControl : UserControl { // Overridden to return the custom AccessibleObject for the control. protected override AccessibleObject CreateAccessibilityInstance() { return new ChartControlAccessibleObject(this); } // Inner class ChartControlAccessibleObject represents accessible information // associated with the ChartControl and is used when recording tests. public class ChartControlAccessibleObject : ControlAccessibleObject { ChartControl myControl; public ChartControlAccessibleObject(ChartControl ctrl) : base(ctrl) { myControl = ctrl; } } }
Override the accessible object’s Role, State, GetChild and GetChildCount properties and methods.
Implement another accessibility object for the child control and override the child control’s AccessibilityObject property to return that accessibility object.
Override the Bounds, Name, Parent, Role, State, Navigate, and Select properties and methods for the child control’s accessibility object.
Note
This topic starts with the accessibility sample in AccessibleObject in this procedure, and then builds on that in the remaining procedures. If you want to create a working version of the accessibility sample, create a console application and then replace the code in Program.cs with the sample code. You’ll need to add references to Accessibility, System.Drawing, and System.Windows.Forms. You should change the Embed Interop Types for Accessibility to False to eliminate a build warning. You can change the project’s output type from Console Application to Windows Application so that a console window doesn’t appear when you run the application.
Add custom property validation by implementing a property provider
After you’ve implemented basic support for record and playback and property validation, you can make your control’s custom properties available to coded UI tests by implementing a UITestPropertyProvider plug-in. For example, the following procedure creates a property provider that allows coded UI tests to access the State property of the chart control’s CurveLegend child controls.
To add custom property validation
Override the curve legend accessible object’s AccessibleObject.Description property to pass rich property values in the description string, separated from the main description (and each other if you're implementing multiple properties) by semicolons (;).
public class CurveLegendAccessibleObject : AccessibleObject { // Add the state property value to the description. public override string Description { get { // Add “;” and the state value to the end // of the curve legend’s description. return "CurveLegend; " + State.ToString(); } } }
Create a UI test extension package for your control by creating a class library project and adding references to Accessibility, Microsoft.VisualStudio.TestTools.UITesting, Microsoft.VisualStudio.TestTools.UITest.Common, and Microsoft.VisualStudio.TestTools.Extension. Change the Embed Interop Types for Accessibility to False.
Add a property provider class that’s derived from UITestPropertyProvider.
using System; using System.Collections.Generic; using Accessibility; using Microsoft.VisualStudio.TestTools.UITesting; using Microsoft.VisualStudio.TestTools.UITest.Extension; using Microsoft.VisualStudio.TestTools.UITesting.WinControls; using Microsoft.VisualStudio.TestTools.UITest.Common; namespace ChartControlExtensionPackage { public class ChartControlPropertyProvider : UITestPropertyProvider { } }
Implement the property provider by placing property names and property descriptors in a Dictionary<TKey, TValue>.
// Define a map of property descriptors for CurveLegend. private static Dictionary<string, UITestPropertyDescriptor> curveLegendPropertiesMap = null; private static Dictionary<string, UITestPropertyDescriptor> CurveLegendPropertiesMap { get { if (curveLegendPropertiesMap == null) { UITestPropertyAttributes read = UITestPropertyAttributes.Readable | UITestPropertyAttributes.DoNotGenerateProperties; curveLegendPropertiesMap = new Dictionary<string, UITestPropertyDescriptor> (StringComparer.OrdinalIgnoreCase); curveLegendPropertiesMap.Add("State", new UITestPropertyDescriptor(typeof(string), read)); } return curveLegendPropertiesMap; } } // Return the property descriptor. public override UITestPropertyDescriptor GetPropertyDescriptor(UITestControl uiTestControl, string propertyName) { return CurveLegendPropertiesMap[propertyName]; } // Return the property names. public override ICollection<string> GetPropertyNames(UITestControl uiTestControl) { if (uiTestControl.ControlType.NameEquals("Chart") || uiTestControl.ControlType.NameEquals("Text")) { // The keys of the property map are the collection of property names. return CurveLegendPropertiesMap.Keys; } // This is not my control. throw new NotSupportedException(); } // Get the property value by parsing the accessible description. public override object GetPropertyValue(UITestControl uiTestControl, string propertyName) { if (String.Equals(propertyName, "State", StringComparison.OrdinalIgnoreCase)) { object[] native = uiTestControl.NativeElement as object[]; IAccessible acc = native[0] as IAccessible; string[] descriptionTokens = acc.accDescription.Split(new char[] { ';' }); return descriptionTokens[1]; } // This is not my control. throw new NotSupportedException(); }
Override UITestPropertyProvider.GetControlSupportLevel to indicate that your assembly provides control-specific support for your control and its children.
public override int GetControlSupportLevel(UITestControl uiTestControl) { // For MSAA, check the control type. if (string.Equals(uiTestControl.TechnologyName, "MSAA", StringComparison.OrdinalIgnoreCase) && (uiTestControl.ControlType == "Chart"||uiTestControl.ControlType == "Text")) { return (int)ControlSupport.ControlSpecificSupport; } // This is not my control, so return NoSupport. return (int)ControlSupport.NoSupport; }
Override the remaining abstract methods of UITestPropertyProvider.
public override string[] GetPredefinedSearchProperties(Type specializedClass) { throw new NotImplementedException(); } public override Type GetSpecializedClass(UITestControl uiTestControl) { throw new NotImplementedException(); } public override Type GetPropertyNamesClassType(UITestControl uiTestControl) { throw new NotImplementedException(); } public override void SetPropertyValue(UITestControl uiTestControl, string propertyName, object value) { throw new NotImplementedException(); } public override string GetPropertyForAction(UITestControl uiTestControl, UITestAction action) { throw new NotImplementedException(); } public override string[] GetPropertyForControlState(UITestControl uiTestControl, ControlStates uiState, out bool[] stateValues) { throw new NotImplementedException(); }
Add an extension package class that’s derived from UITestExtensionPackage.
using System; using Microsoft.VisualStudio.TestTools.UITesting; using Microsoft.VisualStudio.TestTools.UITest.Extension; using Microsoft.VisualStudio.TestTools.UITest.Common; namespace ChartControlExtensionPackage { internal class ChartControlExtensionPackage : UITestExtensionPackage { } }
Define the UITestExtensionPackage attribute for the assembly.
[assembly: Microsoft.VisualStudio.TestTools.UITest.Extension.UITestExtensionPackage( "ChartControlExtensionPackage", typeof(ChartControlExtensionPackage.ChartControlExtensionPackage))] namespace ChartControlExtensionPackage { …
In the extension package class, override UITestExtensionPackage.GetService to return the property provider class when a property provider is requested.
internal class ChartControlExtensionPackage : UITestExtensionPackage { public override object GetService(Type serviceType) { if (serviceType == typeof(UITestPropertyProvider)) { if (propertyProvider == null) { propertyProvider = new ChartControlPropertyProvider(); } return propertyProvider; } return null; } private UITestPropertyProvider propertyProvider = null; }
Override the remaining abstract methods and properties of UITestExtensionPackage.
public override void Dispose() { } public override string PackageDescription { get { return "Supports coded UI testing of ChartControl"; } } public override string PackageName { get { return "ChartControl Test Extension"; } } public override string PackageVendor { get { return "Microsoft (sample)"; } } public override Version PackageVersion { get { return new Version(1, 0); } } public override Version VSVersion { get { return new Version(10, 0); } }
Build your binaries and copy them to %ProgramFiles%\Common\Microsoft Shared\VSTT\10.0\UITestExtensionPackages.
Note
This extension package will be applied to any control that is of type “Text”. If you’re testing multiple controls of the same type, you’ll need to test them separately and manage which extension packages are deployed when you record the tests.
Support code generation by implementing a class to access custom properties
When the coded UI test builder generates code from a session recording, it uses the UITestControl class to access your controls.
UITestControl uIAText = this.UIItemWindow.UIChartControlWindow.UIAText;
Assert.AreEqual(this.AssertMethod3ExpectedValues.UIATextState, uIAText.GetProperty("State").ToString());
If you’ve implemented a property provider to provide access to your control’s custom properties, you can add a specialized class that is used to access those properties so that the generated code is simplified.
ControlLegend uIAText = this.UIItemWindow.UIChartControlWindow.UIAText;
Assert.AreEqual(this.AssertMethod3ExpectedValues.UIATextState, uIAText.State);
To add a specialized class to access your control
Implement a class that’s derived from WinControl and add the control’s type to the search properties collection in the constructor.
public class CurveLegend:WinControl { public CurveLegend(UITestControl c) : base(c) { // The curve legend control is a “text” type of control. SearchProperties.Add( UITestControl.PropertyNames.ControlType, "Text"); } }
Implement your control’s custom properties as properties of the class.
public virtual string State { get { return (string)GetProperty("State"); } }
Override your property provider’s UITestPropertyProvider.GetSpecializedClass method to return the type of the new class for the curve legend child controls.
public override Type GetSpecializedClass(UITestControl uiTestControl) { if (uiTestControl.ControlType.NameEquals("Text")) { // This is a text type of control. For my control, // that means it’s a curve legend control. return typeof(CurveLegend); } // This is not a curve legend control. return null; }
Override your property provider’s UITestPropertyProvider.GetPropertyNamesClassType method to return the type of the new class’s PropertyNames method.
public override Type GetPropertyNamesClassType(UITestControl uiTestControl) { if (uiTestControl.ControlType.NameEquals("Text")) { // This is a text type of control. For my control, // that means it’s a curve legend control. return typeof(CurveLegend.PropertyNames); } // This is not a curve legend control. return null; }
Create intent-aware actions by implementing an action filter
When Visual Studio records a test, it captures each mouse and keyboard event. However, in some cases, the intent of the action can be lost in the series of mouse and keyboard events. For example, if your control supports autocomplete, the same set of mouse and keyboard events could result in a different value when the test is played back in a different environment. You can add an action filter plug-in that replaces the series of keyboard and mouse events with a single action. This way, you can replace the series of mouse and keyboard events resulting in the selection of a value with a single action that sets the value. Doing that protects coded UI tests from the differences in autocomplete from one environment to another.
To create intent-aware actions
Implement an action filter class that’s derived from UITestActionFilter, overriding the properties ApplyTimeout, Category, Enabled, FilterType, Group and Name.
internal class MyActionFilter : UITestActionFilter { // If the user actions we are aggregating exceed the time allowed, // this filter is not applied. (The timeout is configured when the // test is run.) public override bool ApplyTimeout { get { return true; } } // Gets the category of this filter. Categories of filters // are applied in priority order. public override UITestActionFilterCategory Category { get { return UITestActionFilterCategory.PostSimpleToCompoundActionConversion; } } public override bool Enabled { get { return true; } } public override UITestActionFilterType FilterType { // This action filter operates on a single action. get { return UITestActionFilterType.Unary; } } // Gets the name of the group to which this filter belongs. // A group can be enabled/disabled using configuration file. public override string Group { get { return "ChartControlActionFilters"; } } // Gets the name of this filter. public override string Name { get { return "Convert Double-Click to Single-Click"; } }
Override UITestActionFilter.ProcessRule(IUITestActionStack). The example here replaces a double-click action with a single click action.
public override bool ProcessRule(IUITestActionStack actionStack) { if (actionStack.Count > 0) { MouseAction lastAction = actionStack.Peek() as MouseAction; if (lastAction != null) { if (lastAction.UIElement.ControlTypeName.Equals( ControlType.Text.ToString(), StringComparison.OrdinalIgnoreCase)) { if(lastAction.ActionType == MouseActionType.DoubleClick) { // Convert to single click. lastAction.ActionType = MouseActionType.Click; } } } } // Do not stop aggregation. return false; }
Add the action filter to the UITestExtensionPackage.GetService(Type) method of your extension package.
public override object GetService(Type serviceType) { if (serviceType == typeof(UITestPropertyProvider)) { if (propertyProvider == null) { propertyProvider = new PropertyProvider(); } return propertyProvider; } else if (serviceType == typeof(UITestActionFilter)) { if (actionFilter == null) { actionFilter = new RadGridViewActionFilter(); } return actionFilter; } return null; }
Build your binaries and copy them to %ProgramFiles%\Common Files\Microsoft Shared\VSTT\10.0\UITestExtensionPackages.
Note
The action filter does not depend on the accessibility implementation or on the property provider.
Debug your property provider or action filter
Your property provider and action filter are implemented in an extension package that's loaded and run by the coded UI test builder in a process separate from your application.
To debug your property provider or action filter
Build the debug version of your extension package, and copy the .dll and .pdb files to %ProgramFiles%\Common Files\Microsoft Shared\VSTT\10.0\UITestExtensionPackages.
Run your application (not in the debugger).
Run the coded UI test builder.
codedUITestBuilder.exe /standalone
Attach the debugger to the codedUITestBuilder process.
Set breakpoints in your code.
In the coded UI test builder, create asserts to exercise your property provider, and record actions to exercise your action filters.