Selenium中被误用的XPath

用Selenium实现自动化测试的过程中,如果选择页面上的元素并且对之进行各种操作,是一个常见的任务。Selenium提供了多种定位方法:

  • id:最有效、最方便的方法
  • name:跟id类似的
  • class name:对某些具有相同类的元素一网打尽的好方法
  • link text 和 partial link text: 用在定位超链接上比较多
  • tag name:与class name有点类似
  • css selector:如果你试用jQuery,这个一定是你喜欢的方法
  • xpath:。。。 /html/body/div/div[2]/div[2]/div[2]/div[5]/div/p[2]

网上很多Selenium的介绍文章,在讲述如何利用XPath定位元素的时候,通常都是这样子说的“打开Firefox浏览器,安装Firebug插件,然后就能方便地获得该元素的XPath了”。由于不求甚解,在一段时间内我真以为这些看起来没什么意义,中间穿插着各种数组操作,读起来反人类反社会的所谓XPath就是真的XPath,同志们大家都被误导了。

什么是XPath:http://www.w3.org/TR/xpath/
XPath基础教程:http://www.w3schools.com/xpath/default.asp

XPath在Selenium测试中有好些缺点:1. 性能差,定位元素的性能比起大多数其他方法要差;2. 不够健壮,XPath会随着页面元素布局的改变而改变;3. 兼容性不好,在不同的浏览器下对XPath的实现是不一样的。如此多的弱点,为什么它还存在于Selenium中呢?Selenium提供了这7个元素定位的工具,就好像工具箱里面有锤子有老虎钳有螺丝刀,每个工具都能完成特定的任务,前提是要在正确的前提下,正确地使用。

XPath通常会在如下场景:一个写自动化测试的人,发现他想要操作的元素不能通过id, name, link text等比较方便有效的方法来进行定位,苦逼的他没能说服开发这个页面的人把他想要的id加上,他开始用所谓的XPath来定位元素,代码中充满了各种让人摸不着头脑的XPath(/html/body/div/div[3]/div[2]/div[4]/p[2]),在我看来这样的代码跟录制出来的脚本没有任何区别。可读性差,几乎不能维护。XPath理论上可以这样使用,但是实际上应该避免这样的使用。

XPath的一些优点是大家需要知道的,例如:1. XPath可以通过某个元素找到它的祖先(Ancestors);2. 可以做布尔逻辑判断,例如/button[@value=’submit’ or @name=’tijiao’]

回到上面的场景,假如说那个苦逼的人想定位到页面上的一个提交按钮,这个按钮不能通过id或者name来定位。这个时候他要做的事情不是打开Firebug定位提交按钮右击鼠标再点“Copy XPath”。而是应该是找开发把id或者name加上。如果不行,解决思路可以是:1. 找到该按钮的特征,例如按钮的文字是 submit;2. 用XPath定位,可以这样写://button[@value=’submit’]。

我个人对使用XPath比较反感的,如果可能的话,尽可能使用id或者name。真的要用XPath,千万千万不要打开Firebug定位提交按钮右击鼠标再点“Copy XPath”。先认真学习XPath,后使用。在很长一段时间里面,我对XPath真的是恨之入骨,恨不得先杀之而后快,但是想到存在就是合理,那么多大牛们都没有把XPath摒弃与Selenium之外,XPath必然有它的价值。最近花了点时间学习了一下XPath,并且读了一些关于如何在Selenium里面正确使用XPath的文章,豁然开朗。

参考文章:

WebDriver配置Firefox代理服务器

这玩意儿网上很多,但是坑更多,现在记录一个肯定能用的。

我的环境是Python 2.6 + Selenium 2.6

from selenium import webdriver
profile = webdriver.FirefoxProfile()
profile.set_preference('network.proxy.type', 1)
profile.set_preference('network.proxy.http', 'proxy_url')
profile.set_preference('network.proxy.http_port', 3128)
profile.set_preference('network.proxy.ssl', 'proxy_url')
profile.set_preference('network.proxy.ssl_port', 3128)
profile.update_preferences()
driver = webdriver.Firefox(profile)

这些坑分别是:

有些地方只告诉你配置network.proxy.http和network.proxy.http_port。但是如果不设置network.proxy.type,一切都是浮云。这个配置是个整数,默认是0,就是直接连接;1就是手工配置代理。
profile.set_preference(‘network.proxy.type’, 1)

那个端口号3128是整数
profile.set_preference(‘network.proxy.http’, ‘proxy_url’)
profile.set_preference(‘network.proxy.http_port’, 3128)

如果有些资源是https的,是需要另外配置network.proxy.ssl和network.proxy.ssl_port的。例如facebook的图片。。。

set完以后,是需要update_preferences的。。。
profile.update_preferences()

最后贴一个完整的Firefox配置参数表:http://kb.mozillazine.org/Firefox_:_FAQs_:_About:config_Entries

如果发现自己的配置好像没有生效,那么在webdriver启动的Firefox里面输入about:config。然后对着上面那个配置参数表来看。总会发现掉哪个坑的。

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

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把自动化测试自动化起来。