Hudson + WebDriver 组织自动化测试

之前介绍过如何使用TestNG来驱动WebDriver用Ant来自动化运行测试。今天分享一下如何把这些东西都放到HudsonJenkins也行)里面呢?

Hudson是一个比较流行都持续集成工具。用Hudson来驱动自动化测试的好处有以下这些:

  • 类似crontab的自动任务管理
  • 丰富的插件支持
  • 支持分布式任务
  • 容易部署

其实整个过程很简单,把Hudson跑起来,新建一个Job,配置一下Ant任务就好了。这里只分享一下我遇到的一些坑。

是否使用Source Code Management获取最新的测试代码?

如果每次测试都拉最新的代码,好处就是保证测试代码是最新的。但是也会带来一些问题,测试代码本身也是代码,怎么保证最新的测试代码没有问题呢?我个人认为,如果团队比较小,可以直接拉最新的代码;如果团队大,需要控制。

如果不用SCM插件,怎么样更新自动化测试代码?

我想到的一种办法就是,在Hudson里面建立一个构建自动化测试代码的Job,这个Job的产物就是自动化测试的包,譬如说如果用WebDriver或者Selenium,就把测试代码build成一个或者若干个jar包,然后建立一个latest的软链接指向最新的jar包;在运行自动化测试的Job里面做好配置,运行测试的目标jar包就指向latest.jar就OK了。

TestNG的结果如何跟Hudson整合

Hudson插件很多,可以用testng-plugin来完成这个任务。配置比较简单,在Ant脚本里面配置好TestNG的result output,然后在Hudson里面把测试报告的模式填好。我直接填的TestNG的默认结果文件“testng-results.xml”。build.xml节点配置的一个例子:

    
        
            
        
        
    

首先在testng节点指定outputdir属性,然后测试运行完成以后把结果文件移动到Hudson的workspace

怎么样把Ant的参数传递给TestNG

很多时候我们会希望通过ant把一些参数传递给testng.xml,从而使得测试更加灵活。例如传递不用的base_url可以测试不同的站点。还有配置不同的浏览器。虽然之前这篇文章已经介绍了如何把Ant的参数传递给TestNG,但是那个方法有个缺点,如果在测试方法A里面调用了测试方法B,测试方法B是不能拿到Ant传进去的参数的。我的办法比较土,就是首先写好一个testng_base.xml的模板文件,把一些可能经常改变的数值替换成参数,然后用Ant的replace任务做字符串替换。

    
        
        
    

WebDriver测试失败后自动获取截图

UI自动化测试其实并不是那么稳定,可能是因为UI元素的改动,也可能是因为网络的不稳定,在测试失败的时候,WebDriver通常会抛出一些异常;通过异常信息通常都能知道大概是哪里出错了,但是如果能加上截屏,那就更加好了。尤其是用Remote WebDriver运行测试,所有测试都是通过Selenium Grid分发到各个节点来运行,不同节点的配置还有可能不是完全一样。

如果是使用RemoteWebDriver的话,它提供了一个很好的功能,就是会把运行测试发生异常时候的截图也放到异常里面,具体可以参考RemoteWebDriver的简介。代码很简单:

public String extractScreenShot(WebDriverException e) {
    Throwable cause = e.getCause();
    if (cause instanceof ScreenshotException) {
        return ((ScreenshotException) cause).getBase64EncodedScreenshot();
    }
    return null;
}

怎么样才能在异常发生的时候自动把异常抓住呢?简单来说就是要对写测试代码的人是透明了,在写测试代码的时候不需要特别去处理异常。这里需要实现WebDriverEventListener接口,然后把RemoteWebDriver对象和实现WebDriverEventListener接口的对象包到一起,实例化一个EventFiringWebDriver对象。之后的事情就跟用一个普通的RemoteWebDriver对象没有任何区别。

实现WebDriverEventListener接口的一个例子:

public class MyEventListener implements WebDriverEventListener {
    public void onException(Throwable ex, WebDriver arg1) {
        String filename = generateRandomFilename(ex);
        try {
            byte[] btDataFile = Base64.decodeBase64(extractScreenShot(ex).getBytes());
            File of = new File(filename);
            FileOutputStream osf = new FileOutputStream(of);
            osf.write(btDataFile);
            osf.flush();
            osf.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String generateRandomFilename(Throwable ex) {
        Calendar c = Calendar.getInstance();
        String filename = ex.getMessage();
        int i = filename.indexOf('\n');
        filename = filename.substring(0, i).replaceAll("\\s", "_")
                    .replaceAll(":", "")
                    + ".png";
                    filename = "" + c.get(Calendar.YEAR) + "-" + c.get(Calendar.MONTH)
                    + "-" + c.get(Calendar.DAY_OF_MONTH) + "-"
                    + c.get(Calendar.HOUR_OF_DAY) + "-" + c.get(Calendar.MINUTE)
                    + "-" + c.get(Calendar.SECOND) + "-" + filename;
        return filename;
    }

    private String extractScreenShot(Throwable ex) {
        Throwable cause = ex.getCause();
        if (cause instanceof ScreenshotException) {
            return ((ScreenshotException) cause).getBase64EncodedScreenshot();
        }
        return null;
    }

    @Override
    public void afterChangeValueOf(WebElement arg0, WebDriver arg1) {
    // TODO Auto-generated method stub

    }
}

实例化一个EventFiringWebDriver对象:

@Test
public void setup(){
    String remote_driver_url = "http://localhost:4444/wd/hub";
    DesiredCapabilities capability = null;
    capability = DesiredCapabilities.firefox();
    WebDriverEventListener eventListener = new MyEventListener();
    WebDriver driver = new EventFiringWebDriver(new RemoteWebDriver(new URL(
                    remote_driver_url), capability)).register(eventListener);
}

之后如果测试遇到任何异常,都会在basedir(这个是要自己配置的)下面生成一个类似这样的png截图(2011-7-22-20-55-9-Element_is_not_currently_visible_and_so_may_not_be_interacted_with.png)。

这些代码段演示了如何使用WebDriverEventListener接口以及EventFiringWebDriver类,实现监听测试中所抛出的异常,并且把异常里面附带的截图保存为png文件。

参考博客:Generating a screen capture on exception thrown with Selenium 2

TestNG+Ant自动运行测试

之前一篇博客分享了如何使用WebDriver+TestNG实现UI自动化,现在就要让自动化测试自己跑起来,不需要人工干预。需要用到Ant,以及一些定时任务工具,例如Linux的crontab。

如何在Ant的build.xml里面正确配置TestNG呢?

1. 在build.xml里面定义testng任务,在classpath里面指定testng.jar


2. 在build.xml里面新建一个 叫regression的target


    
    
    
    

    

    
        
    
    
        
        
    

    
        
        
        
    

    
        
            
        
    

在target里面新建一个testng标签,里面需要设置的属性有:outputdir – 测试结果输出目录;classpathref – 那些自动化测试代码的目标路径,通常就是编译完成以后的那个目标路径,例如xxx/bin;delegateCommandSystemProperties – 接受传递命令行参数作为系统变量,这个设置为true可以在调用Ant的时候通过 -Dfoo=value 把参数传递给TestNG;里面还有一个xmlfileset节点,这个节点就是指定testng.xml文件的目录以及具体文件。

regression 的 target 有一个depends属性,意思就是跑regression之前需要做compile,而跑compile之前需要clean,应该很容易理解。直接在命令行里面运行:

ant -Durl=http://www.google.com -f build.xml regression

这里出现了 -Durl=http://www.google.com ,回到之前的配置,delegateCommandSystemProperties=”true”。如果这个参数为true,那么通过命令行的 -D 参数可以把一些变量传递给TestNG。譬如说TestNG的测试方法里面是有@Parameters({“url”})标签的话,就能通过ant -Durl=xxx 来传递url的值给到TestNG。例如

@Parameters({"url"})
@Test
public void search(String url){
    WebDriver driver = new FirefoxDriver();
    driver.get(url);
    WebElement query = driver.findElement(By.name("q"));
    query.sendKeys("Cheese");
    query.submit();
}

如果这样调用:ant -Durl=http://www.google.com -f build regression 。那么就会进入google的首页搜索,如果是: ant -Durl=http://magustest.com -f build regression ,那么就会找不到叫“q”的元素,呵呵。

接下来只要把cron job配好就完成了

15 * * * * ant -f /home/maguschen/workspaces/automation/build.xml regression

WebDriver + TestNG 应用

Selenium 2 已经发布了一个多月,官方版本已经到了Selenium 2.3,并且在Google code里面可以找到2.4的下载。Selenium 2 最大的更新就是集成了WebDriver。这两者是什么关系呢?如果你搜索WebDriver,第一条结果是Selenium。其实WebDriver和Selenium可以说是在实现UI Automation的竞争对手。Selenium是运行在JavaScript的sandbox里面,所以很容易就支持不同的浏览器;而WebDriver则是直接操作浏览器本身,更接近用户的真实操作,但正因为如此,所以WebDriver在多浏览器/操作系统的支持上就要落后于Selenium。不过从Selenium 2开始,这两个项目合并了,可以继续用原来的Selenium,也可以考虑迁移到WebDriver。我个人认为WebDriver应该是以后的大趋势,还是值得迁移的。至于你信不信,我反正是信了。

作为一个轻量级的UI Automation框架,需要写一些驱动它的代码,大部分人会选择JUnit,因为JUnit是单元测试的事实标准;但是我会用TestNG。这些UI Automation的东西,它们本身不是单元测试,而且也没有太多单元测试的风格。

从一段简单的测试开始

public class GoogleTest  {
    @Test
    public void search(ITestContext context) {
        WebDriver driver = new FirefoxDriver();

        driver.get("http://www.google.com");

        WebElement element = driver.findElement(By.name("q"));

        element.sendKeys("magus");
        element.submit();

        Assert.assertTrue(driver.getTitle().contains("magus"), "Something wrong with title");
    }
}

TestNG应用了Java的Annotations,只需要在测试方法上面打上@Test就可以标示出search是一个测试方法。用TestNG运行测试还需要一个testng.xml的文件,文件名其实可以随便起,没有关系的。


    
        
            
                
                    
                
            
        
    


我想让测试更加灵活,1. 可以配置使用任意支持的浏览器进行测试;2. 配置所有Google的URL;3. 配置搜索的关键字。修改后的代码:

public class GoogleTest  {
    WebDriver driver;

    @Parameters({"browser"})
    @BeforeTest
    public void setupBrowser(String browser){
        if (browser.equals("firefox")){
            driver = new FirefoxDriver();
        } else {
            driver = new ChromeDriver();
        }
    }

    @Parameters({ "url", "keyword" })
    @Test
    public void search(String url, String keyword, ITestContext context) {        driver.get(url);
        WebElement element = driver.findElement(By.name("q"));
        element.sendKeys(keyword);
        element.submit();
        Assert.assertTrue(driver.getTitle().contains(keyword), "Something wrong with title");        }
}

testng.xml


    
    
    
    
        
            
                
                    
                    
                
            
        
    

利用TestNG的@Parameters标签,让测试方法从testng.xml里面读取参数,实现参数化。在testng.xml的配置中,test节点需要增加一个属性的配置: preserve-order=”true”。这个preserve-order默认是false,在节点下面的所有方法的执行顺序是无序的。把它设为true以后就能保证在节点下的方法是按照顺序执行的。TestNG的这个功能可以方便我们在testng.xml里面拼装测试。假设我们有很多独立的测试方法,例如

  • navigateCategory
  • addComment
  • addFriend
  • login
  • logout

就可以在testng.xml里面拼出不同的测试,例如


    
        
            
                
                
                
            
        
    


    
        
                            
                
                
                
            
        
    

TestNG比JUnit更加适合做一些非单元测试的事情,不是说JUnit不好,而是不能把JUnit当成万能的锤子,到处钉钉子。WebDriver的API比Selenium的更加简洁,会是以后的大趋势。

之后打算分享一下如何用ant把自动化测试自动化起来。

代码覆盖工具gcov, lcov的一些使用经验

使用gcov和lcov做代码覆盖有一段时间了,期间走了一些弯路,算是一些经验教训。

  • gcov可以对shared object进行代码覆盖信息收集。

之前的一篇文章说过,gcov不能收集.so的代码覆盖率信息,其实这个是错的。

如果是C++程序,在CXXFLAGS中加入-fprofile-arcs -ftest-coverage 作为编译选项,并且在LDFLAGS中加入-lgcov作为链接选项。如果没有-lgcov选项编译出来的.so文件在动态加载的时候会提示类似 undefined reference to ‘__gcov_merge_add’ 或者 undefined reference to ‘__gcov_init’这样的错误。

如果是C程序,在CFLAGS中加入-fprofile-arcs -ftest-coverage 作为编译选项,也是在LDFLAGS中加入-lgcov作为链接选项,就OK了。跑测试的时候跟正常测试一样,最后把收集到的.gcda文件处理一下就能得到代码覆盖率报告

  • 用lcov处理.gcda文件的时候,在处理某些文件的时候hang了。

对于这个问题,不知道根本原因是什么,可能是那个文件的某行代码触发了bug,我对此是workaround了一下。lcov本身是一堆Perl脚本,打开它,通常在这里“/usr/bin/geninfo”,然后找到这行,注释掉

push(@gcov_options, “-a”) if ($gcov_caps->{‘all-blocks’});

  • genhtml的一个参数:-f, –frames

Use HTML frames for source code view。这样会在每个结果页的左边生成一个缩略图,review代码的时候很方便。但是,如果你的项目里面用到了一些第三方的库,而那些第三方的库没有提供完整的源代码,那么使用这个参数就会出现问题。

gd-png: fatal libpng error: Image width or height is zero in IHDR
gd-png error: setjmp returns error condition

解决方法可以是1. 尝试找一下源代码;2. 通常这些第三方库,我们都是放在一个单独的文件夹里面的,这样子用lcov处理.gcda文件的时候,可以指定目录,把这些第三方库的目录跳过去

  • genhtml的一个参数:–num-spaces NUM

默认值是8,出来的报告看着很费劲,建议换成4,如果代码有很多层,那设成2也是可以的

TestLink和BugZilla集成中的一些问题

前两天有个朋友写信给我问一个testlink和bugzilla集成的问题,从他信里面的描述得出,他已经成功把这两个系统集成好了,但是有一些功能用不了,例如不能显示bugzilla里面的id、状态、标题信息等。其实原因是testlink的作者只实现了bugzilla集成的部分接口,其他的接口是要我们自己来写的。大家可以浏览一下testlink安装目录下的/lib/bugtracking,里面有好些文件,其中文件int_bugtracking.php是testlink和其他所有bug tracking系统(bugzilla, jira, mantis等)做集成的基类。还有一个文件叫int_bugzilla.php,这个文件就是testlink和bugzilla集成的代码,里面定义了一个类:bugzillaInterface,它是bugtrackingInterface的子类,并且在类bugzillaInterface里面重写了一部分方法,这也是为什么如果我们自己不修改代码的话,只能用到部分功能(例如只能连接,但却不能显示相关的信息)。

假如说,现在想在testlink关联bugzilla的一个bug之前,验证一下bug id是否存在,就要在int_bugzilla.php里面重写checkBugID_existence方法。

$query = "SELECT bug_id FROM {$this->dbSchema}.bugs WHERE bug_id='" . $id."'";
$query_results = $this->dbConnection->exec_query($query);
if ($query_results && ($this->dbConnection->num_rows($query_results) == 1))
{
    return true;
}
return false;

如果要自己补充这个int_bugzilla.php的时候,需要一点php的知识,并且对bugzilla数据库有所了解,以前公司的同事告诉我用php的一个神器,vardump。你懂的。

用lcov生成diff代码覆盖率报告

lcov是建立在gcov之上的一个可以生成html代码覆盖率报告的工具,最近公司开始尝试引入代码覆盖来提高产品质量,lcov很好地满足了我们的需求,虽然lcov本身支持生成代码覆盖率的diff报告,但是跟我们的需求不太符合。

首先说一下我们的情况,我们有一套自动化回归测试集,可以看做是我们测试的全集。现在已经完成了基于这个回归测试集的代码覆盖率报告,这其中肯定有某些行没有被覆盖到的,如何评估这些没有被测试过的行的风险呢?最开始是DEV跟QA一起review,由开发来评估这些没有被测试过的代码。最近提出了一个新的思路,就是用Production的代码覆盖和本地回归测试的代码覆盖做一个diff,着重看一下那些在Production里面实际被执行过的代码中,有哪些是我们本地回归测试所没有覆盖的,因为这些“裸奔”的代码才是最危险的代码。

查一下文档,lcov在生成html报告的时候可以做这个事情,在genhtml的时候使用参数“–baseline-file baseline-file”,指定了这个参数以后就会在用输入的tracefile里面的counter来减去baseline-file里面的counter,完成这个减法计算以后再生成报告。可以用Local测试的数据作为basefile,两者一减,在报告里面那些cover的行,就是Production上跑到的代码而回归测试集没有能覆盖的部分。但是如果直接用的话,结果可能不是我们想要的,因为我用了genhtml这样的一个特性:“Note that when a count for a particular line in baseline-file is greater than the count in the tracefile, the result is zero.”。

举个例子,如果我们的代码被执行了若干次:

Code A B C D
Local 0 40 50 60
Production 50 50 50 50
Actual result 50 (local uncover) 10 (local uncover) 0 (covered) 0 (covered)
Expected local uncover covered covered covered

主要看第三列,B这行代码。如果在Local测试中某行代码被执行了40次,而同一行代码在Production被执行了50次,那么diff出来的结果是这行代码没有被覆盖,而实际的结果是回归测试已经覆盖到了。解决思路很简单,我们可以让Local的counter增加N倍,这个N要足够大,就能避免这种情况的发生了。当我们按照正常操作生成一个tracefile的时候,接下来就用“–add-tracefile tracefile” 来把这个tracefile的counter加上去,

lcov -a mytest.info -a mytest.info -o mytest_2x.info

lcov -a mytest_2x.info -a mytest_2x.info -o mytest_4x.info

写个shell脚本帮你干这个事情,不到半小时就能把counter增加2的N次方倍。最后以这个放大了counter的tracefile作为基线,生成的diff report

genhtml -o diffresult –num-spaces 4 –legend -b mytest_1024x.info prod.info

最终结果:

Code A B C D
Local 0 40960 51200 61440
Production 50 50 50 50
Actual result 50 (local uncover) 0 (covered) 0 (covered) 0 (covered)
Expected local uncover covered covered covered

什么样的代码最危险?没有被测试过的代码是最危险的。Production和regression test的代码覆盖率diff报告可以给我们提供一些有针对性的信息。

2010年终总结 新年展望

时间过得很快,转眼又一年过去了,在公司已经一年半了。看了一下去年写的总结,真简短,对2010的展望居然只有一句话

明年工作上主要还是集中在数据库相关技术,自动化回归测试框架,Python应该是主要的编程语言。

那先回顾一下吧,今年我的确围绕着数据库开展工作,我主要测试的是一个原始的OLAP数据仓库,星型结构,若干fact表加若干dimension表,做的比较多的工作有:如果数据重新load,验证。如果数据需要修复,验证。客户需要新的数据,测试。性能优化,验证。另外还有若干和数据库相关的工作,省略之。

自动化回归测试框架,我们CORE有3个产品,每个产品都有自己的regression,总得来说都是大同小异,工作原理就是用python脚本去控制自己的程序,然后各自进行一些操作,获取一些结果,进行比较,展示结果。在5、6月份的时候首先把Adserver的regression界面重写了一次,从原先的很山寨变成了现在的山寨,看起来大家对这个山寨的工具还比较满意;之后在11月的时候把ETL的regression用同样的方法重写了一次,当然,当中有不少改进。中间还搞了不少零零散散的regression。抓住主要的思想,快速持续观察产品状况。

Python,果然今年用了很多,主要用来完成一些自动化的工作,web.py写regression的UI。

综上所述,一年前的预测还是相当的准,并且,有挺多预测外的收获。由于需要写点UI,所以摆弄了一下HTML和JS。把regression挪到数据库上进行管理,学了点数据库的东西。

明年,明年干点啥呢?

首先当然进一步巩固现在的自动化成果,并且尽可能把现在还没有覆盖的点或者面给补上。单机执行已经可能在不久的将来会成为瓶颈,明年可能会考虑一些利用硬件虚拟化,并行,来提高效率的工作。今年我帮大家提高了不少效率,明年我希望让大家自己帮自己提高效率。来年工作肯定还是围绕数据库,MySQL肯定是必须的,之后可能会接触到一些基于列存储的数据库,这应该是以后的方向。Python依然是主要的工具,本来想学习一下Ruby,现在看来优先级很低,随缘。测试技术方面的尝试应该有Exploratory Testing和Model Based Testing这样相对前沿的基础,还有Code Coverage这样成熟的技术。

在web.py中处理表单中的多选下拉框

web.py是一个轻量级的web framework,源代码只有7000多行(我自己用wc -l计算,肯定偏多了),用它来做个简单的站点很方便,上手容易。我在公司就用web.py做了我们自己的自动化测试管理工具,主要是管理自动化用例,展示测试结果。最近遇到一个问题,我想做一个多选下拉框,但是等我做完以后发现结果跟我预想的不太一样。

我用的是web.py的web.form.Dropdown类来生成一个多选下拉框。只需在初始化dropdown的时候传一个multiple=”参数就OK了

form.Dropdown(‘adserver_id’, description=’Adserver: ‘, args=ADSERVERS, multiple=”)

这样生成的html就是



不过当我选中多个选项提交表单的时候,我在服务端得到的结果却是最后一个选中的选项,例如我选了Coretest2 – 1和Coretest3 – 1,那么我在服务端拿到的数据就只有Coretest3 – 1。用Firebug抓了一下请求看,原来POST的数据是长这个样子的:

adserver_id: 1
adserver_id: 2

在web.py中,一般是通过web.input()来获取表单提交的数据。而web.input()会把所有提交的数据给storify方法处理一下(可以更容易的访问dict的数据,例如原来是mydict[‘name’],可以写成mydict.name),而这个方法默认会把adserver_id处理成唯一的key,所以无论输入是多少个adserver_id,最终拿到的只有1个。解决方法很简单,用这个方法来获取post的数据web.input(adserver_id=[]),这样提交adserver_id的多个数据就会被保存到一个列表里面了。

但是这时候会遇到另一个问题,就是如果想要更改这个case的时候,就没有办法在web.py里面用 value=xxx 来指定哪些options是应该被选中的。value=xxx只能指定一个option,怎么办?我是用了JavaScript来解决。思路很简单,就是页面Load完以后,JavaScript获取一下哪些options应该被选中,然后就选上那些项,OK

rework

<rework>,中文名《重来》,一本最近比较火的书,周四拿到书,今天刚过了一遍,真是一本好书,尤其推荐在startup公司的朋友们读读。肯定会让你全身热血沸腾。下面是一些阅读体会:

  • Planning is guessing – 一个字:干!回头想想,以前曾经有过很多“计划”,最后都是一句“计划赶不上变化啊”就丢到一边了,别计划了,开干吧。就好像那个adserver regression UI那样,不需要计划,想到什么就做什么。
  • Scratch your own itch – 只有你自己才知道你需要什么样的工具来改善自己的工作,所以我建议AF QA的同事,别在原来adserver那个工具上改,做一个真能解决问题的regression
  • Start making something – 其实web.py看一天就可以开始动手做东西了,咱们又不是要应付考试
  • Ignore the detail early on – 之前就是犯过一个这样的错误,想做一个很酷的功能,折腾了1天,都没做出来,最后用一个朴实的方案,2个小时就OK了,而且据我观察,其实这个“功能”大家用的并不多
  • Launch now!
  • Meeting are toxic – 现在在公司还好,大家开会都很理性 : )
  • Good enough is fine – 这个东西是有点山寨,但是我们也不需要show给客户看嘛,呵呵
  • Go to sleep – 前段时间老加班,效率真的降低不少,工作时间的延长并不代表能完成更多的任务
  • Don’t copy – 我的成功可以复制,但是不能粘贴 – [段子]
  • Send people home at 5 – 赞成,但是不太现实

书中还有其他很多很好的观点,没能一一列出,如果不想买纸版书的朋友,可以找找哪里有PDF下,这本书是http://37signals.com/出品,大家可能不知道这个公司是干嘛的,没关系,Ruby on Rails听过吧?作者就在这个公司。