ZK Automated Tests
Gerard Cristofol, Project Manager, Indra Sistemas S.A., Spanish.
April 19, 2007
Applicable to ZK 2.2.1 and later.
Abstract
This document presents a way to run automated tests from home-made zul scripts.
The Issue
Due to the dynamic nature of the zul pages, and particularly, the dynamic component id assignment at DOM level, automated test execution is a difficult task. Most of the widely available tools don’t expect components ids to changes between deployments so they simply wouldn’t work.
I've read some of the complaints on the general forum. This approximation tries to address them.
The Solution
To solve the issue we’ll focus on develop some extra code that, being included on existent zul pages, will sequentially send events to components and so simulate the user interaction. No more than a lousy test engine, but enough in most scenarios.
For every zul page we want to test, a sequence of actions has to be written. These actions can be either: set properties of input elements (like text areas), send events to buttons, etc.
Let’s make it happen
I’ll try to provide all the elements, so it’s easy to reproduce the whole thing with your own zul pages.
1. Action Sequence Script
The script is nothing more than a sequence of events which together trigger an interesting functionality on the server side.
<?xml version="1.0" encoding="UTF-8"?>
<AutomationTest
xmlns="http://binding.bsf.indra.es/grinder"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://binding.bsf.indra.es/grinder
.\xsd\grinder.xsd">
<Actions script="input.zul">
<Action component-id="thetextbox">
<InputElementAction property="sample text"/>
</Action>
<Action component-id="theintbox">
<InputElementAction property="1"/>
</Action>
<Action component-id="thebutton">
<ButtonAction event="onClick"/>
</Action>
</Actions>
</AutomationTest>
This action sequence could be complex enough to deserve its own schema. Here’s the basic schema for example purposes: (The scope of this example is limited to certain type of events. Although it can be easily extended to support those that fit your application needs)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="http://binding.bsf.indra.es/grinder"
xmlns:gnr="http://binding.bsf.indra.es/grinder"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified">
<xs:element name="AutomationTest">
<xs:complexType>
<xs:sequence>
<xs:element ref="gnr:Actions" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Actions">
<xs:complexType>
<xs:sequence>
<xs:element ref="gnr:Action" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="script" type="xs:string" use="required"/>
<xs:attribute name="functionality" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="Action">
<xs:complexType>
<xs:sequence>
<xs:element ref="gnr:InputElementAction" minOccurs="0"/>
<xs:element ref="gnr:ButtonAction" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="component-id" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="InputElementAction">
<xs:complexType>
<xs:attribute name="property" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="ButtonAction">
<xs:complexType>
<xs:attribute name="event" type="gnr:eventType" use="required"/>
</xs:complexType>
</xs:element>
<xs:simpleType name="eventType">
<xs:restriction base="xs:string">
<xs:enumeration value="onClick"/>
<xs:enumeration value="onRightClick"/>
<xs:enumeration value="onDoubleClick"/>
<xs:enumeration value="onOK"/>
<xs:enumeration value="onCancel"/>
<xs:enumeration value="onCtrlKey"/>
<xs:enumeration value="onChange"/>
<xs:enumeration value="onChanging"/>
<xs:enumeration value="onClose"/>
<xs:enumeration value="onTimer"/>
<xs:enumeration value="onFocus"/>
<xs:enumeration value="onBlur"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
2. Create the Binding Classes with XJC
Having a schema will allow us to make binding classes, thus simplifying the code to be developed behind this robot. Any XML Binding tool will do. For this example I’ve used Sun’s JAXB Reference Implementation. Documentation on the XJC binding compiler usage and its Ant Task is available at http://java.sun.com/webservices/docs/1.6/jaxb/ant.html
3. Include zscript
Next, we’ll need a chunk of zscript capable of include the original page and interact with it. This example not only does that but also proves my remarkable skills with html. Check out the red box. Wow! How you like that?
Here’s the code:
<?xml version="1.0" encoding="utf-8"?>
<?page title="ZK::Automated Test"?>
<window xmlns:h="http://www.w3.org/1999/xhtml" border="none">
<zscript>
import es.indra.bsf.portal.grinder.GrinderTestForm;
import es.indra.bsf.binding.grinder.Action;
import java.util.Iterator;
</zscript>
<vbox>
<h:table style="border-width: 5; border-style: solid; border-color: red;" >
<h:tr>
<h:td>
This windows includes "<label id="etiqueta"/>" to perform automated tests
</h:td>
</h:tr>
<h:tr>
<h:td>
<textbox id="pantalla" value="input.zul"/>
<button id="show" label="show">
<attribute name="onClick">{
etiqueta.value=pantalla.value;
include.src=pantalla.value;
}</attribute>
</button>
<button id="test" label="execute">
<attribute name="onClick">{
GrinderTestForm gtf = GrinderTestForm.getInstance();
Iterator it = gtf.getActionsByPantalla(pantalla.value);
while( it.hasNext())
{
Action action = (Action)it.next();
gtf.execute(desktop, action);
}
}</attribute>
</button>
</h:td>
</h:tr>
</h:table>
</vbox>
<include id="include"/>
</window>
Notice that the only interesting part is the red colour one. It obtains the singleton instance and executes the actions defined in the action sequence xml.
The page itself looks something like this:
4. The Java Code
The only missing link now is this GrinderTestForm singleton implementation. Just a little bit of plumbing between the xml and the actual event dispatching. Here it is:
package es.indra.bsf.portal.grinder;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
import java.util.Iterator;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.zkoss.lang.Expectable;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Button;
import org.zkoss.zul.impl.InputElement;
import es.indra.bsf.binding.grinder.Action;
import es.indra.bsf.binding.grinder.Actions;
import es.indra.bsf.binding.grinder.AutomationTest;
import es.indra.bsf.binding.grinder.ButtonAction;
import es.indra.bsf.binding.grinder.InputElementAction;
class ScriptConfigException extends RuntimeException {
public ScriptConfigException(String m){
super(m);}
}
class ScriptWrongUseException extends Exception implements Expectable {
public ScriptWrongUseException(String m){
super(m);}
}
/**
* Singleton class that dispatches events to zul components
* according to the sequence described in grinder.xml
* @author gcristofol@indra.es
*/
public class GrinderTestForm {
private static Log log = LogFactory.getLog(GrinderTestForm.class);
public static final String GRINDER_XML = "grinder.xml";
public static final String GRINDER_PACKAGE = "es.indra.bsf.binding.grinder";
// Test Sequences
private AutomationTest _testAutomation = null;
static class SingletonHolder {
static GrinderTestForm instance = new GrinderTestForm();
}
public static GrinderTestForm getInstance() {
return SingletonHolder.instance;
}
private GrinderTestForm() {
// Read the XML from classpath
URL url = null;
try {
URLClassLoader myClassLoader = (URLClassLoader) GrinderTestForm.class
.getClassLoader();
url = myClassLoader.findResource(GRINDER_XML);
if (url == null) {
throw new ScriptConfigException("Can't find " + GRINDER_XML
+ " in classpath");
}
// create binding and unmarshall
JAXBContext jc = JAXBContext.newInstance(GRINDER_PACKAGE);
Unmarshaller u = jc.createUnmarshaller();
// Get pieces of the XML instance.
log.info("Get pieces of the XML instance "+ url.getPath());
// Bind the instance to the generated XJC types.
_testAutomation = (AutomationTest) u.unmarshal(url.openStream());
} catch (IOException e) {
throw new ScriptConfigException("Configuration file not found: url=" + url + " file="
+ GRINDER_XML +e.getMessage());
} catch (JAXBException e) {
e.printStackTrace();
throw new ScriptConfigException("JAXBException "+e.getMessage());
}
}
public static Component findComponentById(Desktop desktop, String id) throws ScriptWrongUseException {
Iterator componentIterator = desktop.getComponents().iterator();
Component result = null;
while (componentIterator.hasNext()) {
result = (Component) componentIterator.next();
if (id.equals(result.getId()))
return result;
}
throw new ScriptWrongUseException("Component "+id+" undefined on desktop "+desktop);
}
public Iterator getActionsByPantalla(String pantalla) throws ScriptWrongUseException {
List<Actions> actions = _testAutomation. getActions();
for(Actions a : actions) {
if(a.getScript().equalsIgnoreCase(pantalla))
return a.getAction().iterator();
}
throw new ScriptWrongUseException("Window "+pantalla+" undefined on xml "+GRINDER_XML);
}
public void execute(Desktop desktop, Action accion) throws ScriptWrongUseException {
if (accion.getInputElementAction() != null) {
this.executePropertyAction(desktop, accion.getComponentId(), accion.getInputElementAction());
} else {
this.executeEventAction(desktop, accion.getComponentId(), accion.getButtonAction());
}
}
private void executePropertyAction(Desktop desktop, String componentId, InputElementAction value)
throws ScriptWrongUseException {
log.info("executePropertyAction on "+componentId+" value "+value.getProperty());
InputElement ie = (InputElement)findComponentById(desktop, componentId);
ie.setText(value.getProperty());
}
private void executeEventAction(Desktop desktop, String componentId, ButtonAction value)
throws ScriptWrongUseException {
log.info("executeEventAction on "+componentId+" value "+value.getEvent());
Button result = (Button)findComponentById(desktop, componentId);
String event = value.getEvent().value();
log.debug("id: " + result.getId() + "; action: " + result.getAction() + "; event: " + event);
Events.sendEvent(result, new Event(event, result));
}
}
Without consider the code needed to unmarshall the code into the beans. This code performs a simple job, finds components and performs actions. Fortunately the ZK Java api provides an easy way to (re)dispatch events. Depending on the component performs different actions: For the textboxes uses setText and for the buttons Events.sendEvent(...)
Still I don’t believe you
Then the next step is try to automate zkdemo-all\userguide\simple\input.zul It’s always better to recycle an existent zul, (especially if you are lazy). It doesn’t have an id for every component, so we have to fix it with the following patch:
56c56
< textbox: <textbox id="thetextbox" value="text..."/>
---
> textbox: <textbox value="text..."/>
58,63c58
< <row>int box:<intbox id="theintbox"/></row>
< <row>button:<button id="thebutton" label="clickme">
< <attribute name="onClick">{
< thebutton.label=";-)";
< }</attribute>
< </button></row>
---
> <row>int box:<intbox/></row>
That is, add “thetextbox” id to the textbox, “theintbox” id to the intbox and finally add a button with id “thebutton”, and some silly response code to the onClick event.
Remember “1. Action Sequence Script”? A closer look at the xml reveals that suits exactly the modified input.zul we’ve just made. How convenient! Again:
<?xml version="1.0" encoding="UTF-8"?>
<AutomationTest xmlns="http://binding.bsf.indra.es/grinder"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://binding.bsf.indra.es/grinder
.\xsd\grinder.xsd">
<Actions script="input.zul">
<Action component-id="thetextbox">
<InputElementAction property="sample text"/>
</Action>
<Action component-id="theintbox">
<InputElementAction property="1"/>
</Action>
<Action component-id="thebutton">
<ButtonAction event="onClick"/>
</Action>
</Actions>
</AutomationTest>
Finally, let’s use the thing. Drop the script in zkdemo-all\userguide\simple folder. I call it grinder.zul. Then go to http://localhost:8080/zkdemo-all/userguide/simple/grinder.zul
The usage is simple. Press “show” to include the stated page, and “execute” to launch our script. I’ve pressed the buttons and cool things happened, look:
But I don’t want to press any buttons
In most cases the lousy test engine described serves the demands. Particularly if you merely want to compare the “direct” business logic invocations (from test cases) vs. the overhead of ZK interface.
Nonetheless if you want to test your container and deployment environment, then you simply have to provide a single zul script for every functionality that has to be fired. Easy to do adding a timer to the after mentioned zul.
<zscript>
test()
{
GrinderTestForm.onTimer("foo.zul");
}
</zscript>
<timer id="timer" delay="1000" repeats="false" onTimer="test()"/>
And the onTimer method to GrinderTestForm class
public static void onTimer(String pantalla) throws ScriptWrongUseException {
//Invocation time begins...
long start = System.currentTimeMillis();
GrinderTestForm gtf = GrinderTestForm.getInstance();
Iterator it = gtf.getActionsByPantalla(pantalla);
log.debug("Desktop id: " + Executions.getCurrent().getDesktop().getId());
log.debug("Session: " + Executions.getCurrent().getDesktop().getSession().toString());
while( it.hasNext()) {
Action action = (Action)it.next();
gtf.execute(Executions.getCurrent().getDesktop(), action);
}
//Invocation time ends
long end = System.currentTimeMillis();
log.info("Window: "+pantalla);
log.info("Duracion = " + (end - start) + " ms" );
}
Do we need the timer? Yes, because of the include. Quote of ZK manual: --If the include component is used to include a ZUML page, the included page will become part of the desktop. However, the included page is not visible until the request is processed completely. In other words, it is visible only in the following events, triggered by user or timer --
Since we have a single url which automatically fires all the events with any user interaction, it’s easy to stress some parts of your application.
Future
Future is twofold:
- Improve
The polite way to get this same functionality would be making it part of the ZK components. Using a component <robot src="whatever-sequence.xml"/> to include and execute the "test sequence" instead of developing new scripts just for testing. Test-mode might be enabled and disabled in zk.xml
- The Real Approach
Limitations are obvious; it would take a lot of work to get visually attractive results. There’s lot’s of tools out there that produce beautiful charts, the really cool approach it’s definitely integrate with those tools. We are halfway in the path to integrate grinder (that explains some of the variable names) that means create a version of HTMLLayoutServlet that builds a map of the id → uuid equivalences to dynamically fire some events.
Gerard Cristofol is a Project Manager at Indra Sistemas, Spain. He has got a degree in Computer Science, a MPM, RSA Certification, and extensive experience in Java related technologies: web applications, enterprise integration. He is now focusing on ESB service infrastructures.
Copyright © Gerard Cristofol. This article is licensed under GNU Free Documentation License. |