厚脸皮说测试

在这里我们只说测试

Calabash Android 初始化和一些使用技巧

Calabash Android 真的不错

Android自动化测试有着茫茫多的自动化测试框架,由于本人是Ruby的fans。所以对Ruby封装的一些工具情有独钟。虽然是Ruby的fans,但是对BDD一直不太能接受。最近正好有一个机会,开始尝试着使用Calabash Android自动化测试做一些东西。

首先,还是先废话几句吧。Android的自动化测试版本太多,不能在这里一一介绍。但是,现在国内使用的框架基本上可以分为两大类:Android JUnit系列和UiAutomator系列。

Android Junit系列就包括大名鼎鼎的Robotium和今天要说的Calabash Android。这个系列除了有签名限制不能夸应用以外,都非常健壮和稳定。

UiAutomator系列最著名的当然要说是Appium。UiAutomator可以跨应用操作,整体操作完全是基于UI的操作。但是Android官方对UiAutomator的支持并不是非常好,比如输入法等。使得UiAutomator的运行稳定性相对于Android JUnit差很多。再加上Android本身的版本分裂,UiAutomator那就更会让开发者觉得头大。所以,如果没有跨应用的操作,请尽量不要选择UiAutomator系列的工具,至少现在不要。

在Android JUnit这个系列上选择一款工具做自动化测试的话,个人更倾向于选择Calabash Android](https://github.com/calabash/calabash-android)或者Selendroid。独立的QA团队更需要无源码方式的自动化测试框架。其次,简单高效稳定是任何自动化测试框架的基础。从这两点出发,选择了以上两款工具。Selendroid是使用Java开发的,一般情况下需要使用Java来编写自动化测试用例。作为Ruby的fans更应该尝试一下Calabash Android](https://github.com/calabash/calabash-android)。

Calabash的环境配置

Calabash的本身的安装相对简单,并且有官方文档。现在安装的Calabash都是0.5版本的。

安装完成之后,进行简单的配置才可以使用Calabash。

1、配置环境变量

配置环境变量最关键点

export ANDROID_HOME=/Users/<user>/adt-bundle-mac-x86_64-20140702/sdk
export PATH=$PATH:$ANDROID_HOME/tools

2、生成签名,并且知道自己签名的password和alias

生成签名可以使用一下Shell来生成。

keytool -genkey -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US"

生成之后签名的位置在

~/.android/debug.keystore

password是:

android

alias是:

androiddebugkey

以上这些关键元素在配置Calabash的时候都很重要。

3、配置Calabash

Calabash的配置一般情况加只需要配置签名相关信息就好。运行命令行:

calabash-android setup 520c4f587081a6b530a03c3e86c8bfc4.apk -v

然后按照提示分别输入,签名证书的位置,密码和alias。成功之后会看到这样一句提示:

Saved your settings to .calabash_settings. You can edit the settings manually or run this setup script again

当然,你也可以手动的配置文件完成设置。配置文件的地址为:

~/.calabash_settings

4、Calabash Console

Calabash Console对于会写代码的人来说,一种非常方便调试的工具。使用命令:

calabash-android console 520c4f587081a6b530a03c3e86c8bfc4.apk

当然有时就会报错。报错有两种,一种是需要按照本文之前的内容进行Calabash配置。还有一种情况是需要对被测试应用进行重新签名。重新签名的命令:

calabash-android resign 520c4f587081a6b530a03c3e86c8bfc4.apk -v

成功进入Calabash Console之后,还需要运行两个命令行,才能开始传统意义上的自动化测试的调试:

reinstall_apps
start_test_server_in_background

经过以上两条命令,被测试程序会启动,这是可以通过一些Calabash的命令进行自动化测试了。

当然,也会有一些异常情况,例如,如果拿了网易新闻的Android客户端做这个实验,就会发现不会成功。因为网易新闻做了签名验证,当程序的签名被替换掉以后,程序无法正常启动。

Appium中文输入问题的一些探索

Appium输入中文的问题

在使用Appium做手机端的自动化测试时,你可以会遇到输入中文的问题。但是由于Appium是三个自动化测试工具的集合,所以遇到的中文问题也可能会比较难说清楚。Appium支持iOS、Android和FireFoxOS三种操作系统。但是FireFoxOS一般人都不用,所以,文章中它是最后一次露面了。

Appium在iOS端自动化测试底层使用的是官方的UI Autoamtion。在Android端,4.2以上使用的官方的Uiautomator,4.1以下使用的时eBay的selendroid。所以在输入中文的问题上,三个平台理论上都有可能遇到问题。本文之后将重点调研Uiautomator的输入中文的问题。

关于中文输入的的结论:

  • Appium iOS 完全支持
  • Appium Android selendroid 应该支持(需要实践确认,目前没有试过,只是猜测)
  • Appium Android Uiautomator 不支持,应该是硬伤,目前无解

selendroid的调研之后补充。

Appium iOS中文输入调研

首先先来说一下UI Automation。苹果官方的UI Automation在输入时有两种方法:

  • (1)直接使用Element的setValue方法。
  • (2)UI Automation中的 UIAKeyboard对象有一个typeString方法。

以上两个方法在模拟器上都是完美支持中文的。所以不管在iOS上面怎么玩,都是支持中文的。当然两个方式是有区别的。方法(1)简单直接,基本上就是一个简单的set方法。输入时不会触发什么类似textChanged事件。方法(2)需要很多支持,方法二是完全模拟人手工输入过程。苹果对英文以外的输入都做了很好地兼容。

Appium iOS版本的 sendkeys方法,直接对应 UIAKeyboard的typeString方法。由于typeString方法完美支持中文。所以Appium iOS版本也就支持中文了。当然,typeString方法也有缺点。当一个输入框内有内容的时候,typeString的输入方法是添加,所以,如果之前的内容不需要的时候,还需要先清除掉,在进行typeString。在没有特殊要求的情况下,我比较喜欢setValue这个方法,但是遗憾的时Appium不支持直接setValue。

Appium Android Uiautomator中文输入调研

之所以能有这篇文章,主要是因为最近想使用Appium做一些App之间相互跳转的自动化确认测试。一个中文的App,如果不能输入中文基本上就是歇菜。在使用学习工具的时候发现了很多坑,不能输入中文的这个坑最大,最郁闷。所以,把自己的调研方法写出来,供大家参考。

首先 在一切未知的情况下,使用了这个方法进行输入:

input.sendKeys("舌尖上的中国");

发现结果不对,然后尝试了不输入中文:

input.sendKeys("ssssssss");

结果也不对,发现手机上使用的输入法是Google中文输入法。把输入法切换到英文输入法,以后,英文的正确的了,中文的还是有问题。于是Google了一下,找到了一个帖子。上面信息量很大,看起来比较靠谱的是有一个方法,利用JS运行器来直接输入方法,翻译为Java版本以后,代码如下:

Java
1
2
3
    inputDir.put("element",((RemoteWebElement)input).getId());
    inputDir.put("text","舌尖上的中国");
    driver.executeScript("element:setText", inputDir);

这个方法会出现一个没有实现的异常。

好吧,看源码。

在使用Appium 的sendkeys方法是,Client会发送一个http请求到Appium的server。在AppiumServer端的源码中 有一个 rounting 的文件,输入方法会发送这样的路由: rest.post(‘/wd/hub/session/:sessionId?/element/:elementId?/value’, controller.setValue); 然后树藤摸瓜,找到controller的setValue方法。然后一步一步的查找,最终会找到bootstrap项目中的AndroidElement.java文件,其中有setText方法。代码如下:

Java
1
2
3
4
5
6
7
8
9
10
 public boolean setText(final String text) throws UiObjectNotFoundException {
    if (UnicodeEncoder.needsEncoding(text)) {
      Logger.info("Sending Unicode text to element: " + text);
      String encodedText = UnicodeEncoder.encode(text);
      return el.setText(encodedText);
    } else {
      Logger.info("Sending plain text to element: " + text);
      return el.setText(text);
    }
  }

在Appium Android Uiautomator中,输入方法就是这一段代码。从这一段代码中会发现Appium基本没有做什么事情,最终调用了Uiautomator本身的setText方法。所以,接下来需要确认原生的Uiautomator是否可以支持中文输入。 经过简单的测试发现,Uiautomator不支持中文输入,然后继续Google解决方案。发现了一个uiautomator-unicode-input-helper开源项目。直接拿来尝试了中文,也发现不对。按照作者的说法是可以支持日语,但是貌似想支持中文的话,还要进行一定量的开发。

继续看看Uiautomator为什么不支持中文。然后继续找Android的源码,发现Uiautomator的所有的功能都是通过UiAutomatorBridge来完成的。在UiAutomatorBridge类中有一个成员变量类型是InteractionController。输入是通过InteractionController来实现的。具体的setText方法代码如下:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   public boolean More ...sendText(String text) {
        if (DEBUG) {
                    Log.d(LOG_TAG, "sendText (" + text + ")");
        }
        mUiAutomatorBridge.setOperationTime();
        KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());
        if (events != null) {
            for (KeyEvent event2 : events) {
                // We have to change the time of an event before injecting it because
                // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
                // time stamp and the system rejects too old events. Hence, it is
                // possible for an event to become stale before it is injected if it
                // takes too long to inject the preceding ones.
                KeyEvent event = KeyEvent.changeTimeRepeat(event2,
                        SystemClock.uptimeMillis(), 0);
                if (!injectEventSync(event)) {
                    return false;
                }
            }
        }
        return true;
    }

通过这段代码,基本上就明白了,Android的Uiautomator完全没有兼容英文以外的输入法的意思。并且也没有提供直接设置属性的方法。

Appium Android Uiautomator 要支持输入中文还有很长的一段路需要走。伤心了。

《iOS测试指南》勘误一

《iOS测试指南》出版并且上市已经有一个月的时间了。随着该书陆续的与读者见面,一些隐藏着的小错误也慢慢浮出水面。在这里先简单的说明一下,截止到现在笔者所知的一些书内的错误。

Byte 和 bit的区别

Byte和bit是有区别,8个bit是一个Byte。一般说文件大小的时候,单位是Byte。说网络带宽时一般使用bit。在《iOS测试指南》的125页中,在描述几种手机上网带宽时,写错了,网络带宽的地址。

书中原始如图所示,这里应该使用 kb,来表示网络带宽,并不是使用KB。 alt text

非常抱歉,一个简单的疏忽就让网络带宽原地提高了8倍,真可谓差之毫厘,失之千里。

千万不要去Copy代码

我在写《iOS测试指南》的过程中,书稿经过了好几次修改,但是依然还是能发现一些错别字。文字功底已经没有办法挽回了,只能认真的把实例代码写好了。所以,我都很认真的写着示例代码。保证所有的代码都是运行过,并且没有问题的才写进到书中。“代码运行过,不代表没有问题。”这是测试工程师经常拿来教育开发工程师的话,自己也犯错误了。

在书中的第43页,有一段代码中的注释有问题。

alt text

注释写错了,很明显这不是一个笔误。这是一个复制之后没有修改完全的错误。在编写代码的过程中千万不要去Copy代码。没有Copy就没有低级错误。

iOS 7的改动带来的问题

在本书中使用了很多次Recipse程序,Recipse程序是官方文档中用来介绍UI Automation的示例程序。本书的第八章介绍KIF时,也是使用了Recipse程序来完成一些功能测试。当需要删除Recipse程序中的菜单时,系统提供的TableViewCell会有删除确认。但是那个确认的按钮的一些属性有些变化。如图所示。

alt text

图中红框内的属性和iOS 6中的属性已经有所变化。

在 iOS 7.0以上的版本,删除确认按钮的属性是 Delete。但是在iOS 6或者5中,删除确认按钮的属性是 Confirm Deletion for xxx(xxx 是 TableCell的name属性)

所以为了测试能在iOS所有的系统上都能完美的运行,代码可以这样修改:

Objective-C
1
2
3
4
5
6
7
8
//点击删除确认按钮
if ([[[UIDevice currentDevice] systemVersion] floatValue] < 7.0) {
    [tester tapViewWithAccessibilityLabel:@"Confirm Deletion for aaa"
                                   traits:UIAccessibilityTraitButton];
} else {
    [tester tapViewWithAccessibilityLabel:@"Delete"
                                   traits:UIAccessibilityTraitButton];
}

当然,因为属性是很多自动化测试工具都要用到的,所以,大家在iOS6升级到iOS7时都会有这个问题。当你使用的UI Automation的时候,也会遇到这个问题。当然,在UI Automation的时候,还有更大的一个问题。在iOS 7系统中,由于Apple公司的问题,导致UI Automation的所有滑动操作无效。

由于iOS7中滑动操作无效,所以,只能使用别的操作路径来进行删除。

完整的代码如下:

JavaScript
1
2
3
4
5
6
7
8
9
test("删除刚才添加的饺子菜单,通过判断总菜单数量的变化来判断测试是否成功",
    function(){
        oldCellsLength = Finder.findElementByName("Empty list").cells().length
        Finder.findElementByName("Edit").tap();
        Finder.findElementByName("Delete 饺子").tap();
        Finder.findElementByName("饺子").buttons()[0].tap();
        newCellsLength = Finder.findElementsByClassType("TableCell").length
        assertEquals(newCellsLength,oldCellsLength-1);
    }

以上两处需要进行代码更改的地方,都是Apple升级iOS7以后,需要针对iOS7进行的一些容错处理。需要容错的主要由2处:

  1. iOS 7 中 TableCells中自带的删除按钮的Accessbility属性发生了变化。有原来的 “Confirm Deletion for xxx(xxx 是 TableCell的name属性)” 变化为 “Delete”。

  2. UI Autoamtion的所有滑动相关的手势无效。

针对以上的内容需要进行iOS7相关的适配。适配方法请仔细阅读代码。《iOS测试指南》出版的时候,iOS7已经发了,但是未能对iOS7中的内容进行全面的兼容,在此表示深深的歉意。

这些年用过的iOS测试框架

iOS自动化测试一直很神秘,很多人都在探索和找寻最强大的那一款自动化测试工具。个人观点,没有最强大只有最适合,适合以后使用熟练了自然功能强大了。

写在正文之前的几句废话。说到功能自动化测试一般特指基于UI层面的自动化,本文中介绍的自动化测试框架都是UI自动化测试框架。更大概念的自动化测试以后有时间再详细介绍吧。说到UI自动化测试,就会有不同的一些声音。说什么UI自动化测试投入产出比低,UI变化快脚本维护成本高等。其实,这是一个怎么看待UI自动化测试问题。个人认为适量的UI自动化测试投入产出比还是很高的,具体“适量”怎么理解需要看具体的场景。如果你觉得UI自动化一无是处,那么看到这里就可以千万别往下看了。

iOS到底有多少测试框架?大家一直在总结,之前stackoverflow上有一篇神帖总结过不下20种,当然不只是UI自动化测试框架,还包括单元测试框架什么的。(链接找不到了,由于时间比较老了很多工具已经不维护或者很少人使用,不看也罢)

最近发现了一篇Blog The current state of iOS automated functional testing总结了一些UI自动化框架,总结的比较到位推荐给大家。结合自己的使用经验,对该Blog进行一些补充,希望您喜欢。

按照iOS自动化测试框架的实现原理来划分,iOS自动化测试框架大致可以分为两个大类4种类型:

  • UI Automation系
    • 扩展型UI Automation
    • 驱动型UI Automation
  • 非 UI Automation系
    • 私有API型
    • 注入编译型

UI Automation

UI Automation是Apple官方提供的UI自动化测试的解决方法,虽然直接使用非常不爽,但是还不得不说。虽然不好用,但是作为一些工具的底层实现还是需要了解的。官方教程相关API文档 可以方便查阅

扩展型UI Automation

TuneupJs是最早的iOS自动化测试工具,以JavaScript扩展库方法提供了很多好用js工具,最重要的是提供了超简洁的单元测试框架和持续继承解决方案。

ynm3k是笔者维护的iOS自动化测试框架,借鉴或者说抄袭了很多TuneUpJs的想法,在其基础上加入了UI控件定位的很多方法,让测试脚本更加简单便捷。当然之后还有后续的维护计划,过了变态的996以后会开始实施。

如果你偏爱这种类型的测试框架,我真心的推荐ynm3k。因为TuneUpJS已经不维护了,ynm3k还会做一些好玩的功能。最关键的是我觉得ynm3k的UI控件定位识别功能真的很棒。(老王卖瓜了这么长时间还请大家见谅)

驱动型UI Automation

驱动型UI Automation 在自动化测试底层使用了UI Automation库,通过TCP通信的方式驱动UI Automation来完成自动化测试。通过这种方式,编辑脚本的语言不在局限于JavaScript,理论上讲可以是任何一种语言。所以有了iOSDriverAppium.

iOSDriver和Appium还兼容了WebDriver的Json Wire Protocol协议,意味着你可以写WebDriver脚本来完成iOS自动化测试。当然在手机端的一些操作和Web端是不同的,iOSDriver和Appium都扩展了相关的功能。iOSDriver选择了多加入Java语言Jar包的方式支持,而Appium则选择了通过WebDriver注入JavaScript的方式支持。iOSDriver的实现方式决定了只能使用Java语言编辑脚本,而Appium则支持更多的语言。

当然让我真正选择Appium的原因是因为Appium更加轻便易用一些。如果你不经常玩Java,使用iOSDriver的时候一定会在环境设置方面卡很长的时间。

私有API型

直接使用私有API对UI界面进行操作是最简洁有效的自动化测试方式。私有API结合iOS单元测试框架OCUnit的组合完全非常棒,至少对iOS开发工程师来说。在这种类型的测试框架中,KIF是必须介绍的。原因很简单,Google在使用。对于那些对Google无理由崇拜的人们,赶紧用起来吧。

笔者关注了KIF一段时间,起初1.0版本的时候,真心的不好用。当然工具的维护者也意识到了这个方面的问题,推出了KIF的2.0版本,更新了很多的API。在2.0版本时,还不错。如果想做纯UI界面操作的话,不推荐使用KIF。

注入编译型

注入编译型是指在编译时注入一个Server到App内部,通过Server对外通信完成UI操作指令的执行。其中最著名的代表为FrankCalabash. 它们还是BDD测试框架的杰出代表。维护大单位都是全球知名的敏捷咨询公司。喜欢cucumber和ruby的人可以考虑。

大概介绍到这里,之后会陆续进行每种类型的详细介绍。

UI Automation 中触发iOS模拟器内存警告

在UI Automation的api中没有提供模拟内存警告的接口。在Monkey测试的过程中 MemoryWarning还是非常有用的。所以需要想办法自己封一个simulateMemoryWarning接口。

经过调研,决定采用AppleScript来实现功能,然后在UI Automation中使用host.performTaskWithPathArgumentsTimeout调用AppleScript方式来实现。

调试AppleScript。在stackoverflow 中,可以找到OC和AppleScript版本的内存告警实现。其中AppleScript的实现为:

AppleScript simulate memory warning
1
2
3
4
5
6
7
8
9
10
11
tell application "System Events"
  tell process "iPhone Simulator"
      tell menu bar 1
          tell menu bar item "Hardware"
              tell menu "Hardware"
                  click menu item "Simulate Memory Warning"
              end tell
          end tell
      end tell
  end tell
end tell

简单的调试发现不稳定,最好加入一下代码:

AppleScript make sure iphone simulator activate
1
2
3
tell application "iPhone Simulator"
  activate
end tell

之前的脚本只能支持英文版操作系统,为了兼容中文操作系统,再次加入代码:

AppleScript get os language & simulate memory warning
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
on get_language()
  return user locale of (get system info)
end get_language
set menubar to "Hardware"
set menuclick to "Simulate Memory Warning"
if get_language() = "zh_CN" then
  set menubar to "硬件"
  set menuclick to "模拟内存警告"
end if
tell application "iPhone Simulator"
  activate
end tell
tell application "System Events"
  tell process "iPhone Simulator"
      tell menu bar 1
          tell menu bar item menubar
              tell menu menubar
                  click menu item menuclick
              end tell
          end tell
      end tell
  end tell
end tell

保存好文件以后,放入固定的路径,使用UI Automation调用,可以参考一下代码:

JavaScript UI Automation
1
2
3
4
5
6
7
8
9
10
simulateMemoryWarning: function(){
    var target = UIATarget.localTarget();
    var host = target.host();
    var result = host.performTaskWithPathArgumentsTimeout("/usr/bin/osascript", [this.scriptPath+"SMW.scpt"], 5);
    if(result.exitCode==0){
        return 0;
        }else{
         return null;
            }
}

好的,功能就做完了。还算顺利,但是如果你是一个geek的话,会觉得之前的AppleScript中有一段很蛋疼的代码。没错我也觉得蛋疼,并且有些老外也觉得蛋疼,所以有一个现成的函数可以使用:

AppleScript use menu_click function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
on menu_click(mList)
  local appName, topMenu, r
  -- Validate our input
  if mList's length < 3 then error "Menu list is not long enough"
  -- Set these variables for clarity and brevity later on
  set {appName, topMenu} to (items 1 through 2 of mList)
  set r to (items 3 through (mList's length) of mList)
  -- This overly-long line calls the menu_recurse function with
  -- two arguments: r, and a reference to the top-level menu
  tell application "System Events" to my menu_click_recurse(r, ((process appName)'s ¬
      (menu bar 1)'s (menu bar item topMenu)'s (menu topMenu)))
end menu_click
on menu_click_recurse(mList, parentObject)
  local f, r
  -- `f` = first item, `r` = rest of items
  set f to item 1 of mList
  if mList's length > 1 then set r to (items 2 through (mList's length) of mList)
  -- either actually click the menu item, or recurse again
  tell application "System Events"
      if mList's length is 1 then
          click parentObject's menu item f
      else
          my menu_click_recurse(r, (parentObject's (menu item f)'s (menu f)))
      end if
  end tell
end menu_click_recurse
tell application "iPhone Simulator"
  activate
end tell
menu_click({"iPhone Simulator", "硬件", "模拟内存警告"})

大概玩了一下AppleScript,感觉语法方面有点颠覆之前学过的语言,但是如果能熟练了以后,写代码就像说话一下。感兴趣的同学可以试试。以上相关功能代码已经加入ynm3k中。也可以直接使用的。

才开始写Blog

是的,才开始写Blog。之前一直想写,这样可以沉淀一些东西,但是一直没写。

回首工作历程,做测试工程师也10年了,越做越觉得很多东西搞不定,很多问题解决不了。当然,写了Blog也解决不了。

慢慢来吧,一切都会好起来的(深表怀疑 ^_^ )。

本Blog为了标新立异,会遵循以下原则:

  1. 实践。有很多实践需要沉淀总结,希望能自己完成积累。可能有很多实践内容有雷同嫌疑,但是可以保证都亲手做过,可以实现。
  2. 吐槽困惑。测试方面存在着太多的槽点,可以吐很多很多。在吐槽时,我会尽可能的保证公正,尽量多的说事实和现象,尽量少的表达观点,写给那些聪明的读者。