-
利用客户端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