《軟件測試自動化之道》讀書筆記 之 基於Windows的UI測試


《軟件測試自動化之道》讀書筆記 之 基於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 }
View Code

 圖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         }
View Code

獲得待測程序主窗體的句柄

要獲得待測程序主窗體的句柄,可使用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。

C#與win32 api數據類型對照 

參數:

  • 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         } 
View Code

獲得有名字控件的句柄

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);
View Code

參數:

  • 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         }
View Code

注意:這里索引的順序是加入主窗體的順序。見如下代碼,主窗體的索引值為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         }
View Code

你也可以通過工具“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);
View Code

鼠標單擊一個控件

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);
View Code

處理消息對話框

消息對話框是一個上層(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);
View Code
  • 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);
View Code

 

示例程序

  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
View Code

 

參考

[1] C# DllImport的用法

[2] C#與win32 api數據類型對照


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM