微軟提供的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的測試框架。