微软提供的UI Automation框架给开发windows平台的自动化测试带来了很大的便利,这里就总结一下相关的代码。
首先,直接使用UI Automation框架,完成一个NotePad的about窗口中的 “OK” button的点击:
1 AutomationElement root = AutomationElement.RootElement; 2 AutomationElement about_notepad_windows = root.FindFirst( 3 TreeScope.Descendants, 4 new AndCondition( 5 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window), 6 new PropertyCondition(AutomationElement.NameProperty, "About Notepad"))); 7 if (about_notepad_windows == null) 8 { 9 Console.WriteLine("About Notepad window doesn't exist!!"); 10 return; 11 } 12 13 AutomationElement ok_button = about_notepad_windows.FindFirst( 14 TreeScope.Children, 15 new AndCondition( 16 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button), 17 new PropertyCondition(AutomationElement.NameProperty, "OK"))); 18 Object invokePatternObject; 19 if (ok_button.TryGetCurrentPattern(InvokePattern.Pattern, out invokePatternObject)) 20 { 21 (invokePatternObject as InvokePattern).Invoke(); 22 }
好吧,上面是面向过程的代码,不利于复用,那么让我们来将其抽象成类,
首先定义一个基类UIAControl,它包含有搜索的根节点searchRoot,搜索范围searchScope和条件searchConditions,使用这三个对象来搜索一个AutomationElement对象并将其赋给innerElement,由于默认使用的是AndCondition来关联所有传入的condition对象,所以将CombineCondition方法设为虚方法,以便如果有子类想要使用其他关联条件处理condition的时候可以覆盖:
1 public abstract class UIAControl 2 { 3 private UIAControl searchRoot; 4 private TreeScope searchScope; 5 private List<Condition> searchConditions; 6 7 protected void AddSearchCondition(Condition condition) 8 { 9 this.searchConditions.Add(condition); 10 } 11 12 public UIAControl() 13 { 14 searchConditions = new List<Condition>(); 15 searchScope = TreeScope.Descendants; 16 } 17 18 public UIAControl(UIAControl searchRoot) 19 : this() 20 { 21 this.searchRoot = searchRoot; 22 } 23 24 public UIAControl(IntPtr hwnd) 25 : this() 26 { 27 searchConditions.Add(PropertyConditionFactory.GetHandleCondition(hwnd)); 28 } 29 30 public AutomationElement.AutomationElementInformation? ControlInformation 31 { 32 get 33 { 34 if (Exists()) 35 { 36 return InnerElement.Current; 37 } 38 return null; 39 } 40 } 41 42 private AutomationElement innerElement; 43 public AutomationElement InnerElement 44 { 45 get 46 { 47 if (innerElement == null) 48 { 49 innerElement = SearchElement(); 50 } 51 return innerElement; 52 } 53 } 54 55 protected virtual AutomationElement SearchElement() 56 { 57 AutomationElement ele = null; 58 if (searchRoot == null) 59 { 60 ele = AutomationElement.RootElement; 61 } 62 else 63 { 64 ele = searchRoot.InnerElement; 65 } 66 67 if (ele == null || 0 == searchConditions.Count) 68 { 69 return ele; 70 } 71 else 72 { 73 Condition conditions = CombineAllConditions(); 74 try 75 { 76 return ele.FindFirst(searchScope, conditions); 77 } 78 catch(Exception ex) 79 { 80 Console.WriteLine("Getting exception when searching element: " + ex.Message); 81 return null; 82 } 83 } 84 } 85 86 //Can override this method to return other type conditions, default will return AndCondition 87 protected virtual Condition CombineAllConditions() 88 { 89 if (searchConditions.Count > 1) 90 { 91 return new AndCondition(searchConditions.ToArray()); 92 } 93 else if (searchConditions.Count == 1) 94 { 95 return searchConditions.First(); 96 } 97 else 98 { 99 return null; 100 } 101 102 } 103 104 public virtual bool Exists() 105 { 106 //Before checking existence, set innerElement to null to trigger fresh search. 107 return null != SearchElement(); 108 } 109 110 public bool WaitTillExist(int timeout, int interval = 2000) 111 { 112 Stopwatch stopwatch = new Stopwatch(); 113 stopwatch.Start(); 114 while (stopwatch.ElapsedMilliseconds < timeout) 115 { 116 if (this.Exists()) 117 { 118 return true; 119 } 120 121 Thread.Sleep(interval); 122 } 123 124 return false; 125 } 126 127 protected bool CallPattern<T>(T pattern, Action<T> action) where T : BasePattern 128 { 129 if (pattern != null) 130 { 131 try 132 { 133 action(pattern); 134 return true; 135 } 136 catch (Exception ex) 137 { 138 Console.WriteLine(ex.Message); 139 } 140 } 141 return false; 142 } 143 144 protected T GetPattern<T>() where T : BasePattern 145 { 146 var ele = InnerElement; 147 if (ele != null) 148 { 149 try 150 { 151 var patternIdentifier = (AutomationPattern)(typeof(T).GetField("Pattern").GetValue(null)); 152 return ele.GetCurrentPattern(patternIdentifier) as T; 153 } 154 catch (Exception ex) 155 { 156 Console.WriteLine(ex.Message); 157 } 158 } 159 return null; 160 } 161 162 public bool Invoke() 163 { 164 return CallPattern(GetPattern<InvokePattern>(), (pattern) => pattern.Invoke()); 165 } 166 }
之后,就可以定义其他控件类型了,下面定义了一个button的对象,只有一个click方法:
1 public class UIAButton : UIAControl 2 { 3 public UIAButton(UIAControl root, string name) 4 : base(root) 5 { 6 AddSearchCondition(PropertyConditionFactory.GetNameCondition(name)); 7 AddSearchCondition(PropertyConditionFactory.GetControlTypeCondition(ControlType.Button)); 8 } 9 10 public void Click() 11 { 12 this.Invoke(); 13 } 14 }
再定义一个window对象:
1 public class UIAWindow : UIAControl 2 { 3 public UIAWindow(string name) 4 { 5 AddSearchCondition(PropertyConditionFactory.GetNameCondition(name)); 6 AddSearchCondition(PropertyConditionFactory.GetControlTypeCondition(ControlType.Window)); 7 } 8 9 public bool Close() 10 { 11 return CallPattern(GetPattern<WindowPattern>(), (pattern) => pattern.Close()); 12 } 13 14 public bool Maximize() 15 { 16 return CallPattern(GetPattern<WindowPattern>(), (pattern) => pattern.SetWindowVisualState(WindowVisualState.Maximized)); 17 } 18 19 public bool Minimize() 20 { 21 return CallPattern(GetPattern<WindowPattern>(), (pattern) => pattern.SetWindowVisualState(WindowVisualState.Minimized)); 22 } 23 24 public bool Resize(int width, int height) 25 { 26 return CallPattern(GetPattern<TransformPattern>(), (pattern) => pattern.Resize(width, height)); 27 } 28 }
最后,使用上面两个控件类型来完成Ok按钮的点击:
1 UIAWindow windows = new UIAWindow("About Notepad"); 2 UIAButton ok_button = new UIAButton(windows, "OK"); 3 if (windows.WaitTillExist(3000)) 4 { 5 ok_button.Click(); 6 }
当然,如果是稍微上点规模的项目,就需要利用这些控件抽象出一个对应产品功能的produc类了。
如果不想从头写起的话,可以看看开源工具White, 一个优秀的基于UI Automation的测试框架。