• 利用客户端Javascript代码简化Selenium测试 - [Test]

    2008-12-23

    这是一个基于JSF的WEB应用,被测的场景是编辑一个巨大的表单,然后进行预览操作,最后再回到编辑页面,验证各个字段是否和以前的一样,据此间接检验各个页面元素与后台Bean的绑定是否正确。

    以前的测试不是人读的

    下面是之前实现这一测试的代码片段:

    @Test
    public void createFullRequest_priview_backEditing_checkData(){
    goToCreationPage();
    browser.click(ADD_APPOINTMENT);
    browser.click("lorryTrucking1");
    browser.select("containerSizeType1_1", "label=20' Platform");
    typeAndBlur("containerQuantity1_1", "1");
    browser.select("cargoPackageType1_1", "label=Bar");
    browser.click("containerTrucking1");
    browser.type("containerQuantity1_1", "1");
    browser.click("containerIsSOC1_1");
    browser.select("containerSizeType1_2", "label=20' x 8' x 8' 6 Hanger Container");
    browser.type("containerQuantity1_2","1");

    // omitted 100+ lines.
    }

    这样的测试有几个问题:

    • 可读性差。
    • 为重用而提取了很多辅助函数,这带来了较深的函数调用,页面上被设置了那些数据不一目了然。
    • 对一些动态生成页面元素,是通过复杂的XPath设值的,这对实现细节有很重的依赖。

     
    期望这样写测试

    对于上面的要求,这样的测试是最自然的:

    @Test
    public void test_some_scenario() {
    BookingRequestState state = new BookingRequestState();
    state.bookingOffice = "BJI";
    state.carrierBL = true;
    state.numberOfOriginalBL = "1";
    state.addContainer(new ContainerState("200", "42RE", "100", true, "some other description"));
    state.addCargo(new CargoState("marks1", "10", "AR", "description1", "11", "12","1"));
    state.addCargo(new CargoState("marks2", "20", "ASN", "description2", "21", "22","2"));
    // omitted 30+ lines, could be moved into test data fixture class.

    new CreationPage(browser)
    .populate(state)
    .gotoPreviewPage()
    .gotoCreationPage()
    .assertStateConsistentWith(state);
    }

    前一部分是在准备要Populate的数据,后面反应了测试的流程,最后拿当时Creation Page的State与最初的State进行比较。在原有测试基础上是可以重构到这个地步的,但我们意识到,

     
    客户端已经有了Populate页面的便捷方法

    实际上,系统中已经有很方便的方法来为页面填数据了。具体就是先构建一个Javascript的Object Literal,这个对象代表了当前Web页面所编辑的Model或State:

    var state = {
    bookingOffice : "BJI",
    carrierBL : true,
    numberOfOriginalBL : "1",
    containers : [{ quantity : "200", sizeType : "42RE", cargoWeight : "100",
    shipperOwned : true, cargoDescription : "some other description"}],
    cargos : [{ marks : "marks1", quantity : "10", packageType : "AR" ,
    description : "description1", grossWeight: "11", volume : "12"},
    { marks : "marks2", quantity : "20", packageType : "ASN",
    description : "description2", grossWeight: "21", volume : "22"}],
    // omitted 30+ lines.
    };

    调用代表当前页面的Javascript对象的setState方法,将整个页面Populate出来。这里对这一做法有所介绍。

        creationPage.setState(state);

    其中creationPage对象也是StatefulObject,是一个全局对象,它的setState方法要比这里介绍的复杂,因为各个页面元素都可能对应着一些事件,只有正确调用了这些事件,才能保证得到用户想要的结果,否则就可能出现给Textbox赋值了,但它可能还是禁止编辑的状态。下面是setState方法的片段(其中用到了jQuery):

         var jQueryObj = $(domObj);
    var valueChanged = false;
    if(domObj.type === "checkbox"){
    if(domObj.checked !== value){
    jQueryObj.click();
    valueChanged = true;
    }
    }
    else if(domObj.type === "radio"){
    if(value === true){
    jQueryObj.click();
    valueChanged = true;
    }
    }
    else if(domObj.tagName === "SELECT"){
    if(domObj.value !== value){
    domObj.value = value;
    jQueryObj.change();
    valueChanged = true;
    }
    }
    else if(domObj.value !== value){
    domObj.value = value;
    valueChanged = true;
    }

    if(valueChanged){
    jQueryObj.blur();
    }

    这里在设置值的同时处理了各种事件,确保行为完整。

    既然可以这么简单填写页面,就没有利用再在Java代码里面费劲地type,select了。我们可以在Java里面构建出一个Object Literal对象字符串,然后通过下面的调用来填写页面:

        String state = "{name1:value1, name2:value2}";
    browser.runScript("window.creationPage.setState(" + state + ");");

     
    在Java里建立对应的State对象

    public class BookingRequestState{
    public String bookingOffice;
    public Boolean carrierBL;
    public String numberOfOriginalBL;
    public List<ContainerModel> containers = new ArrayList<ContainerModel>();
    public List<CargoModel> cargos = new ArrayList<CargoModel>();
    // omitted other fields.

    public void addContainer(ContainerState container) {
    containers.add(container);
    }

    // return object literal used in javascript
    public String getStateObject() {
    StringBuilder b = new StringBuilder("{ ");
    if(bookingOffice != null) { b.append("bookingOffice:'" + bookingOffice + "',"); };
    if(carrierBL != null) { b.append("carrierBL:" + carrierBL + ","); };
    if(numberOfOriginalBL != null) { b.append("numberOfOriginalBL:'" + numberOfOriginalBL + "',"); };

    if(containers.size() > 0){
    b.append("containers:[ ");
    for(int i=0;i<containers.size();i++){
    b.append(containers.get(i).getStateObject());
    b.append(",");
    }
    b.deleteCharAt(b.toString().length() - 1);
    b.append("],");
    }
    // omitted many lines.
    return b.toString();
    }
    // omitted other methods.
    }

    这一对象封装了页面或页面的一部分所编辑的State,还可以通过getStateObject方法生成一个Javascript Object Literal字符串,这一字符串被用来Populate页面。前面“期望这样写测试”一节中便用这个对象来创建页面。

     
    通过Javascript完成验证逻辑

    上面提到过,这个测试是要到预览页面后再回到编辑页面,再检查页面所有元素都是以前的值。这就要拿到当前页面的State。StatefulObject有getState方法,返回一个Object,代表页面的State,我们可以拿这个State与我们期望的State做比较,来完成测试中assertion逻辑。其实大部分的测试都是在比较页面上的状态与期望的是否一致,所以这个做法有普适性。

    这个比较发生在Javascript里,但要在junit里拿到其结果,我们设计一个Javascript函数实现这一比较:

    AssertFixture = {
    assertStateConsistentWith : function(expected, actual){
    this.result = null;
    for(var p in expected){
    if(actual[p] != expected[p]){
    this.result = p + " does not match. expected:<" + expected[p] + "> but was:<" + actual[p] + ">";
    return this.result;
    }
    return this.result;
    }
    }
    }

    可见,一旦两个对象不匹配,会产生junit风格的错误信息,存在AssertFixture.result对象中,在Java端,通过browser.getEval方法拿到这个值:

    public void assertStateConsistentWith(String expectedStateObject, String actualStateObjectExpression) {
    String result = browser.getEval("window.AssertFixture.assertStateConsistentWith.call(window.AssertFixture, " + expectedStateObject + ", " + actualStateObjectExpression + ");");
    assertTrue(result, result.equals("null"));
    }

    一旦出错,junit会打出对应的错误信息。

    这个Javascript写的验证函数放在那里呢?在Java端通过browser.runScript动态加载进来还是干脆放在Production环境中,随时可以访问?我倾向于后者,简单,也没什么大乱子。


    将Selenium对象封装到抽象层次更高的Java对象中

    回顾“期望这样写测试”一节,我们没有看到对selenium对象的直接操作,它们被封装到了Page对象中。每个主要的Page都有自己的对象,这些对象封装了Page的主要操作,支持更高层次的复用。

    现在齐了,应该都走通了。结束前想说:

     
    用Java写Web页面测试是不得已而为之的

    显见,通过Java代码来写WEB页面的测试,不直接,麻烦,尤其是涉及到大表单的时候。如果能直接用Javascript来写,就自然多了。但其中会面临很多技术问题要解决,比如说一段Javascript的测试代码在页面跳转之后会失效等等,这类问题Selenium其实都已经解决了,也许可以利用这一点,在Selenium的运行环境之上,用Javascript写端到端的验收测试。

     


    收藏到:Del.icio.us