因为一些私人的事情,本来早已经应该完成的一篇文章一直到今天才可以草草了结。在前面的两篇文章《图像相似度算法的C#实现及测评》《对“画条线”(Draw a line)的单元测试几点想法和实践 》中,先后介绍了一个简单的会读直方图算法和一些关于GUI画图的测试想法。有必要说明的是,在《对“画条线”(Draw a line)的单元测试几点想法和实践》中提到的几种方法,最实用的是Mock法并不是今天的主题。
这篇文章中继续前面的思路,简单写写有关GUI自动化测试的一点想法。
问题
对于画线,画图等应用程序的功能自动化测试的解决方案?
解决思路
采取截图法,即将用例中的输出截图,以图片作为输出结果,当然之前需要一个相应的图片作为预期结果,以便于比较。
对于预期结果图片,可以采用的方式是先运行一次自动化测试代码截得一幅图片,然后手动检查图片是否为与其效果,如是则将该图片作为预期结果。(在功能自动化测试中,在第一次运行自动化测试脚本的时候,是应该在人工监视的条件下进行的,而更多时候在我们调试相应的脚本的时候就已经完成了相应的工作。)
示例代码
1,待测代码示例
/// <summary>
/// 在Windows窗口上画图形
/// </summary>
class Draw_A_Line : System.Windows.Forms.Form
{
//private Form MainForm;
System.ComponentModel.Container component = null;
Pen myPen = new Pen(Color.Black);
Bitmap bmpImg = null;
public Draw_A_Line()
{
InitializeComponent();
}
private void InitializeComponent()
{
this.component = new System.ComponentModel.Container();
this.Size = new Size(256, 256);
this.Text = "TestMainForm";
}
/// <summary>
/// 清理正在使用的资源
/// </summary>
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (component != null)
{
saveBmp(); //保存截图
component.Dispose();
}
}
base.Dispose(disposing);
}
//static Graphics SaveGraph;
protected override void OnPaint(PaintEventArgs e)
{
Graphics g = e.Graphics;
myPen.Width = 5;
g.DrawLine(myPen, 16, 27, 38, 49);
}
}
2,截图代码
[DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
public static extern int BitBlt(HandleRef hDC, int x, int y, int nWidth, int nHeight, HandleRef hSrcDC, int xSrc, int ySrc, int dwRop);
public void saveBmp()
{
//这里假设要保存一个窗体的内容
int width = 200; //获取宽度
int height = 200; //获取高度
const int SRCCOPY = 0xcc0020; //复制图块的光栅操作码
Bitmap bmSave = new Bitmap(width, height); //用于保存图片的位图对象
Graphics gSave = Graphics.FromImage(bmSave); //创建该位图的Graphics对象
HandleRef hDcSave = new HandleRef(null, gSave.GetHdc()); //得到句柄
Graphics gSrc = this.CreateGraphics(); //创建窗体的Graphics对象
HandleRef hDcSrc = new HandleRef(null, gSrc.GetHdc());
BitBlt(hDcSave, 0, 0, width, height, hDcSrc, 0, 0, SRCCOPY);
gSrc.ReleaseHdc();
gSave.ReleaseHdc();
bmSave.Save(@"C:\test.jpg",System.Drawing.Imaging.ImageFormat.Jpeg);
gSrc.Dispose();
gSave.Dispose();
bmSave.Dispose();
}
3,图像比较类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
namespace Draw_A_Line_UnitTestProject
{
class ImageComparator
{
public bool IsSame { get; private set; }
public ImageComparator(Image actualImage, Image expectedImage)
{
IsSame = false;
IsSame = ImgComp(actualImage, expectedImage);
}
public ImageComparator(string actualImageFile, string expectedImageFile)
{
Image actualImage = Image.FromFile(actualImageFile);
Image expectedImage = Image.FromFile(expectedImageFile);
IsSame = false;
IsSame = ImgComp(actualImage, expectedImage);
}
/// <summary>
/// 比较两张图片是否一致
/// </summary>
/// <param name="actualImage">被测图片</param>
/// <param name="expectedImage">参照图片</param>
/// <returns>如果为真,两张图片一样否则不一样</returns>
private bool ImgComp(Image actualImage, Image expectedImage)
{
//现将两张图片都变成256 X 256 大小
Bitmap actualBmp = Resize(actualImage);
Bitmap expectBmp = Resize(expectedImage);
//获取转换后的图片的直方图
int[] actualHisogram = GetHisogram(actualBmp);
int[] expectHisogram = GetHisogram(expectBmp);
//比较两个直方图数据的相似性
float result = GetFinalResult(actualHisogram, expectHisogram);
//如果相似度为1则表示两张图片是一样的
return (result == 1);
//其实我们可以看到如果仅仅只是要比较两个直方图是不是一样的,完全可以比较数组中相应的index的元素是不是相等即可
}
/// <summary>
/// 将图片的转化为目标大小并返回转化后的图片
/// </summary>
/// <param name="originImage">待转化图片</param>
/// <param name="width">转化后的宽度</param>
/// <param name="height">转化后的高度</param>
/// <returns>转化后的图片</returns>
private Bitmap Resize(Image originImage, int width, int height)
{
Bitmap outImg = new Bitmap(originImage, width, height);
//outImg.Dispose();
return outImg;
}
/// <summary>
/// 将图片转化为256 X 256大小并返回转化后的图片
/// </summary>
/// <param name="originImage">待转化图片</param>
/// <returns>转化后的图片</returns>
private Bitmap Resize(Image originImage)
{
return Resize(originImage, 256, 256);
}
/// <summary>
/// 获取位图文件的直方图数据
/// </summary>
/// <param name="bmpImg">位图文件</param>
/// <returns>位图文件的直方图数组</returns>
private int[] GetHisogram(Bitmap bmpImg)
{
BitmapData data = bmpImg.LockBits(new System.Drawing.Rectangle(0, 0, bmpImg.Width, bmpImg.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
int[] histogram = new int[256];
unsafe
{
byte* ptr = (byte*)data.Scan0;
int remain = data.Stride - data.Width * 3;
for (int i = 0; i < histogram.Length; i++)
histogram[i] = 0;
for (int i = 0; i < data.Height; i++)
{
for (int j = 0; j < data.Width; j++)
{
int mean = ptr[0] + ptr[1] + ptr[2];
mean /= 3;
histogram[mean]++;
ptr += 3;
}
ptr += remain;
}
}
bmpImg.UnlockBits(data);
return histogram;
}
#region 公式计算
/// <summary>
/// 计算两个整数相减后的绝对值除以两数中的较大值
/// </summary>
/// <param name="firstNum">被减数</param>
/// <param name="secondNum">减数</param>
/// <returns>计算结果</returns>
private float GetAbs(int firstNum, int secondNum)
{
int abs = Math.Abs(firstNum - secondNum);
int maxNum = Math.Max(firstNum, secondNum);
return (float)abs / ((maxNum == 0) ? 1 : maxNum);
}
/// <summary>
/// 计算两个直方图的相似度
/// </summary>
/// <param name="actualHisogram">待测图片的直方图</param>
/// <param name="expectedHisogram">参考图片的直方图</param>
/// <returns>得到直方图的相似度</returns>
private float GetFinalResult(int[] actualHisogram, int[] expectedHisogram)
{
if (actualHisogram.Length != expectedHisogram.Length)
{
return 0;
}
float result = 0;
int j = actualHisogram.Length;
for (int i = 0; i < j; i++)
{
result += 1 - GetAbs(actualHisogram[i], expectedHisogram[i]);
}
return result / j;
}
#endregion
}
}
4,调用输出
static void Main(string[] args)
{
//启动待测窗口
Draw_A_Line ad = new Draw_A_Line();
Console.WriteLine("After the AUT loaded, Try to close it manually.");
System.Windows.Forms.Application.Run(ad);
/*************************
* 当窗口弹出后,手动关闭窗口
***************************/
//比较预期输出和实际输出
ImageComparator com = new ImageComparator(@"C:\Test\Test.jpg", @"C:\Test.jpg");
if (com.IsSame)
Console.WriteLine("Haha, the two is the same~~");
Console.ReadKey();
}
方法改进和总结
我们可以看到这个方法存在着很多缺陷,这也是为什么我的标题中加入了“粗糙”的缘故:
1, 部分代码植入到了源代码中,如我们在源代码中重写了Dispose方法,加入了对于保存图像的调用相关代码,也加入了保存图像的方法到应用程序中。
当然,这一点我们可以通过重构以解决,把保存图片的代码抽离这个应该不会太让人纠结。把调用从Dispose方法中抽离出来,目前我的想法是使用多线程,一个线程用来运行待测程序,另外一个线程则用来截图。简单的多线程操作可以参见我的另外一篇介绍文章《Winform自动化测试解决对话框问题(多线程)》。
2,截图方法使用了Windows API,我们应该使用其他更为合适的方法。
3,上面的示例代码只是为了说明一个大体思路,并不能作为一个完整的解决方案。不过笔者会利用业余时间尽快实现一个具有实践意义的解决方案。
完整示例代码下载