《軟件測試自動化之道》讀書筆記 之 基於Windows的UI測試
2014-09-25
測試自動化程序的任務
待測程序
測試程序
啟動待測程序
獲得待測程序主窗體的句柄
獲得有名字控件的句柄
獲得無名字控件的句柄
發送字符給控件
鼠標單擊一個控件
處理消息對話框
處理菜單
檢查應用程序狀態
示例程序
參考
本章主要講述如何使用底層的Windows自動化技術通過用戶界面來測試應用程序。這些技術涉及Win32 API的調用(比如FindWindow()函數)以及想待測程序發送Windows消息(比如WM_LBUTTONUP)。
測試自動化程序的任務
基於Windows的UI測試,逍遙完成的工作主要有以下三類:
- 找到目標窗體/控件的句柄
- 操作這個窗體/控件
- 檢測這個窗體/控件
待測程序
待測程序是一個用來做顏色混合的應用程序,關鍵代碼如下:

1 using System; 2 using System.Windows.Forms; 3 4 namespace WinApp 5 { 6 public partial class Form1 : Form 7 { 8 public Form1() 9 { 10 InitializeComponent(); 11 } 12 13 private void button1_Click(object sender, EventArgs e) 14 { 15 string tb = textBox1.Text; 16 string cb = comboBox1.Text; 17 18 if (tb == "<enter color>" || cb == "<pick>") 19 MessageBox.Show("You need 2 colors", "Error"); 20 else 21 { 22 if (tb == cb) 23 listBox1.Items.Add("Result is " + tb); 24 else if (tb == "red" && cb == "blue" || tb == "blue" && cb == "red") 25 listBox1.Items.Add("Result is purple"); 26 else 27 listBox1.Items.Add("Result is black"); 28 } 29 } 30 31 private void exitToolStripMenuItem1_Click(object sender, EventArgs e) 32 { 33 this.Close(); 34 } 35 } 36 }
圖1 AUT
測試程序
啟動待測程序

1 using System.Diagnostics; 2 3 static void Main(string[] args) 4 { 5 //... 6 string path = "..\\..\\..\\WinApp\\bin\\Debug\\WinApp.exe"; 7 Process p = Process.Start(path); 8 //... 9 }
獲得待測程序主窗體的句柄
要獲得待測程序主窗體的句柄,可使用FindWindow() Win32 API函數來解決這個問題。
FindWindow()的函數簽名用C++來描述是:
HWND FindWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName);
Win32 API函數FindWindow()是Windows操作系統的一部分,是由傳統的C++程序而不是用受控代碼(managed code)編寫的。它返回HWND(Handle of Window), 是一個窗體的句柄
C#要使用Win32 API函數FindWindow(),可通過.Net平台的invoke(P/Invoke)機制,P/Invoke相關特性位於System.Runtime.InteropServices命名空間內。
[DllImport("user32.dll", EntryPoint = "FindWindow", CharSet = CharSet.Auto)] static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
DllImport是用來將特性化方法由非托管動態鏈接庫 (DLL) 作為靜態入口點公開。
說明[1]:
1、DllImport只能放置在方法聲明上。
2、DllImport具有單個定位參數:指定包含被導入方法的 dll 名稱的 dllName 參數。
3、DllImport具有五個命名參數:
a、CallingConvention 參數指示入口點的調用約定。如果未指定 CallingConvention,則使用默認值 CallingConvention.Winapi。
b、CharSet 參數指示用在入口點中的字符集。如果未指定 CharSet,則使用默認值 CharSet.Auto。
c、EntryPoint 參數給出 dll 中入口點的名稱。如果未指定 EntryPoint,則使用方法本身的名稱。
d、ExactSpelling 參數指示 EntryPoint 是否必須與指示的入口點的拼寫完全匹配。如果未指定 ExactSpelling,則使用默認值 false。
e、PreserveSig 參數指示方法的簽名應當被保留還是被轉換。當簽名被轉換時,它被轉換為一個具有 HRESULT 返回值和該返回值的一個名為 retval 的附加輸出參數的簽名。如果未指定 PreserveSig,則使用默認值 true。
f、SetLastError 參數指示方法是否保留 Win32"上一錯誤"。如果未指定 SetLastError,則使用默認值 false。
4、它是一次性屬性類。
5、此外,用 DllImport 屬性修飾的方法必須具有 extern 修飾符。
注意:.NET平台大大簡化了數據類型的模型,從而提高了程序開發的效率。要使用P/Invoke機制,必須為Win32數據類型找到相應的 C#數據類型。
此示例中,在.NET壞境中,窗體句柄的類型是System.IntPtr,它是一個平台相關的類型,用來代表指針(內存地址)或者句柄。它對應於Win32的數據類型HWND。String對應於LPCTSTR。
參數:
- lpClassName:是窗體類名稱,更OOP中類無任何關系。是由系統生成的一個字符串,用來把相應的窗體注冊到操作系統。因為窗體/控件類的名稱並不具有唯一性,對查找一個窗體/控件並沒有太大幫助。因此,在本示例中傳給它null。
- lpWindowName:是窗體的名稱。也被叫做window title或者window caption。在windows form程序中,這個值通常稱為form name。
獲得待測程序主窗體的句柄示例代碼如下:

1 using System.Runtime.InteropServices; 2 3 static void Main(string[] args) 4 { 5 //... 6 IntPtr mwh = FindMainWindowHandle("Form1", 100, 25); 7 //... 8 } 9 10 [DllImport("user32.dll", EntryPoint = "FindWindow", CharSet = CharSet.Auto)] 11 static extern IntPtr FindWindow(string lpClassName, string lpWindowName); 12 13 static IntPtr FindMainWindowHandle(string caption, int delay, int maxTries) 14 { 15 return FindTopLevelWindow(caption, delay, maxTries); 16 } 17 18 static IntPtr FindTopLevelWindow(string caption, int delay, int maxTries) 19 { 20 IntPtr mwh = IntPtr.Zero; 21 bool formFound = false; 22 int attempts = 0; 23 24 do 25 { 26 //FindWindow 27 mwh = FindWindow(null, caption); 28 if (mwh == IntPtr.Zero) 29 { 30 Console.WriteLine("Form not yet found"); 31 Thread.Sleep(delay); 32 ++attempts; 33 } 34 else 35 { 36 Console.WriteLine("Form has been found"); 37 formFound = true; 38 } 39 } while (!formFound && attempts < maxTries); 40 41 if (mwh != IntPtr.Zero) 42 return mwh; 43 else 44 throw new Exception("Could not find Main Window"); 45 }
獲得有名字控件的句柄

1 static void Main(string[] args) 2 { 3 //... 4 IntPtr tb = FindWindowEx(mwh, IntPtr.Zero, null, "<enter color>"); 5 //... 6 } 7 8 [DllImport("user32.dll", EntryPoint = "FindWindowEx",CharSet = CharSet.Auto)] 9 static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
參數:
- hwndParent:控件目標的父窗體句柄
- hwndChildAfter:從哪個控件開始找,即從下一個控件開始找
- lpszClass:class name(如上參數lpClassName)
- lpszWindow:目標控件的window name/title/caption
圖2 Spy++捕獲控件button1
獲得無名字控件的句柄
如何獲得一個沒有名字的空間的句柄,可通過隱含索引來查找相應控件

1 static void Main(string[] args) 2 { 3 //... 4 IntPtr cb = FindWindowByIndex(mwh, 2); 5 //... 6 } 7 8 [DllImport("user32.dll", EntryPoint = "FindWindowEx",CharSet = CharSet.Auto)] 9 static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); 10 11 static IntPtr FindWindowByIndex(IntPtr hwndParent, int index) 12 { 13 if (index == 0) 14 return hwndParent; 15 else 16 { 17 int ct = 0; 18 IntPtr result = IntPtr.Zero; 19 do 20 { 21 //FindWindowEx 22 result = FindWindowEx(hwndParent, result, null, null); 23 if (result != IntPtr.Zero) 24 ++ct; 25 } while (ct < index && result != IntPtr.Zero); 26 27 return result; 28 } 29 }
注意:這里索引的順序是加入主窗體的順序。見如下代碼,主窗體的索引值為0,先后加入的控件button1的索引為1,comboBox1的索引為2,...

1 private void InitializeComponent() 2 { 3 //... 4 this.Controls.Add(this.button1); 5 this.Controls.Add(this.comboBox1); 6 this.Controls.Add(this.textBox1); 7 this.Controls.Add(this.menuStrip1); 8 this.Controls.Add(this.listBox1); 9 //... 10 }
你也可以通過工具“spy++”來查找先后。
發送字符給控件
SendMessage()的函數簽名用C++簽名如下:
LRESULT SendMessage(HWND HwND, UINT Msg, WPARAM wParam, LPARAM lParam);
- HwND:目標窗體/控件的句柄
- Msg:要發給該控件的Window消息
- wParam, lParam:它們的意思和數據類型取決於相應的Windows消息
本例中,我們要發送一個VM_CHAR消息。當按鍵按下時,VM_CHAR消息會發送給擁有鍵盤焦點的那個控件。實際上,VM_CHAR是一個Windows的常量符號,它定義為0x0102。wParam參數指定的是被按下按鍵的字符代碼。lParam參數指定的是不同的按鍵狀態碼,比如重復次數、掃描碼等。有了這些信息,就可以創建相應的C#簽名:
[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] static extern void SendMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam);
發送字符給控件的示例代碼:

1 static void Main(string[] args) 2 { 3 //... 4 SendChars(tb, "red"); 5 //... 6 } 7 8 static void SendChars(IntPtr hControl, string s) 9 { 10 foreach (char c in s) 11 { 12 SendChar(hControl, c); 13 } 14 } 15 16 static void SendChar(IntPtr hControl, char c) 17 { 18 uint WM_CHAR = 0x0102; 19 SendMessage1(hControl, WM_CHAR, c, 0); 20 } 21 [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] 22 static extern void SendMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam);
鼠標單擊一個控件
PostMessage()的函數簽名用C++簽名如下:
BOOL PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM LParam);
PostMessage()和SendMessage()的參數列表完全一致,他們的不同是:SendMessage()會等相應的Windows消息之后才會返回;PostMessage()不會。
相應的C#簽名:
[DllImport("user32.dll", EntryPoint = "PostMessage", CharSet = CharSet.Auto)] static extern bool PostMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam);
鼠標單擊一個控件的示例代碼:

1 static void Main(string[] args) 2 { 3 //... 4 ClickOn(okButt); 5 //... 6 } 7 8 static void ClickOn(IntPtr hControl) 9 { 10 uint WM_LBUTTONDOWN = 0x0201; 11 uint WM_LBUTTONUP = 0x0202; 12 PostMessage1(hControl, WM_LBUTTONDOWN, 0, 0); 13 PostMessage1(hControl, WM_LBUTTONUP, 0, 0); 14 } 15 [DllImport("user32.dll", EntryPoint = "PostMessage", CharSet = CharSet.Auto)] 16 static extern bool PostMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam);
處理消息對話框
消息對話框是一個上層(top-level)窗體,使用FindWindow()函數捕獲它。
處理菜單
處理菜單的的示例代碼:

1 static void Main(string[] args) 2 { 3 //... 4 5 //mwh: main window handle 6 IntPtr hMainMenu = GetMenu(mwh); 7 IntPtr hFile = GetSubMenu(hMainMenu, 0); 8 int iExit = GetMenuItemID(hFile, 2); 9 uint WM_COMMAND = 0x0111; 10 SendMessage2(mwh, WM_COMMAND, iExit, IntPtr.Zero); 11 //... 12 } 13 14 [DllImport("user32.dll")] // 15 static extern IntPtr GetMenu(IntPtr hWnd); 16 17 [DllImport("user32.dll")] // 18 static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos); 19 20 [DllImport("user32.dll")] // 21 static extern int GetMenuItemID(IntPtr hMenu, int nPos); 22 23 [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] 24 static extern void SendMessage2(IntPtr hWnd, uint Msg, int wParam, IntPtr lParam);
- GetMenu():返回程序主菜單的句柄
- GetSubMenu():返回子菜單的句柄
- GetSubMenu():返回菜單項的索引值。
在該示例中選擇File->Exit,並點擊它。
圖3 處理菜單
檢查應用程序狀態
使用VM_GETTEXT和SendMessage()獲得控件狀態
檢查應用程序狀態的示例代碼

1 static void Main(string[] args) 2 { 3 //... 4 uint VM_GETTEXT = 0x000D; 5 byte[] buffer=new byte[256]; 6 string text = null; 7 int numFetched = SendMessage3(tb, VM_GETTEXT, 256, buffer); 8 text = System.Text.Encoding.Unicode.GetString(buffer); 9 Console.WriteLine("Fetched " + numFetched + " chars"); 10 Console.WriteLine("TextBox1 contains = " + text); 11 //... 12 } 13 [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] 14 static extern int SendMessage3(IntPtr hWndControl, uint Msg, int wParam, byte[] lParam);
示例程序

1 // Chapter 3 - Windows-Based UI Testing 2 // Example Program: WindowsUITest 3 4 using System; 5 using System.Diagnostics; 6 using System.Runtime.InteropServices; 7 using System.Threading; 8 9 namespace WindowsUITest 10 { 11 class Class1 12 { 13 [STAThread] 14 static void Main(string[] args) 15 { 16 try 17 { 18 Console.WriteLine("\nLaunching application under test"); 19 20 string path = "..\\..\\..\\WinApp\\bin\\Debug\\WinApp.exe"; 21 Process p = Process.Start(path); 22 23 Console.WriteLine("\nFinding main window handle"); 24 IntPtr mwh = FindMainWindowHandle("Form1", 100, 25); 25 Console.WriteLine("Main window handle = " + mwh); 26 27 Console.WriteLine("\nFinding handles to textBox1, comboBox1"); 28 Console.WriteLine(" button1, listBox1"); 29 30 // you may want to add delays here to make sure Form has rendered 31 IntPtr tb = FindWindowEx(mwh, IntPtr.Zero, null, "<enter color>"); 32 IntPtr cb = FindWindowByIndex(mwh, 2); 33 IntPtr butt = FindWindowEx(mwh, IntPtr.Zero, null, "button1"); 34 IntPtr lb = FindWindowByIndex(mwh, 5); 35 36 if (tb == IntPtr.Zero || cb == IntPtr.Zero || 37 butt == IntPtr.Zero || lb == IntPtr.Zero) 38 throw new Exception("Unable to find all controls"); 39 else 40 Console.WriteLine("All control handles found"); 41 42 Console.WriteLine("\nClicking button1"); 43 ClickOn(butt); 44 45 Console.WriteLine("Clicking away Error message box"); 46 IntPtr mb = FindMessageBox("Error"); 47 if (mb == IntPtr.Zero) 48 throw new Exception("Unable to find message box"); 49 IntPtr okButt = FindWindowEx(mb, IntPtr.Zero, null, "OK"); 50 if (okButt == IntPtr.Zero) 51 throw new Exception("Unable to find OK button"); 52 ClickOn(okButt); 53 54 Console.WriteLine("Typing 'red' and 'blue' to application"); 55 SendChars(tb, "red"); 56 57 Console.WriteLine("Check for textBox1"); 58 uint VM_GETTEXT = 0x000D; 59 byte[] buffer = new byte[256]; 60 string text = null; 61 int numFetched = SendMessage3(lb, VM_GETTEXT, 256, buffer); 62 text = System.Text.Encoding.Unicode.GetString(buffer); 63 Console.WriteLine("Fetched " + numFetched + " chars"); 64 Console.WriteLine("TextBox1 contains = " + text); 65 66 ClickOn(cb); 67 SendChars(cb, "blue"); 68 69 Console.WriteLine("Clicking on button1"); 70 ClickOn(butt); 71 72 Console.WriteLine("\nChecking listBox1 for 'purple'"); 73 74 uint LB_FINDSTRING = 0x018F; 75 int result = SendMessage4(lb, LB_FINDSTRING, -1, "Result is purple1"); 76 if (result >= 0) 77 Console.WriteLine("\nTest scenario result = Pass"); 78 else 79 Console.WriteLine("\nTest scenario result = *FAIL*"); 80 81 82 Console.WriteLine("\nExiting app in 3 seconds . . . "); 83 //GetMenu not work 84 Thread.Sleep(3000); 85 IntPtr hMainMenu = GetMenu(mwh); 86 IntPtr hFile = GetSubMenu(hMainMenu, 0); 87 int iExit = GetMenuItemID(hFile, 2); 88 uint WM_COMMAND = 0x0111; 89 SendMessage2(mwh, WM_COMMAND, iExit, IntPtr.Zero); 90 91 Console.WriteLine("\nDone"); 92 Console.ReadLine(); 93 } 94 catch (Exception ex) 95 { 96 Console.WriteLine("Fatal error: " + ex.Message); 97 } 98 } // Main() 99 100 static IntPtr FindTopLevelWindow(string caption, int delay, int maxTries) 101 { 102 IntPtr mwh = IntPtr.Zero; 103 bool formFound = false; 104 int attempts = 0; 105 106 do 107 { 108 mwh = FindWindow(null, caption); 109 if (mwh == IntPtr.Zero) 110 { 111 Console.WriteLine("Form not yet found"); 112 Thread.Sleep(delay); 113 ++attempts; 114 } 115 else 116 { 117 Console.WriteLine("Form has been found"); 118 formFound = true; 119 } 120 } while (!formFound && attempts < maxTries); 121 122 if (mwh != IntPtr.Zero) 123 return mwh; 124 else 125 throw new Exception("Could not find Main Window"); 126 } // FindTopLevelWindow() 127 128 static IntPtr FindMainWindowHandle(string caption, int delay, int maxTries) 129 { 130 return FindTopLevelWindow(caption, delay, maxTries); 131 } 132 133 static IntPtr FindMessageBox(string caption) 134 { 135 int delay = 100; 136 int maxTries = 25; 137 return FindTopLevelWindow(caption, delay, maxTries); 138 } 139 140 static IntPtr FindWindowByIndex(IntPtr hwndParent, int index) 141 { 142 if (index == 0) 143 return hwndParent; 144 else 145 { 146 int ct = 0; 147 IntPtr result = IntPtr.Zero; 148 do 149 { 150 result = FindWindowEx(hwndParent, result, null, null); 151 if (result != IntPtr.Zero) 152 ++ct; 153 } while (ct < index && result != IntPtr.Zero); 154 155 return result; 156 } 157 } // FindWindowByIndex() 158 159 static void ClickOn(IntPtr hControl) 160 { 161 uint WM_LBUTTONDOWN = 0x0201; 162 uint WM_LBUTTONUP = 0x0202; 163 PostMessage1(hControl, WM_LBUTTONDOWN, 0, 0); 164 PostMessage1(hControl, WM_LBUTTONUP, 0, 0); 165 Thread.Sleep(1000); 166 } 167 168 static void SendChar(IntPtr hControl, char c) 169 { 170 uint WM_CHAR = 0x0102; 171 SendMessage1(hControl, WM_CHAR, c, 0); 172 } 173 174 static void SendChars(IntPtr hControl, string s) 175 { 176 foreach (char c in s) 177 { 178 SendChar(hControl, c); 179 } 180 } 181 182 // P/Invoke Aliases 183 184 [DllImport("user32.dll", EntryPoint = "FindWindow", CharSet = CharSet.Auto)] 185 static extern IntPtr FindWindow(string lpClassName, string lpWindowName); 186 187 [DllImport("user32.dll", EntryPoint = "FindWindowEx", CharSet = CharSet.Auto)] 188 static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); 189 190 // for WM_CHAR message 191 [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] 192 static extern void SendMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam); 193 194 // for WM_COMMAND message 195 [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] 196 static extern void SendMessage2(IntPtr hWnd, uint Msg, int wParam, IntPtr lParam); 197 198 // for WM_LBUTTONDOWN and WM_LBUTTONUP messages 199 [DllImport("user32.dll", EntryPoint = "PostMessage", CharSet = CharSet.Auto)] 200 static extern bool PostMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam); 201 202 // for WM_GETTEXT message 203 [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] 204 static extern int SendMessage3(IntPtr hWndControl, uint Msg, int wParam, byte[] lParam); 205 206 // for LB_FINDSTRING message 207 [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] 208 static extern int SendMessage4(IntPtr hWnd, uint Msg, int wParam, string lParam); 209 210 // Menu routines 211 [DllImport("user32.dll")] // 212 static extern IntPtr GetMenu(IntPtr hWnd); 213 214 [DllImport("user32.dll")] // 215 static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos); 216 217 [DllImport("user32.dll")] // 218 static extern int GetMenuItemID(IntPtr hMenu, int nPos); 219 220 } // class 221 } // ns
參考
[1] C# DllImport的用法