C#借助API实现黑盒自动化测试工具的编写
本文代码下载(VS2010开发):http://download.csdn.net/source/2796362
本文摘要:
1:一个简单的例子
1.1:EnumChildWindows介绍
1.2:主要源码
2:难点:如何获取指定的控件句柄
2.1:使用SPY++
2.2:获取控件位置
2.3:获取控件ID
1:一个简单的例子
在日常编码过程中,我们常常会进行自动化测试。这里的自动化测试不是指单元测试,而是模拟人工输入来进行快速的、高并发的测试。可以使用的自动化工具有LOADRUNNER,以及目前在VS2010中的功能很强大的测试工作平台(录制操作步骤,自动生成代码)。但是,这些工具的熟练掌握也有一定的时间成本,并且,最主要的,对于一个程序员来说,那不够灵活。所以,比较高效的一个做法是,调用WINDOWS API,自己动手写编码来实现。
下面做一个简单的演示。为了简便起见,假设存在这样一个应用程序:
1:提供一个WINFORM窗体,上面存在一个TextBox,以及一个Button;
2:点击Button,会弹出提示框,提示框内容为TextBox的值;
现在,测试要求如下:
1:在300台机器上运行上面的程序;
2:到这300台机器上去点击这个Button,看看上文中的功能2有没有实现;
很显然,实际情况中没有这么简单的程序,实际的情况有可能是点击Button,统一下载一个文件,而测试的要求可能就变为考核服务器的负载。现在,测试部显然也没有300个人坐在客户机上验证测试的结果,这个时候,就需要我们提供一个自动化的测试工具,来完成必要的测试任务。
测试工具,首先也是一个C#的程序,它的主要目的是:
1:获取上文应用程序的窗口句柄,继而获取TextBox句柄及Button句柄;
2:为TextBox随机填入一些字符;
3:模拟点击Button;
1.1:EnumChildWindows介绍
在这里需要介绍下EnumChildWindows,
EnumChildWindows可是个好东西,可以枚举一个父窗口的所有子窗口:
BOOL EnumChildWindows(
HWND hWndParent, // handle to parent window // 父窗口句柄
WNDENUMPROC lpEnumFunc, // callback function // 回调函数的地址
LPARAM lParam // application-defined value // 你自已定义的参数
);
就这么简单,让我们再定义一个回调函数,像下面这样:
BOOL CALLBACK EnumChildProc(
HWND hwnd, // handle to child window
LPARAM lParam // application-defined value
);
在调用EnumChildWindows 这个函数时,直到调用到最个一个子窗口被枚举或回调函数返回一个false,否则将一直枚举下去。
1.2:简单例子的主要源码
测试工具的主要代码如下:
private void button1_Click(object sender, EventArgs e) { //获取测试程序的窗体句柄 IntPtr mainWnd = FindWindow(null, "FormLogin"); List<IntPtr> listWnd = new List<IntPtr>(); //获取窗体上OK按钮的句柄 IntPtr hwnd_button = FindWindowEx(mainWnd, new IntPtr(0), null, "OK"); //获取窗体上所有控件的句柄 EnumChildWindows(mainWnd, new CallBack(delegate(IntPtr hwnd, int lParam) { listWnd.Add(hwnd); return true; }), 0); foreach (IntPtr item in listWnd) { if (item != hwnd_button) { char[] UserChar = "luminji".ToCharArray(); foreach (char ch in UserChar) { SendChar(item, ch, 100); } } } SendMessage(hwnd_button, WM_CLICK, mainWnd, "0"); } public void SendChar(IntPtr hand, char ch, int SleepTime) { PostMessage(hand, WM_CHAR, ch, 0); System.Threading.Thread.Sleep(SleepTime); } public static int WM_CHAR = 0x102; public static int WM_CLICK = 0x00F5; [DllImport("User32.dll", EntryPoint = "SendMessage")] public static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, string lParam); [DllImport("user32.dll")] public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll")] public static extern int AnyPopup(); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("user32.dll")] public static extern int EnumThreadWindows(IntPtr dwThreadId, CallBack lpfn, int lParam); [DllImport("user32.dll")] public static extern int EnumChildWindows(IntPtr hWndParent, CallBack lpfn, int lParam); [DllImport("user32.dll", CharSet = CharSet.Ansi)] public static extern IntPtr PostMessage(IntPtr hwnd, int wMsg, int wParam, int lParam); [DllImport("user32.dll", CharSet = CharSet.Ansi)] public static extern IntPtr SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern IntPtr SendMessageA(IntPtr hwnd, int wMsg, int wParam, int lParam); [DllImport("user32.dll", CharSet = CharSet.Auto)] static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern int GetWindowTextLength(IntPtr hWnd); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] public static extern IntPtr GetParent(IntPtr hWnd); public delegate bool CallBack(IntPtr hwnd, int lParam);
运行效果:
2:难点:如何获取指定的控件句柄
细心的人可能已经发现,上文中,给文本框赋值的地方,使用了如下代码:
foreach (IntPtr item in listWnd) { if (item != hwnd_button) { char[] UserChar = "luminji".ToCharArray(); foreach (char ch in UserChar) { SendChar(item, ch, 100); } } }
假设我们的窗体上有多个文本框,那么事实上,这段代码会给所有的文本框输入"luminji”字样。这在多数应用程序中都是不允许的,我们需要精确定位需要控制的控件。
我们在得到OK按钮的句柄的时候,使用了函数:
IntPtr hwnd_button = FindWindowEx(mainWnd, new IntPtr(0), null, "OK");
而想要获取文本框句柄的时候,这个函数却不能使用,因为,所有文本框都是没有标题的,也就是类似"OK"这个值。有人说,那就使用控件ID吧。且看:
2.1:获取控件ID
非.NET程序,一旦程序被生成,控件ID就是固定的,所以这一招,用在非.NET程序中,那是再好也不过了。
根据ID来得到控件句柄的函数声明如下:
[DllImport("user32.dll ", EntryPoint = "GetDlgItem")] public static extern IntPtr GetDlgItem( IntPtr hParent, int nIDParentItem);
其中,第一个参数就是窗体的句柄,第二个参数就是控件ID。
但是,显然,这种方法不适用于我们的.NET程序,因为我们会发现,我们的.NET程序没运行一次,这个ID是变化的。
2.2:获取控件位置
所以,最终的一个方案是:根据控件位置,人工比对后得到我们想要的控件句柄。该函数的声明如下:
好了,现在的关键就是怎么取得这个控件的位置。我们在VS中查看,某个控件有X坐标和Y坐标,以上面程序的这个TextBox来说,其在VS中显示的位置是“70,83”,但是而VS中显示的,是不包含标题和边框的坐标值。但是这个坐标值可以作为我们人工比对的参考。
更精确的坐标值,我们写代码来实现,如下:
EnumChildWindows(mainWnd, new CallBack(delegate(IntPtr hwnd, int lParam) { listWnd.Add(hwnd); StringBuilder className = new StringBuilder(126); StringBuilder title = new StringBuilder(200); GetWindowText(hwnd, title, 200); RECT clientRect; GetClientRect(hwnd, out clientRect); int controlWidth = clientRect.Width; int controlHeight = clientRect.Height; int x = 0, y = 0; IntPtr parerntHandle = GetParent(hwnd); if (parerntHandle != IntPtr.Zero) { GetWindowRect(hwnd, out clientRect); RECT rect; GetWindowRect(parerntHandle, out rect); x = clientRect.X - rect.X; y = clientRect.Y - rect.Y; Debug.Print(x.ToString()); Debug.Print(y.ToString()); } return true; }), 0);
2.3:根据EnumChildWindows枚举次序得到句柄
2.4:使用SPY++
SPY++是微软的一个工具,用户获取窗体上的ID或者类型或者句柄等信息。因为在我们的这个例子里,ID和句柄在每台机器上都是不变的,所以这个工具对于我们来说,没有多大的用处。但是,当你HACK别人的程序的时候,它会发挥一定作用。
参考:
1:http://book.21www.cn/info/vb/api/4076.html
2:http://dev.firnow.com/course/3_program/cshapo/csharpjs/20100714/441439.html