One of QF-Test's benefits is that complex tests can be created without writing a single line
of code. However, there are limits to what can be achieved with a GUI alone. When testing
a program which writes to a database, for example, one might want to verify that the
actual values written to the database are correct; or one might want to read values from a
database or a file and use these to drive a test. All this and more is possible with the
help of powerful scripting languages like Jython and Groovy.
3.0+
While Jython is supported since the beginning of QF-Test, Groovy has found its way into
QF-Test a bit later (QF-Test version 3). This language might be more convenient than Jython
for those who are familiar with Java. It's mainly a matter of individual preference
whether to utilize Jython or Groovy scripting inside QF-Test.
In this chapter the basics of the scripting integration are explained in detail with
Jython. Most of that also holds true for Groovy, especially QF-Test's run-context
methods are the same for both languages. The peculiarities of Groovy will be explained in
the section Scripting with Groovy.
3.0+
The scripting language to use for a given 'Server script' or
'SUT script' node is determined by its 'Script language'
attribute, so you can mix both languages inside a test-suite. The default language to use
for newly created script nodes can be set via the option Default script language for script nodes.
Python is an excellent, object oriented scripting language written in C by Guido van
Rossum. A wealth of information including an excellent Python tutorial is available at
http://www.python.org. Python is a standard
language that has been around for years with extensive freely accessible documentation.
Therefore this manual only explains how Jython is integrated into QF-Test, not the
language itself. Python is a very natural language. Its greatest strength is the
readability of Python scripts, so you should have no problems following the examples.
Jython (formerly called JPython) is a Java implementation of the language Python.
It has the same syntax as Python and almost the same set of features. The object
systems of Java and Jython are very similar and Jython can be integrated seamlessly
into applications like QF-Test. This makes it an invaluable tool for Java scripting.
Jython has its own web page at
http://www.jython.org. There is also an extensive
tutorial available which may help you getting started with this scripting language.
QF-Test uses Jython version 2.5 which supports a large majority of the standard Python
library.
The approach to scripting in QF-Test is inverse from that of other
GUI test tools. Instead of driving the whole test from a script, QF-Test
embeds scripts into its own test-suite. This is achieved with the two
nodes 'Server script' and 'SUT script'.
Both nodes have a 'Script' attribute for the
actual code.
3.0+
The internal script editor has some useful features to ease the typing of
code. Reserved key words, built-in functions, standard types, literals and
comments are highlighted. Indentation is handled automatically inside of
code blocks. With [TAB] and
[Shift-TAB] respectively several selected
lines can be indented manually.
However, the probably most useful feature - at least for the QF-Test newbie - might be the
input assistance for many built-in methods. Type, for example, rc. and
maybe some initial letters of a method name. Then press [Ctrl-Space] to make a popup window displaying the appropriate methods and descriptions
of QF-Test's run-context (cf. chapter 37). Select one of the
methods and confirm with [Return] to insert it into the script
code. To get a list of all objects equipped with help, just press [Ctrl-Space] with the mouse cursor positioned after whitespace.
'Server scripts' are useful for tasks like calculating the values
of variables or reading and parsing data from a file and using that to
drive a test. 'SUT scripts' on the other hand give full access
to the components of the SUT and to every Java API that the SUT
exposes. An 'SUT script' might be used to retrieve or check
values in the SUT to which QF-Test doesn't have access. The 'SUT script'
node additionally requires a 'Client' attribute for
the name of the SUT client to run in.
'Server scripts' are run in a Jython interpreter embedded in QF-Test
itself, while 'SUT scripts' are run in a Jython interpreter
embedded in the SUT. These interpreters are independent of each
other and do not share any state. However, QF-Test uses the RMI
connection between itself and the SUT for seamless integration of
'SUT scripts' into the execution of a test.
Through the menu »Extras«-»Jython terminal...« you can open a window with an interactive command prompt for the Jython
interpreter embedded into QF-Test. You can use this terminal to experiment with Jython
scripts, get a feel for the language, but also to try out some sophisticated stuff like
setting up database connections. The keystrokes [Ctrl-Up]
and [Ctrl-Down] let you cycle through previous input and
you can also edit any other line or mark a region in the terminal and simply press
[Return] to send it to the Jython interpreter. In that case QF-Test
will filter the '>>>' and '...' prompts from previous interpreter output.
Similar Jython terminals are available for each SUT client. The respective menu items
are located below the »Clients« menu.
Note When working in a Jython terminal, there's one thing you need
to be aware of: The commands issued to the interpreter are not executed on the event
dispatch thread. This may not mean anything to you and most of the time it doesn't cause
any problems, but it may deadlock your application if you access any Swing or SWT
components or invoke their methods. To avoid that, QF-Test provides the global method
runAWT (and runSWT respectively) that executes arbitrary code
on the event dispatch thread. For example, to get the number of visible nodes in a
JTree component named tree, use
runAWT("tree.getRowCount()") (or runAWT { tree.getRowCount() }
in Groovy) to be on the safe side.
When executing 'Server scripts' and 'SUT scripts', QF-Test provides a special
environment in which a local variable named rc is bound. This variable
represents the run-context which encapsulates the current state of the execution
of the test. It provides an interface (fully documented in section 37.6)
for accessing QF-Test's variables, for calling QF-Test 'Procedures' and can be used to add
messages to the run-log. To 'SUT scripts' it also provides access to the actual
Java components of the SUT's GUI.
For those cases where no run-context is available, i.e. Resolvers, TestRunListeners, code
executing in a background thread etc. QF-Test also provides a module called qf
with useful generic methods for logging and other things. Please see section 37.7 for details.
Probably the best way to learn about Jython and QF-Test is through
examples, so we're going to provide a few here. Full technical
background and a comprehensive API reference are available in
chapter 37.
Working examples are also provided in the test-suite
doc/tutorial/demo-script.qft.
One thing the run-context can be used for is to add arbitrary
messages to the run-log that QF-Test generates for each test-run. These
messages may also be flagged as warnings or errors.
|
|
rc.logMessage("This is a plain message")
rc.logWarning("This is a warning")
rc.logError("This is an error") |
|
|
| | Example 13.1: Logging messages from scripts | |
When working with compact run-logs (which is strongly encouraged, see the option
Create compact run-log), plain messages may be removed from the run-log to
preserve memory. When an error happens, the most recent 100 or so nodes in the run-log
are kept even in a compact run-log, so in general this is not a problem. If you really
need to make sure that a message will definitely be kept in the run-log you can enforce
this by specifying the optional second parameter dontcompactify, e.g.
|
|
rc.logMessage("This message will not be removed", dontcompactify=true)
# or simply
rc.logMessage("This message will not be removed", 1) |
|
|
| | Example 13.2: Logging messages that will not get removed in compact run-logs | |
Note Only the logMessage method has that extra parameter.
Warnings and errors are never removed from the run-log, so it doesn't apply to
logWarning and logError.
Most of the time logging messages is tied to evaluating some
condition. In that case, it is often desirable to get a result in
the HTML or XML report equivalent to that of a 'Check' node. The
methods rc.check and rc.checkEqual will do just that:
|
|
var = 0
rc.check(var == 0, "!Value of var is 0")
rc.checkEqual('${system:user.language}', 'en', "English locale required",
rc.EXCEPTION) |
|
|
| | Example 13.3: Performing checks | |
For the old-style report the message is treated like a 'Check' if it starts with an '!'.
The optional last argument changes the error level in case of
failure.
Using QF-Test variables in Jython scripts is not difficult, but there
are two viable ways to do so and it is important to understand the
difference and which method to apply in which case.
First, standard QF-Test variable expansion takes place before the
script is parsed and executed, so you can use $(var)
or ${group:name} style variables. This is very handy if
you know that the variable values are either numbers or Boolean values,
because Jython will recognize these without quoting:
|
|
if ${qftest:batch}:
rc.logMessage("We are running in batch mode")
else:
rc.logMessage("We are running in interactive mode") |
|
|
| | Example 13.4: QF-Test variable expansion | |
The example above will work fine, because
${qftest:batch} will expand to either true
or false. Though standard Jython does not recognize
these as Boolean values, the special environment QF-Test provides for
the scripts makes this work. The following example also works well,
provided $(i) is a numeric value, for example a
'Loop' index.
|
|
# log some value
rc.logMessage("data[$(i)]:" + data[$(i)]) |
|
|
| | Example 13.5: More variable expansion | |
It gets a bit more complicated when using QF-Test variables with
arbitrary string values. Strings need to be quoted for Jython, using
either single quotes ' or double quotes ".
|
|
rc.logMessage("$(someText)") |
|
|
| | Example 13.6: Simple text expansion | |
The code above will work very well unless $(someText)
expands to a value that contains line-breaks or double quote
characters. In that case, the script is not valid Jython code and
a ScriptException is thrown.
To avoid that kind of problem you should make it a habit to use the run-context's
lookup method (see section 37.6 for API reference) instead of
$(...) or ${...:...} whenever you want to access a QF-Test value
as a string. That way you'll never have to worry about quoting.
|
|
# access a simple variable
rc.logMessage(rc.lookup("someText"))
# access a property or resource
rc.logMessage(rc.lookup("qftest", "version")) |
|
|
| | Example 13.7: Using rc.lookup to access string
variables | |
If you want to combine multiple variables in one string, it is easier
to use rc.expand instead of
rc.lookup. Note that you must escape the '$' characters
by doubling them to prevent QF-Test from expanding the values itself
(see section 36.6).
|
|
rc.logMessage("The resource is" +
rc.expand("$${$$(group):$$(name)}")) |
|
|
| | Example 13.8: Using rc.expand for complex variable access | |
Note Let us again stress the difference between the '$' and the
rc.lookup methods for accessing variables: '$' expressions are expanded
before the script is passed to the Jython interpreter. That means the text
"$(var)" in the script is replaced by a verbatim copy of the value of the
variable var. The method rc.lookup however returns the value
of var during the processing of the script and, as explained above, is
recommended for accessing string values.
To make the results of a Jython script available during further
test execution, values can be stored in global or local variables.
The effect is identical to that of a 'Set variable' node. The
corresponding methods in the run-context are
rc.setGlobal and rc.setLocal.
|
|
# Test if the file /tmp/somefile exists
from java.io import File
rc.setGlobal("fileExists", File("/tmp/somefile").exists()) |
|
|
| | Example 13.9: Using rc.setGlobal | |
After executing the above example $(fileExists) will
expand to 1 if the file /tmp/somefile exists and to 0
if it doesn't.
To clear a variable, set it to None, to clear all global variables use
rc.clearGlobals() from a 'Server script'.
Sometimes it is helpful to have a Jython variable available in several scripting
nodes. If the value of the variable is not a simple string or integer, it is normally not
sufficient to use rc.setGlobal(...) to store it in a global QF-Test variable
because the value will be converted to a string in the process. Instead, such a variable
should be declared global as shown in the following example.
|
|
global globalVar
globalVar = 10000 |
|
|
| | Example 13.10: Global Jython variable | |
The globalVar is now accessible within all further scripting nodes of the
same type ('Server scripts' or 'SUT scripts' of the same client). For changing
the value of globalVar within another script, the global
declaration is necessary again. Otherwise a new local variable is created instead of
accessing the existing global. Use the del statement to remove a global
Jython variable:
|
|
global globalVar
del globalVar |
|
|
| | Example 13.11: Delete a global Jython variable | |
Sometimes one would like to use variable values that have been
defined in one Jython interpreter in a different interpreter. For
example, an 'SUT script' might have been used to create a list
of items displayed in a table. Later we want to iterate over that
list in a 'Server script'.
To simplify such tasks, the run-context provides a symmetrical set
of methods to access or set global variables in a different
interpreter. For 'SUT scripts' these methods are named
toServer and fromServer. The corresponding
'Server script' methods are toSUT and
fromSUT.
The following example illustrates how an 'SUT script' can set
a global variable in QF-Test's interpreter:
|
|
cellValues = []
table = rc.lookup("idOfTable")
for i in range(table.getRowCount()):
cellValues.append(table.getValueAt(i, 0))
rc.toServer(tableCells=cellValues) |
|
|
| | Example 13.12: Setting a server variable from an
'SUT script' | |
After the above script is run, the global variable named
"tableCells" in QF-Test's interpreter will hold the array of cell
values.
Note The cell values in the above example are not
necessarily strings. They could be numbers, date values, anything.
Unfortunately Jython's pickle mechanism isn't smart
enough to transport instances of Java classes (not even
serializable ones), so the whole exchange mechanism is limited to
primitive types like strings and numbers, along with Jython objects
and structures like arrays and dictionaries.
For 'SUT scripts' the run-context provides an additional
method that is extremely useful. Calling
rc.getComponent("componentId") will retrieve the
information of the 'Component' node in the test-suite with
the 'Id' "componentId" and pass that to QF-Test's
component recognition mechanism. The whole process is basically the
same as when simulating an event, including the possible exceptions
if the component cannot be found.
If the component is located, it will be passed to Jython, not as
some abstract data but as the actual Java object. All methods
exposed by the Java API for the component's class can now be
invoked to retrieve information or achieve effects which are not
possible through the GUI alone. To get a list of a component's method
see section 6.5.
|
|
# get the custom password field
field = rc.getComponent("tfPassword")
# read its crypted value
passwd = field.getCryptedText()
rc.setGlobal("passwd", passwd)
# get the table component
table = rc.getComponent("tabAddresses")
# get the number of rows
rows = table.getRowCount()
rc.setGlobal("tableRows", rows) |
|
|
| | Example 13.13: Accessing components with rc.getComponent | |
You can also access sub-items this way. If the
componentId parameter references an item, the result of
the getComponent call is a pair, the component and the
item's index. The index can be used to retrieve the actual value.
The following example shows how to get the value of a table cell.
Note the convenient way Jython supports sequence unpacking during
assignment.
|
|
# first get the table and index
table, (row,column) = rc.getComponent("tableAddresses@Name@Greg")
# then get the value of the table cell
cell = table.getValueAt(row, column) |
|
|
| | Example 13.14: Accessing sub-items with rc.getComponent | |
The run-context can also be used to call back into QF-Test and execute
a 'Procedure' node. Jython is perfect for reading and parsing
data from a database or from a file, so this feature can be used to
run data-driven tests.
Parameters are passed from a Jython script to a QF-Test 'Procedure'
in a Jython dictionary. The keys and values of the dictionary can
be any kind of Jython object. They are converted to strings before
they are passed to QF-Test.
|
|
rc.callProcedure("text.clearField",
{"component": "nameField"}) |
|
|
| | Example 13.15: Simple procedure call | |
In the example above the 'Procedure' named "clearField" in the
'Package' named "text" will be called. The single parameter for
the call named "component" is set to the value "nameField".
The value returned by the 'Procedure' through a 'Return' node is returned as the
result of the rc.callProcedure call.
Note Great care must be taken when using
rc.callProcedure(...) in 'SUT script' nodes. Only short-running
'Procedures' should be called that won't trigger overly complex actions in the SUT.
Otherwise a DeadlockTimeoutException might be caused. For data-driven tests where
for some reason the data must be determined in the SUT, use
rc.toServer(...) to transfer the values to QF-Test's interpreter, then drive
the test from a 'Server script' node where these restrictions do not apply.
Many of the options described in chapter 29 can also be set at runtime
via rc.setOption. Constants for option names are predefined in the class
Options which is automatically available for Jython and Groovy
scripts.
A real-life example where this might be useful is if you want to replay an event on a
disabled component, so you need to temporarily disable QF-Test's check for the
enabled/disabled state:
|
|
rc.setOption(Options.OPT_PLAY_THROW_DISABLED_EXCEPTION, false) |
|
|
| | Example 13.16: Jython/Groovy example for setOption | |
After replaying this special event, the original value read from the configuration file
or set in the option dialog can be restored by unsetting the option as the following
example shows:
|
|
rc.unsetOption(Options.OPT_PLAY_THROW_DISABLED_EXCEPTION) |
|
|
| | Example 13.17: Jython/Groovy example for unsetOption | |
NoteBe sure to set QF-Test options in a 'Server script' node and SUT options in
an 'SUT script' node, otherwise the setting will have no effect. The option
documentation in chapter 29 shows which one to use.
We are going to close this section with a complex example, combining
features from Jython and QF-Test to execute a data-driven test. For
the example we assume that a simple table with the three columns
"Name", "Age" and "Address" should be filled with values read from a
file. The file is assumed to be in "comma-separated-values" format
with "|" as the separator character, one line per table-row, e.g.:
John Smith|45|Some street, some town
Julia Black|35|Another street, same town
To verify the SUT's functionality in creating new table rows, a
QF-Test 'Procedure' should be created that takes three parameters
"name", "age", and "address", creates a new table-row and fills it
with these values. Then we can use Jython to read and parse the
data from the file, iterate over the data-sets and call back to QF-Test
for each table-row to be created. The name of the file to read is
passed in a QF-Test variable named "filename". When we have finished filling
the table, we compare the state of the actual table component with
the data read from the file to make sure everything is OK.
|
|
import string
data = []
# read the data from the file
fd = open(rc.lookup("filename"), "r")
line = fd.readline()
while line:
# remove whitespace
line = string.strip(line)
# split the line into separate fields
# and add them to the data array
if len(line) > 0:
data.append(string.split(line, "|"))
line = fd.readline()
# now iterate over the rows
for row in data:
# call a qftest procedure to create
# one new table row
rc.callProcedure("table.createRow",
{'name': row[0], 'age': row[1],
'address': row[2]})
# verify that the table-rows have been filled correctly
table = rc.getComponent("tabAddresses")
# check the number of rows
rc.check(table.getRowCount() == len(data), "Row count")
if table.getRowCount() == len(data):
# check each row
for i in range(len(data)):
rc.check(table.getValueAt(i, 0)) == data[i][0],
"Name in row " + `i`)
rc.check(table.getValueAt(i, 1)) == data[i][1],
"Age in row " + `i`)
rc.check(table.getValueAt(i, 2)) == data[i][2],
"Address in row " + `i`) |
|
|
| | Example 13.18: Executing a data-driven test | |
Of course, the example above serves only as illustration. It is too
complex to be edited comfortably in QF-Test and too much is hard-coded,
so it is not easily reusable. For real use, the code to read and
parse the file should be parameterized and moved to a module, as
should the code that verifies the table. This topic is covered in
the following section.
You might face a situation where you want to work with a component,
which you have to search before working with it. Sometimes recording
all required components can be exhaustive or might be to
complicated. For such cases you can use the method
rc.overrideElement to set the found component (either
by generic components or via scripting) to a
QF-Test component. Now you can work with the assigned component and use
all available QF-Test nodes.
Let's imagine that we have a panel and we want to work with the first
textfield, but because of changing textfields we cannot rely on the
standard way of the recognition. Now we can implement a script,
which looks for the first textfield and assigns that textfield to
the PriorityAwtSwingComponent from the standard library
qfs.qft. Once we have executed that script we can work
with any QF-Test nodes using the PriorityAwtSwingComponent,
which actually performs all actions on the found textfield.
|
|
from de.qfs.apps.qftest.extensions import ResolverRegistry
panel = rc.getComponent("myPanel")
for component in panel.getComponents():
if ResolverRegistry.instance().isInstance(component, \
"javax.swing.JTextField"):
rc.overrideElement("PriorityAwtSwingComponent", component)
break
|
|
|
| | Example 13.19: Using rc.overrideElement | |
This concept is very useful if you know an algorithm to determine
the target component of your test-steps.
You can find such priority-components for all engines in the
standard library qfs.qft. You can also find an
illustrative example in the provided demo test-suite carconfig_en.qft,
located in the directory demo/carconfig in
your QF-Test installation.
Modules for Jython in QF-Test are just like standard Python
modules. You can import these modules into QF-Test scripts and
call their methods, which simplifies the development of complex
scripts and increases maintainability since modules are available
across test-suites.
Modules intended to be shared between test-suites should
be placed in the directory jython under QF-Test's
root directory. Modules written specifically for one
test-suite can also be placed in the test-suite's directory. The
version-specific directory
qftest-3.4.4/jython/Lib is reserved for
modules provided by Quality First Software GmbH. Jython modules must have the file
extension .py.
To improve example 13.18 you could write a module
csvtable.py with methods loadTable to read
the data from the file and verifyTable to verify the
results. An example module is provided in
qftest-3.4.4/doc/tutorial/csvtable.py.
Following is a simplified version:
|
|
import string
def loadTable(file, separator='|'):
data = []
fd = open(file, "r")
line = fd.readline()
while line:
line = string.strip(line)
if len(line) > 0:
data.append(string.split(line,separator))
line = fd.readline()
return data
def verifyTable(rc, table, data):
ret = 1
# check the number of rows
if table.getRowCount() != len(data):
if rc:
rc.logError("Row count mismatch")
return 0
# check each row
for i in range(len(data)):
row = data[i]
# check the number of columns
if table.getModel().getColumnCount() != len(row):
if rc:
rc.logError("Column count mismatch " +
"in row " + `i`)
ret = 0
else:
# check each cell
for j in range(len(row)):
val = table.getModel().getValueAt(i, j)
if str(val) != row[j]:
if rc:
rc.logError("Mismatch in row " +
`i` + " column " +
`j`)
ret = 0
return ret |
|
|
| | Example 13.20: Writing a module | |
The code above should look familiar. It is an improved version of
parts of example 13.18. With that module in place,
the code that has to be written in QF-Test is reduced to:
|
|
import csvtable
# load the data
data = csvtable.loadTable(rc.lookup("filename"))
# now iterate over the rows
for row in data:
# call a qftest procedure to create
# one new table row
rc.callProcedure("table.createRow",
{'name': row[0], 'age': row[1],
'address': row[2]})
# verify that the table-rows have been filled correctly
table = rc.getComponent("tabAddresses")
csvtable.verifyTable(rc, table, data) |
|
|
| | Example 13.21: Calling methods in a module | |
For more complex import of data QF-Test can be extended with existing
Python modules. For example, at http://python-dsv.sourceforge.net/
an excellent module for very flexible CSV import is freely available.
Python comes with a simple line-oriented debugger called pdb. Among its
useful features is the ability for post-mortem debugging, i.e. analyzing why a script
failed with an exception. In Python you can simply import the pdb package and
run pdb.pm() after an exception. This will put you in a debugger environment
where you can examine the variable bindings in effect at the time of failure and also
navigate up to the call stack to examine the variables there. It is somewhat similar to
analyzing a core dump of a C application.
Though Jython comes with pdb, the debugger doesn't work very well inside QF-Test
for various reasons. But at least post-mortem debugging of Jython scripts is supported
from the Jython terminals (see section 13.1). After a
'Server script' node fails, open QF-Test's Jython terminal, for a failed
'SUT script' node open the respective SUT Jython terminal, then just execute
debug(). This should have a similar effect as pdb.pm() described
above. For further information about the Python debugger please see the documentation for
pdb in Python version 2.5 at http://www.python.org/doc/2.5/lib/module-pdb.html.
Jython version 2.5 was a major rewrite of the Java version of Python. Though most of the
changes are backwards compatible, there are some subtle differences resulting from changes
in the Java integration as well as the Python language itself.
Jython now has a real boolean type with values True and False
whereas in older versions integer values 0 and 1 served as boolean values. This can
cause problems if boolean results from calls like file.exists() are
assigned to a QF-Test variable, e.g. "fileExists" and later checked in a
'Condition' attribute in the form $(fileExists) == 1. Such
conditions generally be written as just $(fileExists) which works well with
all Jython versions.
All Java strings are sequences of 16-bit characters. Python's original strings are made
of 8-bit characters. Later, unicode strings with 16-bit characters were added. Jython
literal strings like "abc" are 8-bit, prepending 'u' for
u"abc" turns them into unicode strings.
In Jython 2.2, Java strings were converted to 8-bit Python strings based on the default
encoding of the Java VM, typically ISO-8859-1 (also known as latin-1) in western
countries. In Jython 2.5, every Java string is now interpreted as a unicode Jython
string. This results in a lot more implicit conversion between 8-bit and unicode
strings, for example when concatenating a Java string - now converted to unicode - and a
literal string like rc.lookup("path") + "/file". Most of the time this
works well, but if the literal string contains characters outside the 7-bit ASCII
character-set, things start to get messy. The default encoding for 8-bit Jython
characters can be specified in the option Default character encoding for Jython
with a default of latin-1 for maximum backwards compatibility. On the upside it is now
possible to have default encodings other than latin-1 and to specify literal strings of
characters in international character sets.
One thing to watch out for is existing code of the form
import types
if type(somevar) == types.StringType:
...
The type types.StringType is the 8-bit string. It does not match unicode
strings. To test whether some variable is a Jython string, regardless of whether it's
8-bit or unicode, change that to
import types
if type(somevar) in types.StringTypes:
...
One new requirement - coming from newer Python versions - is that Python module files
containing characters outside the 7-bit ASCII character must specify the character
encoding to be used in a comment line close to the top of the file, e.g.
# coding: latin-1
Please see http://www.python.org/peps/pep-0263.html for details.
This simple operation is surprisingly difficult in Jython. Given a Java object you would
expect to simply write obj.getClass().getName(). For some objects this
works fine, for others it fails with a cryptic message. This can be very frustrating.
Things go wrong whenever there is another getName method defined by the
class, which is the case for AWT Component, so getting the class name this
way fails for all AWT/Swing component classes.
In Jython 2.2.1 the accepted workaround was to use the Python idiom
obj.__class__.__name__. This no longer works in Jython 2.5 because it
no longer returns the fully qualified class name, only the last part. Instead of
java.lang.String you now get just String. The only solution
that reliably works for version 2.5 is:
from java.lang import Class
Class.getName(obj.getClass())
This also works for 2.2, but it is not nice, so we initiated a new convenience module
with utility methods called qf that gets imported automatically. As a
result you can now simply write
qf.getClassName(obj).
Groovy is a relatively new language for the Java Platform. It was invented by James
Strachan and Bob McWhirter in 2003. All you need for doing Groovy is a Java Runtime
Environment (JRE) and the groovy-all.jar file. This library contains a
compiler to create Java class files and provides the runtime when using that classes in
the Java Virtual Machine (JVM). You may think of Groovy as being Java with an additional
.jar file. In contrast to Java, Groovy is a dynamic language, meaning
that the behaviour of an object is determined at runtime. Groovy also allows to load
classes from sources without creating class files. Finally, it is easy to embed Groovy
scripts into Java applications like QF-Test. Currently QF-Test uses Groovy 1.5.0.
The Groovy syntax is similar to Java, maybe more expressive and easier to read. When
coming from Java you can embrace the Groovy style step by step. Of course we cannot
explain all aspects of the Groovy language here. For in-depth information, please take a
look at the Groovy home page at http://groovy.codehaus.org or read the excellent
book "Groovy in Action" by Dierk Koenig and others. Perhaps the following tips may help a
Java programmer getting started with Groovy.
-
The semicolon is optional as long as a line contains only one statement.
-
Parentheses are sometimes optional, e. g.
println 'hello qfs' means the
same as println('hello qfs').
-
Use
for (i in 0..<len) { ... } instead of for (int i = 0; i <
len; i++) { ... }.
-
The following imports are made by default:
java.lang.*, java.util.*,
java.io.*, java.net.*, groovy.lang.*, groovy.util.*, java.math.BigInteger,
java.math.BigDecimal.
-
Everything is an object, even integers like '1' or booleans like 'true'.
-
Instead of using getter and setter methods like
obj.getXxx(), you can
simply write obj.xxx to access a property.
-
The operator
== checks for equality, not identity, so you can write
if (somevar == "somestring") instead of if
(somevar.equals("somestring")). The method is() checks for identity.
-
Variables have a dynamic type when being defined with the
def
keyword. Using def x = 1 allows for example to assign a
String value to the variable x later in the script.
-
Arrays are defined differently from Java, e. g.
int[] a = [1, 2, 3] or
def a = [1, 2, 3] as int[]. With def a = [1, 2, 3] you
define a List in Groovy.
-
Groovy extends the Java library by defining a set of extra methods for many classes.
Thus you can for example apply an
isInteger() method to any
String object in a Groovy script. That's what is called GDK
(according to the JDK in Java). To get a list of those methods for an
arbitrary object obj, you can simply invoke
obj.class.metaClass.metaMethods.name or use the following example:
|
|
import groovy.inspect.Inspector
def s = 'abc'
def inspector = new Inspector(s)
def mm = inspector.getMetaMethods().toList().sort() {
it[Inspector.MEMBER_NAME_IDX] }
for (m in mm) {
println(m[Inspector.MEMBER_TYPE_IDX] + ' ' +
m[Inspector.MEMBER_NAME_IDX] +
'(' + m[Inspector.MEMBER_PARAMS_IDX] + ')')
} |
|
|
| | Example 13.22: GDK methods for a String object | |
-
Inner classes are not supported, in most cases you can use Closures instead.
A
Closure is an object which represents a piece of code. It can take
parameters and return a value. Like a block, a Closure is defined with
curly braces { ... }. Blocks only exists in context with a
class, an interface, static or object initializers, method
bodies, if, else, synchronized,
for, while, switch, try,
catch, and finally. Every other occurrence of
{...} is a Closure. As an example let's take a look at the
eachFileMatch GDK method of the File class. It takes two
parameters, a filter (e. g. a Pattern) and a Closure. That
Closure takes itself a parameter, a File object for the
current file.
|
|
def dir = rc.lookup('qftest', 'suite.dir')
def pattern = ~/.*\.qft/
def files = []
new File(dir).eachFileMatch(pattern) { file ->
files.add(file.name)
}
files.each {
// A single Closure argument can also be refered with "it"
rc.logMessage(it)
} |
|
|
| | Example 13.23: Closures | |
-
Working with
Lists and Maps is simpler than in Java.
|
|
def myList = [1, 2, 3]
assert myList.size() == 3
assert myList[0] == 1
myList.add(4)
def myMap = [a:1, b:2, c:3]
assert myMap['a'] == 1
myMap.each {
this.println it.value
} |
|
|
| | Example 13.24: Working with lists and maps | |
In QF-Test Groovy scripts we decided not to support the $-Expansion for QF-Test
variables. It only takes place in Jython scripts (see subsection 13.3.3). The reason is that Groovy already uses the dollar sign to
dereference script variables and evaluate expressions within a GString.
|
|
def x = 3
assert "$x" == 3
assert "${2 * x}" == 6 |
|
|
| | Example 13.25: GString expansion | |
Values of QF-Test variables can be obtained at runtime by means of several
rc methods:
-
String lookup(varname) or String lookup(group, varname)
-
String getStr(varname) or String getStr(group, varname)
-
Integer getNum(varname) or Integer getNum(group, varname)
-
Boolean getBool(varname) or Boolean getBool(group, varname)
|
|
rc.setGlobal('fileExists', new File('c:/tmp/somefile.foo').exists())
assert rc.lookup('fileExists') == 'false'
assert rc.getStr('fileExists') == 'false'
assert ! rc.getBool('fileExists')
rc.setGlobal('myvar', '3')
assert rc.getNum('myvar') == 3 |
|
|
| | Example 13.26: Lookup for QF-Test variables in a Groovy script | |
Exchanging variables between several script nodes of the same type
('Server scripts' or 'SUT scripts' of the same client) is even easier than it
does with Jython. The rule is that undeclared variables are assumed to be defined in
the binding of the script. If they are not, they will be added automatically to the
list of binding variables.
|
|
|
|
| | Example 13.27: Define a global Groovy variable | |
|
|
assert myGlobal == 'global'
def globals = binding.variables
assert globals['myGlobal'] == 'global'
globals.remove('myGlobal')
assert globals.find { it == 'myGlobal' } == null |
|
|
| | Example 13.28: Use and delete a global Groovy variable | |
Predefined global variables are the QF-Test run-context rc and the
PrintWriter out, which is used for the script's println
method.
Just like Java classes, Groovy source files (.groovy) can be organized in
packages. Those intended to be shared between test-suites should be placed in
the directory groovy under QF-Test's root directory. Others that are written
specifically for one test-suite can also be placed in the test-suite's directory. The
version-specific directory qftest-3.4.4/groovy is reserved for
Groovy files provided by Quality First Software GmbH.
|
|
package my
class MyModule
{
public static int add(int a, int b)
{
return a + b
}
} |
|
|
| | Example 13.29: MyModule.groovy | |
The file MyModule.groovy could be saved in a subdirectory my
below the suite directory. Then you can use the add method from
MyModule as follows:
|
|
import my.MyModule as MyLib
assert MyLib.add(2, 3) == 5 |
|
|
| | Example 13.30: Using MyModule | |
This code also shows another groovy feature: Type aliasing. By using
import and as in combination you can reference a class by
a name of your choice.