《软件测试自动化之道》读书笔记 之 基于Windows的UI测试

时间:2021-01-06 11:52:14

《软件测试自动化之道》读书笔记 之 基于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 }

《软件测试自动化之道》读书笔记 之 基于Windows的UI测试

 图1 AUT 

测试程序


返回

启动待测程序

 using System.Diagnostics;

         static void Main(string[] args)
{
//...
string path = "..\\..\\..\\WinApp\\bin\\Debug\\WinApp.exe";
Process p = Process.Start(path);
//...
}

获得待测程序主窗体的句柄

要获得待测程序主窗体的句柄,可使用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。

获得待测程序主窗体的句柄示例代码如下:

 using System.Runtime.InteropServices;

         static void Main(string[] args)
{
//...
IntPtr mwh = FindMainWindowHandle("Form1", , );
//...
} [DllImport("user32.dll", EntryPoint = "FindWindow", CharSet = CharSet.Auto)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName); static IntPtr FindMainWindowHandle(string caption, int delay, int maxTries)
{
return FindTopLevelWindow(caption, delay, maxTries);
} static IntPtr FindTopLevelWindow(string caption, int delay, int maxTries)
{
IntPtr mwh = IntPtr.Zero;
bool formFound = false;
int attempts = ; do
{
//FindWindow
mwh = FindWindow(null, caption);
if (mwh == IntPtr.Zero)
{
Console.WriteLine("Form not yet found");
Thread.Sleep(delay);
++attempts;
}
else
{
Console.WriteLine("Form has been found");
formFound = true;
}
} while (!formFound && attempts < maxTries); if (mwh != IntPtr.Zero)
return mwh;
else
throw new Exception("Could not find Main Window");
}

获得有名字控件的句柄

         static void Main(string[] args)
{
//...
IntPtr tb = FindWindowEx(mwh, IntPtr.Zero, null, "<enter color>");
//...
} [DllImport("user32.dll", EntryPoint = "FindWindowEx",CharSet = CharSet.Auto)]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

参数:

  • hwndParent:控件目标的父窗体句柄
  • hwndChildAfter:从哪个控件开始找,即从下一个控件开始找
  • lpszClass:class name(如上参数lpClassName)
  • lpszWindow:目标控件的window name/title/caption

《软件测试自动化之道》读书笔记 之 基于Windows的UI测试

图2 Spy++捕获控件button1  

获得无名字控件的句柄

如何获得一个没有名字的空间的句柄,可通过隐含索引来查找相应控件

         static void Main(string[] args)
{
//...
IntPtr cb = FindWindowByIndex(mwh, );
//...
} [DllImport("user32.dll", EntryPoint = "FindWindowEx",CharSet = CharSet.Auto)]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); static IntPtr FindWindowByIndex(IntPtr hwndParent, int index)
{
if (index == )
return hwndParent;
else
{
int ct = ;
IntPtr result = IntPtr.Zero;
do
{
//FindWindowEx
result = FindWindowEx(hwndParent, result, null, null);
if (result != IntPtr.Zero)
++ct;
} while (ct < index && result != IntPtr.Zero); return result;
}
}

注意:这里索引的顺序是加入主窗体的顺序。见如下代码,主窗体的索引值为0,先后加入的控件button1的索引为1,comboBox1的索引为2,...

         private void InitializeComponent()
{
//...
this.Controls.Add(this.button1);
this.Controls.Add(this.comboBox1);
this.Controls.Add(this.textBox1);
this.Controls.Add(this.menuStrip1);
this.Controls.Add(this.listBox1);
//...
}

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

发送字符给控件的示例代码:

         static void Main(string[] args)
{
//...
SendChars(tb, "red");
//...
} static void SendChars(IntPtr hControl, string s)
{
foreach (char c in s)
{
SendChar(hControl, c);
}
} static void SendChar(IntPtr hControl, char c)
{
uint WM_CHAR = 0x0102;
SendMessage1(hControl, WM_CHAR, c, );
}
[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
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);

鼠标单击一个控件的示例代码:

         static void Main(string[] args)
{
//...
ClickOn(okButt);
//...
} static void ClickOn(IntPtr hControl)
{
uint WM_LBUTTONDOWN = 0x0201;
uint WM_LBUTTONUP = 0x0202;
PostMessage1(hControl, WM_LBUTTONDOWN, , );
PostMessage1(hControl, WM_LBUTTONUP, , );
}
[DllImport("user32.dll", EntryPoint = "PostMessage", CharSet = CharSet.Auto)]
static extern bool PostMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam);

处理消息对话框

消息对话框是一个上层(top-level)窗体,使用FindWindow()函数捕获它。

处理菜单

处理菜单的的示例代码:

         static void Main(string[] args)
{
//... //mwh: main window handle
IntPtr hMainMenu = GetMenu(mwh);
IntPtr hFile = GetSubMenu(hMainMenu, );
int iExit = GetMenuItemID(hFile, );
uint WM_COMMAND = 0x0111;
SendMessage2(mwh, WM_COMMAND, iExit, IntPtr.Zero);
//...
} [DllImport("user32.dll")] //
static extern IntPtr GetMenu(IntPtr hWnd); [DllImport("user32.dll")] //
static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos); [DllImport("user32.dll")] //
static extern int GetMenuItemID(IntPtr hMenu, int nPos); [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
static extern void SendMessage2(IntPtr hWnd, uint Msg, int wParam, IntPtr lParam);
  • GetMenu():返回程序主菜单的句柄
  • GetSubMenu():返回子菜单的句柄
  • GetSubMenu():返回菜单项的索引值。

在该示例中选择File->Exit,并点击它。

《软件测试自动化之道》读书笔记 之 基于Windows的UI测试

图3 处理菜单

检查应用程序状态

使用VM_GETTEXT和SendMessage()获得控件状态

检查应用程序状态的示例代码

         static void Main(string[] args)
{
//...
uint VM_GETTEXT = 0x000D;
byte[] buffer=new byte[];
string text = null;
int numFetched = SendMessage3(tb, VM_GETTEXT, , buffer);
text = System.Text.Encoding.Unicode.GetString(buffer);
Console.WriteLine("Fetched " + numFetched + " chars");
Console.WriteLine("TextBox1 contains = " + text);
//...
}
[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
static extern int SendMessage3(IntPtr hWndControl, uint Msg, int wParam, byte[] lParam);

示例程序

 // Chapter 3 - Windows-Based UI Testing
// Example Program: WindowsUITest using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading; namespace WindowsUITest
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
try
{
Console.WriteLine("\nLaunching application under test"); string path = "..\\..\\..\\WinApp\\bin\\Debug\\WinApp.exe";
Process p = Process.Start(path); Console.WriteLine("\nFinding main window handle");
IntPtr mwh = FindMainWindowHandle("Form1", , );
Console.WriteLine("Main window handle = " + mwh); Console.WriteLine("\nFinding handles to textBox1, comboBox1");
Console.WriteLine(" button1, listBox1"); // you may want to add delays here to make sure Form has rendered
IntPtr tb = FindWindowEx(mwh, IntPtr.Zero, null, "<enter color>");
IntPtr cb = FindWindowByIndex(mwh, );
IntPtr butt = FindWindowEx(mwh, IntPtr.Zero, null, "button1");
IntPtr lb = FindWindowByIndex(mwh, ); if (tb == IntPtr.Zero || cb == IntPtr.Zero ||
butt == IntPtr.Zero || lb == IntPtr.Zero)
throw new Exception("Unable to find all controls");
else
Console.WriteLine("All control handles found"); Console.WriteLine("\nClicking button1");
ClickOn(butt); Console.WriteLine("Clicking away Error message box");
IntPtr mb = FindMessageBox("Error");
if (mb == IntPtr.Zero)
throw new Exception("Unable to find message box");
IntPtr okButt = FindWindowEx(mb, IntPtr.Zero, null, "OK");
if (okButt == IntPtr.Zero)
throw new Exception("Unable to find OK button");
ClickOn(okButt); Console.WriteLine("Typing 'red' and 'blue' to application");
SendChars(tb, "red"); Console.WriteLine("Check for textBox1");
uint VM_GETTEXT = 0x000D;
byte[] buffer = new byte[];
string text = null;
int numFetched = SendMessage3(lb, VM_GETTEXT, , buffer);
text = System.Text.Encoding.Unicode.GetString(buffer);
Console.WriteLine("Fetched " + numFetched + " chars");
Console.WriteLine("TextBox1 contains = " + text); ClickOn(cb);
SendChars(cb, "blue"); Console.WriteLine("Clicking on button1");
ClickOn(butt); Console.WriteLine("\nChecking listBox1 for 'purple'"); uint LB_FINDSTRING = 0x018F;
int result = SendMessage4(lb, LB_FINDSTRING, -, "Result is purple1");
if (result >= )
Console.WriteLine("\nTest scenario result = Pass");
else
Console.WriteLine("\nTest scenario result = *FAIL*"); Console.WriteLine("\nExiting app in 3 seconds . . . ");
//GetMenu not work
Thread.Sleep();
IntPtr hMainMenu = GetMenu(mwh);
IntPtr hFile = GetSubMenu(hMainMenu, );
int iExit = GetMenuItemID(hFile, );
uint WM_COMMAND = 0x0111;
SendMessage2(mwh, WM_COMMAND, iExit, IntPtr.Zero); Console.WriteLine("\nDone");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("Fatal error: " + ex.Message);
}
} // Main() static IntPtr FindTopLevelWindow(string caption, int delay, int maxTries)
{
IntPtr mwh = IntPtr.Zero;
bool formFound = false;
int attempts = ; do
{
mwh = FindWindow(null, caption);
if (mwh == IntPtr.Zero)
{
Console.WriteLine("Form not yet found");
Thread.Sleep(delay);
++attempts;
}
else
{
Console.WriteLine("Form has been found");
formFound = true;
}
} while (!formFound && attempts < maxTries); if (mwh != IntPtr.Zero)
return mwh;
else
throw new Exception("Could not find Main Window");
} // FindTopLevelWindow() static IntPtr FindMainWindowHandle(string caption, int delay, int maxTries)
{
return FindTopLevelWindow(caption, delay, maxTries);
} static IntPtr FindMessageBox(string caption)
{
int delay = ;
int maxTries = ;
return FindTopLevelWindow(caption, delay, maxTries);
} static IntPtr FindWindowByIndex(IntPtr hwndParent, int index)
{
if (index == )
return hwndParent;
else
{
int ct = ;
IntPtr result = IntPtr.Zero;
do
{
result = FindWindowEx(hwndParent, result, null, null);
if (result != IntPtr.Zero)
++ct;
} while (ct < index && result != IntPtr.Zero); return result;
}
} // FindWindowByIndex() static void ClickOn(IntPtr hControl)
{
uint WM_LBUTTONDOWN = 0x0201;
uint WM_LBUTTONUP = 0x0202;
PostMessage1(hControl, WM_LBUTTONDOWN, , );
PostMessage1(hControl, WM_LBUTTONUP, , );
Thread.Sleep();
} static void SendChar(IntPtr hControl, char c)
{
uint WM_CHAR = 0x0102;
SendMessage1(hControl, WM_CHAR, c, );
} static void SendChars(IntPtr hControl, string s)
{
foreach (char c in s)
{
SendChar(hControl, c);
}
} // P/Invoke Aliases [DllImport("user32.dll", EntryPoint = "FindWindow", CharSet = CharSet.Auto)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll", EntryPoint = "FindWindowEx", CharSet = CharSet.Auto)]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); // for WM_CHAR message
[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
static extern void SendMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam); // for WM_COMMAND message
[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
static extern void SendMessage2(IntPtr hWnd, uint Msg, int wParam, IntPtr lParam); // for WM_LBUTTONDOWN and WM_LBUTTONUP messages
[DllImport("user32.dll", EntryPoint = "PostMessage", CharSet = CharSet.Auto)]
static extern bool PostMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam); // for WM_GETTEXT message
[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
static extern int SendMessage3(IntPtr hWndControl, uint Msg, int wParam, byte[] lParam); // for LB_FINDSTRING message
[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
static extern int SendMessage4(IntPtr hWnd, uint Msg, int wParam, string lParam); // Menu routines
[DllImport("user32.dll")] //
static extern IntPtr GetMenu(IntPtr hWnd); [DllImport("user32.dll")] //
static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos); [DllImport("user32.dll")] //
static extern int GetMenuItemID(IntPtr hMenu, int nPos); } // class
} // ns

参考

[1] C# DllImport的用法

[2] C#与win32 api数据类型对照