背景介绍:
介绍搜狗输入法按键响应自动化测试的方法及工具,虽然本文中的工具是深度定制化的输入法测试工具,无法应用在其他项目,但相信本文中所使用的Xposed Hook技术、socket多进程通信技术以及自动化思想会对大家有帮助。
按键响应测试是什么?
当我们在手机上打一个字时,它大致经历了如下过程:
因此,在以上过程中,从用户按下软键盘上的按键到候选词显示的所花费的时间,是衡量输入法性能的一项重要指标。
按键响应怎么测试?
按键响应的测试方法有两种:日志方式和视频拍摄方式。
日志方式:在上述用户按下按键时,程序执行onKeyEvent函数中写日志记下时间戳T1;当开始显示候选词时,程序执行绘制显示函数时写日志记下时间戳T2,通过T2-T1即可获得按键响应的时间。
优点是:通过日志方式计算,工具成本低;
缺点是:经过大量的实践对比,函数执行时界面并没有显示,所以这并不能代表用户实际的体验。
视频拍摄方式:即通过慢速摄像(240帧/S)的录像下,记录从按下按键到显示候选词的过程,进而通过视频拆帧工具进行逐帧的对比。
优点是:拍摄的内容是用户实际的感受,代表了用户实际的体验;
缺点是:拍摄视频并拆帧没有形成的工具,成本高,操作繁琐。
为了得到最接近用户体验的数据,目前搜狗输入法使用的是视频拍摄方式。
按键响应最大的问题是什么?
如上述所说,这个测试过程中效率非常慢,我们遇到了最大的两个问题:
1.如何准确地获得按下按键的时间。
以前我们的做法是这样的:竖一面镜子,通过镜子的反射来获取手指抬起的时间。如下图:
现在我们的做法是这样:
1).通过XPosed的Hook框架,对用户在手机上按下按键和抬起按键的事件进行Hook;
2).将Hook到的信息通过Socket发送给浮动窗口;
3).浮动窗口收到事件消息后,根据按下抬起的时间分别显示绿色和红色,帮助测试人员识别颜色的变化。
通过hook之后的touch时间处理过程
最后的效果如下:
2.如何更高效的拆帧获得时间间隔。
以前我们的做法是这样的:用KMplayer将慢速视频拆帧,然后一帧一帧地人工计算,最后得到T1和T2之间的帧数。
现在我们的做法是这样:
1)通过测试工具,对视频进行自动拆帧
2)基于图像识别的算法,对视频中浮动窗口方框进行识别和输入法候选词识别
3)通过图像识别得到准确的按下时间T1和候选词显示时间T2,最终得到差值。
效果如下:
代码分享阶段:
Xposed Hook的代码实现:
findAndHookMethod("com.android.server.wm.PointerEventDispatcher",null, "onInputEvent", InputEvent.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { //XposedBridge.log( "com.android.server.wm.PointerEventDispatcher Hook Success!"); MotionEvent inputEvent = (MotionEvent) param.args[0]; int k = inputEvent.getAction(); DecimalFormat df = new DecimalFormat("0.00"); float x = inputEvent.getX(); float y = inputEvent.getY(); SocketAddress address = new InetSocketAddress("127.0.0.1", 8803); switch (k & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: XposedBridge.log("ACTION_DOWN " + k + " x=" + df.format(x) + " y=" + df.format(y)); try { String string = "ACTION_DOWN"; byte[] data = string.getBytes(); DatagramPacket mPacket = new DatagramPacket(data, data.length,address); DatagramSocket mSocket = new DatagramSocket(); mSocket.send(mPacket); mSocket.close(); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SocketException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } break; case MotionEvent.ACTION_UP: XposedBridge.log("ACTION_UP " + k + " x=" + df.format(x) + " y=" + df.format(y)); try { String string = "ACTION_UP"; byte[] data = string.getBytes(); DatagramPacket mPacket = new DatagramPacket(data, data.length,address); DatagramSocket mSocket = new DatagramSocket(); mSocket.send(mPacket); mSocket.close(); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SocketException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } break; case MotionEvent.ACTION_POINTER_UP: XposedBridge.log("ACTION_POINTER_UP " + k + " x=" + df.format(x) + " y=" + df.format(y)); break; case MotionEvent.ACTION_POINTER_DOWN: XposedBridge.log("ACTION_POINTER_DOWN " + k + " x=" + df.format(x) + " y=" + df.format(y)); break; } super.beforeHookedMethod(param); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable{ super.afterHookedMethod(param); } }); } public class UDPServer extends Thread{ private static final String MSG_TAG = "UDPServer"; @Override public void run() { try { byte[] buffer = new byte[20]; DatagramSocket mServerSocket = new DatagramSocket(8803); DatagramPacket mPacket = new DatagramPacket(buffer, buffer.length); while (true) { mServerSocket.receive(mPacket); Log.d(MSG_TAG , "接收到的长度:" + mPacket.getLength() ); Log.d(MSG_TAG , "端口地址:" + mPacket.getAddress().getHostAddress() ); String strRead = new String(mPacket.getData()).trim(); Log.d(MSG_TAG , "内容:" + strRead ); Looper curLooper = Looper.myLooper (); Looper mainLooper = Looper.getMainLooper (); String msg = "" ; if (curLooper== null ){ myHandler = new MyHandler(mainLooper); } else { myHandler = new MyHandler(curLooper); } if (strRead.contains("ACTION_UP")){ msg = "ACTION_UP"; }else if(strRead.contains("ACTION_DOWN")){ msg = "ACTION_DOWN"; }else { msg = "NO ACTION"; } myHandler.removeMessages(0); Message m = myHandler.obtainMessage(1, 1, 1, msg); myHandler.sendMessage(m); } } catch (SocketException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } // } catch (ClassNotFoundException e) { // // TODO Auto-generated catch block // e.printStackTrace(); // } } }
作者:搜狗测试 诸葛东明
来源:51Testing软件测试网原创