4.1 Creating test libraries
Robot Framework's actual testing capabilities are provided by test libraries. There are many existing libraries, some of which are even bundled with the core framework, but there is still often a need to create new ones. This task is not too complicated because, as this chapter illustrates, Robot Framework's library API is simple and straightforward.
- 4.1.1 Introduction
- 4.1.2 Creating test library class or module
- 4.1.3 Creating static keywords
- 4.1.4 Communicating with Robot Framework
- 4.1.5 Distributing test libraries
- 4.1.6 Dynamic library API
- 4.1.7 Hybrid library API
- 4.1.8 Using Robot Framework's internal modules
- 4.1.9 Extending existing test libraries
4.1.1 Introduction
Supported programming languages
Robot Framework itself is written with Python and naturally test libraries extending it can be implemented using the same language. When running the framework on Jython, libraries can also be implemented using Java. Pure Python code works both on Python and Jython, assuming that it does not use syntax or modules that are not available on Jython. When using Python, it is also possible to implement libraries with C using Python C API, although it is often easier to interact with C code from Python libraries using ctypes module.
Libraries implemented using these natively supported languages can also act as wrappers to functionality implemented using other programming languages. A good example of this approach is the Remote library, and another widely used approaches is running external scripts or tools as separate processes.
Tip
Python Tutorial for Robot Framework Test Library Developers covers enough of Python language to get started writing test libraries using it. It also contains a simple example library and test cases that you can execute and otherwise investigate on your machine.
Different test library APIs
Robot Framework has three different test library APIs.
Static API
The simplest approach is having a module (in Python) or a class (in Python or Java) with methods which map directly to keyword names. Keywords also take the same arguments as the methods implementing them. Keywords report failures with exceptions, log by writing to standard output and can return values using the return statement.
Dynamic API
Dynamic libraries are classes that implement a method to get the names of the keywords they implement, and another method to execute a named keyword with given arguments. The names of the keywords to implement, as well as how they are executed, can be determined dynamically at runtime, but reporting the status, logging and returning values is done similarly as in the static API.
Hybrid API
This is a hybrid between the static and the dynamic API. Libraries are classes with a method telling what keywords they implement, but those keywords must be available directly. Everything else except discovering what keywords are implemented is similar as in the static API.
All these APIs are described in this chapter. Everything is based on how the static API works, so its functions are discussed first. How the dynamic library API and the hybrid library API differ from it is then discussed in sections of their own.
The examples in this chapter are mainly about using Python, but they should be easy to understand also for Java-only developers. In those few cases where APIs have differences, both usages are explained with adequate examples.
4.1.2 Creating test library class or module
Test libraries can be implemented as Python modules and Python or Java classes.
Test library names
The name of a test library that is used when a library is imported is the same as the name of the module or class implementing it. For example, if you have a Python module MyLibrary (that is, file MyLibrary.py), it will create a library with name MyLibrary. Similarly, a Java class YourLibrary, when it is not in any package, creates a library with exactly that name.
Python classes are always inside a module. If the name of a class implementing a library is the same as the name of the module, Robot Framework allows dropping the class name when importing the library. For example, class MyLib in MyLib.py file can be used as a library with just nameMyLib. This also works with submodules so that if, for example, parent.MyLib module has class MyLib, importing it using just parent.MyLib works. If the module name and class name are different, libraries must be taken into use using both module and class names, such asmymodule.MyLibrary or parent.submodule.MyLib.
Java classes in a non-default package must be taken into use with the full name. For example, class MyLib in com.mycompany.myproject package must be imported with name com.mycompany.myproject.MyLib.
Note
Dropping class names with submodules works only in Robot Framework 2.8.4 and newer. With earlier versions you need to include also the class name like parent.MyLib.MyLib.
Tip
If the library name is really long, for example when the Java package name is long, it is recommended to give the library a simpler alias by using the WITH NAME syntax.
Providing arguments to test libraries
All test libraries implemented as classes can take arguments. These arguments are specified in the Setting table after the library name, and when Robot Framework creates an instance of the imported library, it passes them to its constructor. Libraries implemented as a module cannot take any arguments, so trying to use those results in an error.
The number of arguments needed by the library is the same as the number of arguments accepted by the library's constructor. The default values and variable number of arguments work similarly as with keyword arguments, with the exception that there is no variable argument support for Java libraries. Arguments passed to the library, as well as the library name itself, can be specified using variables, so it is possible to alter them, for example, from the command line.
Importing a test library with arguments | |||
---|---|---|---|
Setting | Value | Value | Value |
Library | MyLibrary | 10.0.0.1 | 8080 |
Library | AnotherLib | ${VAR} |
Example implementations, first one in Python and second in Java, for the libraries used in the above example:
from example import Connection
class MyLibrary:
def init(self, host, port=80):
self._conn = Connection(host, int(port))
def send_message(self, message):
self._conn.send(message)
public class AnotherLib {
private String setting = null;
public AnotherLib(String setting) {
setting = setting;
}
public void doSomething() {
if setting.equals("42") {
// do something ...
}
}
}
Test library scope
Libraries implemented as classes can have an internal state, which can be altered by keywords and with arguments to the constructor of the library. Because the state can affect how keywords actually behave, it is important to make sure that changes in one test case do not accidentally affect other test cases. These kind of dependencies may create hard-to-debug problems, for example, when new test cases are added and they use the library inconsistently.
Robot Framework attempts to keep test cases independent from each other: by default, it creates new instances of test libraries for every test case. However, this behavior is not always desirable, because sometimes test cases should be able to share a common state. Additionally, all libraries do not have a state and creating new instances of them is simply not needed.
Test libraries can control when new libraries are created with a class attribute ROBOT_LIBRARY_SCOPE . This attribute must be a string and it can have the following three values:
TEST CASE
A new instance is created for every test case. A possible suite setup and suite teardown share yet another instance. This is the default.
TEST SUITE
A new instance is created for every test suite. The lowest-level test suites, created from test case files and containing test cases, have instances of their own, and higher-level suites all get their own instances for their possible setups and teardowns.
GLOBAL
Only one instance is created during the whole test execution and it is shared by all test cases and test suites. Libraries created from modules are always global.
Note
If a library is imported multiple times with different arguments, a new instance is created every time regardless the scope.
When the TEST SUITE or GLOBAL scopes are used with test libraries that have a state, it is recommended that libraries have some special keyword for cleaning up the state. This keyword can then be used, for example, in a suite setup or teardown to ensure that test cases in the next test suites can start from a known state. For example, SeleniumLibrary uses the GLOBAL scope to enable using the same browser in different test cases without having to reopen it, and it also has the Close All Browsers keyword for easily closing all opened browsers.
Example Python library using the TEST SUITE scope:
class ExampleLibrary:
ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
def init(self):
self._counter = 0
def count(self):
self._counter += 1
print self._counter
def clear_counter(self):
self._counter = 0
Example Java library using the GLOBAL scope:
public class ExampleLibrary {
public static final String ROBOT_LIBRARY_SCOPE = "GLOBAL";
private int counter = 0;
public void count() {
counter += 1;
System.out.println(counter);
}
public void clearCounter() {
counter = 0;
}
}
Specifying library version
When a test library is taken into use, Robot Framework tries to determine its version. This information is then written into the syslog to provide debugging information. Library documentation tool Libdoc also writes this information into the keyword documentations it generates.
Version information is read from attribute ROBOTLIBRARYVERSION, similarly as test library scope is read from ROBOT_LIBRARY_SCOPE. If ROBOT_LIBRARY_VERSION does not exist, information is tried to be read from __version attribute. These attributes must be class or module attributes, depending whether the library is implemented as a class or a module. For Java libraries the version attribute must be declared as static final.
An example Python module using version:
version = '0.1'
def keyword():
pass
A Java class using ROBOT_LIBRARY_VERSION:
public class VersionExample {
public static final String ROBOT_LIBRARY_VERSION = "1.0.2";
public void keyword() {
}
}
Specifying documentation format
Starting from Robot Framework 2.7.5, library documentation tool Libdoc supports documentation in multiple formats. If you want to use something else than Robot Framework's own documentation formatting, you can specify the format in the source code using ROBOTLIBRARY_DOC_FORMATattribute similarly as scope and version are set with their own ROBOT_LIBRARY* attributes.
The possible case-insensitive values for documentation format are ROBOT (default), HTML, TEXT (plain text), and reST (reStructuredText). Using the reST format requires the docutils module to be installed when documentation is generated.
Setting the documentation format is illustrated by the following Python and Java examples that use reStructuredText and HTML formats, respectively. See Documenting libraries section and Libdoc chapter for more information about documenting test libraries in general.
"""A library for documentation format demonstration purposes.
This documentation is created using reStructuredText__. Here is a link
to the only `Keyword`.
__ http://docutils.sourceforge.net
"""
ROBOT_LIBRARY_DOC_FORMAT = 'reST'
def keyword():
"""Nothing to see here. Not even in the table below.
======= ===== =====
Table here has
nothing to see.
======= ===== =====
"""
pass
/**
* A library for <i>documentation format</i> demonstration purposes.
* This documentation is created using <a href="http://www.w3.org/html">HTML</a>.
* Here is a link to the only Keyword
.
*/
public class DocFormatExample {
public static final String ROBOT_LIBRARY_DOC_FORMAT = "HTML";
/**<b>Nothing</b> to see here. Not even in the table below.
* <table>
* <tr><td>Table</td><td>here</td><td>has</td></tr>
* <tr><td>nothing</td><td>to</td><td>see.</td></tr>
* </table>
*/
public void keyword() {
}
}
Library acting as listener
Listener interface allows external listeners to get notifications about test execution. They are called, for example, when suites, tests, and keywords start and end. Sometimes getting such notifications is also useful for test libraries, and they can register a custom listener by usingROBOT_LIBRARY_LISTENER attribute. The value of this attribute should be an instance of the listener to use, possibly the library itself. For more information and examples see Test libraries as listeners section.
4.1.3 Creating static keywords
What methods are considered keywords
When the static library API is used, Robot Framework uses reflection to find out what public methods the library class or module implements. It will exclude all methods starting with an underscore, and with Java libraries also methods that are implemented only in java.lang.Object are ignored. All the methods that are not ignored are considered keywords. For example, the Python and Java libraries below implement single keyword My Keyword.
class MyLibrary:
def my_keyword(self, arg):
return self._helper_method(arg)
def _helper_method(self, arg):
return arg.upper()
public class MyLibrary {
public String myKeyword(String arg) {
return helperMethod(arg);
}
private String helperMethod(String arg) {
return arg.toUpperCase();
}
}
When the library is implemented as a Python module, it is also possible to limit what methods are keywords by using Python's all attribute. If all is used, only methods listed in it can be keywords. For example, the library below implements keywords Example Keyword and Second Example. Without all, it would implement also keywords Not Exposed As Keyword and Current Thread. The most important usage for all is making sure imported helper methods, such as current_thread in the example below, are not accidentally exposed as keywords.
from threading import current_thread
all = ['example_keyword', 'second_example']
def example_keyword():
if current_thread().name == 'MainThread':
print 'Running in main thread'
def second_example():
pass
def not_exposed_as_keyword():
pass
Note
Support for the all attribute is available from Robot Framework 2.5.5 onwards.
Keyword names
Keyword names used in the test data are compared with method names to find the method implementing these keywords. Name comparison is case-insensitive, and also spaces and underscores are ignored. For example, the method hello maps to the keyword name Hello, hello or even h e l l o. Similarly both the donothing and doNothing methods can be used as the _Do Nothing keyword in the test data.
Example Python library implemented as a module in the MyLibrary.py file:
def hello(name):
print "Hello, %s!" % name
def do_nothing():
pass
Example Java library implemented as a class in the MyLibrary.java file:
public class MyLibrary {
public void hello(String name) {
System.out.println("Hello, " + name + "!");
}
public void doNothing() {
}
}
The example below illustrates how the example libraries above can be used. If you want to try this yourself, make sure that the library is in the library search path.
Using simple example library | |||
---|---|---|---|
Setting | Value | Value | Value |
Library | MyLibrary |
Test Case | Action | Argument | Argument |
---|---|---|---|
My Test | Do Nothing | ||
Hello | world |
Keyword arguments
With a static and hybrid API, the information on how many arguments a keyword needs is got directly from the method that implements it. Libraries using the dynamic library API have other means for sharing this information, so this section is not relevant to them.
The most common and also the simplest situation is when a keyword needs an exact number of arguments. In this case, both the Python and Java methods simply take exactly those arguments. For example, a method implementing a keyword with no arguments takes no arguments either, a method implementing a keyword with one argument also takes one argument, and so on.
Example Python keywords taking different numbers of arguments:
def no_arguments():
print "Keyword got no arguments."
def one_argument(arg):
print "Keyword got one argument '%s'." % arg
def three_arguments(a1, a2, a3):
print "Keyword got three arguments '%s', '%s' and '%s'." % (a1, a2, a3)
Note
A major limitation with Java libraries using the static library API is that they do not support the named argument syntax. If this is a blocker, it is possible to either use Python or switch to the dynamic library API.
Default values to keywords
It is often useful that some of the arguments that a keyword uses have default values. Python and Java have different syntax for handling default values to methods, and the natural syntax of these languages can be used when creating test libraries for Robot Framework.
Default values with Python
In Python a method has always exactly one implementation and possible default values are specified in the method signature. The syntax, which is familiar to all Python programmers, is illustrated below:
def one_default(arg='default'):
print "Argument has value %s" % arg
def multiple_defaults(arg1, arg2='default 1', arg3='default 2'):
print "Got arguments %s, %s and %s" % (arg1, arg2, arg3)
The first example keyword above can be used either with zero or one arguments. If no arguments are given, arg gets the value default. If there is one argument, arg gets that value, and calling the keyword with more than one argument fails. In the second example, one argument is always required, but the second and the third one have default values, so it is possible to use the keyword with one to three arguments.
Using keywords with variable number of arguments | ||||
---|---|---|---|---|
Test Case | Action | Argument | Argument | Argument |
Defaults | One Default | |||
One Default | argument | |||
Multiple Defaults | required arg | |||
Multiple Defaults | required arg | optional | ||
Multiple Defaults | required arg | optional 1 | optional 2 |
Default values with Java
In Java one method can have several implementations with different signatures. Robot Framework regards all these implementations as one keyword, which can be used with different arguments. This syntax can thus be used to provide support for the default values. This is illustrated by the example below, which is functionally identical to the earlier Python example:
public void oneDefault(String arg) {
System.out.println("Argument has value " + arg);
}
public void oneDefault() {
oneDefault("default");
}
public void multipleDefaults(String arg1, String arg2, String arg3) {
System.out.println("Got arguments " + arg1 + ", " + arg2 + " and " + arg3);
}
public void multipleDefaults(String arg1, String arg2) {
multipleDefaults(arg1, arg2, "default 2");
}
public void multipleDefaults(String arg1) {
multipleDefaults(arg1, "default 1");
}
Variable number of arguments (*varargs)
Robot Framework supports also keywords that take any number of arguments. Similarly as with the default values, the actual syntax to use in test libraries is different in Python and Java.
Variable number of arguments with Python
Python supports methods accepting any number of arguments. The same syntax works in libraries and, as the examples below show, it can also be combined with other ways of specifying arguments:
def any_arguments(*args):
print "Got arguments:"
for arg in args:
print arg
def one_required(required, *others):
print "Required: %s\nOthers:" % required
for arg in others:
print arg
def also_defaults(req, def1="default 1", def2="default 2", *rest):
print req, def1, def2, rest
Using keywords with a variable number of arguments | ||||
---|---|---|---|---|
Test Case | Action | Argument | Argument | Argument |
Varargs | Any Arguments | |||
Any Arguments | argument | |||
Any Arguments | arg 1 | arg 2 | arg 2 | |
... | arg 4 | arg 5 | ||
One Required | required arg | |||
One Required | required arg | another arg | yet another | |
Also Defaults | required | |||
Also Defaults | required | these two | have defaults | |
Also Defaults | 1 | 2 | 3 | |
... | 4 | 5 | 6 |
Variable number of arguments with Java
Robot Framework supports Java varargs syntax for defining variable number of arguments. For example, the following two keywords are functionally identical to the above Python examples with same names:
public void anyArguments(String... varargs) {
System.out.println("Got arguments:");
for (String arg: varargs) {
System.out.println(arg);
}
}
public void oneRequired(String required, String... others) {
System.out.println("Required: " + required + "\nOthers:");
for (String arg: others) {
System.out.println(arg);
}
}
It is also possible to use variable number of arguments also by having an array or, starting from Robot Framework 2.8.3, java.util.List as the last argument, or second to last if free keyword arguments (**kwargs) are used. This is illustrated by the following examples that are functionally identical to the previous ones:
public void anyArguments(String[] varargs) {
System.out.println("Got arguments:");
for (String arg: varargs) {
System.out.println(arg);
}
}
public void oneRequired(String required, List<String> others) {
System.out.println("Required: " + required + "\nOthers:");
for (String arg: others) {
System.out.println(arg);
}
}
Note
Only java.util.List is supported as varargs, not any of its sub types.
The support for variable number of arguments with Java keywords has one limitation: it works only when methods have one signature. Thus it is not possible to have Java keywords with both default values and varargs. In addition to that, only Robot Framework 2.8 and newer support using varargs with library constructors.
Free keyword arguments (**kwargs)
Robot Framework 2.8 added the support for free keyword arguments using Python's **kwargs syntax. How to use the syntax in the test data is discussed in Free keyword arguments section under Creating test cases. In this section we take a look at how to actually use it in custom test libraries.
Free keyword arguments with Python
If you are already familiar how kwargs work with Python, understanding how they work with Robot Framework test libraries is rather simple. The example below shows the basic functionality:
def example_keyword(**stuff):
for name, value in stuff.items():
print name, value
Using keywords with **kwargs | ||||
---|---|---|---|---|
Test Case | Action | Argument | Argument | Argument |
Keyword Arguments | Example Keyword | hello=world | # Logs 'hello world'. | |
Example Keyword | foo=1 | bar=42 | # Logs 'foo 1' and 'bar 42'. |
Basically, all arguments at the end of the keyword call that use the named argument syntax name=value, and that do not match any other arguments, are passed to the keyword as kwargs. To avoid using a literal value like foo=quux as a free keyword argument, it must be escaped like foo\=quux.
The following example illustrates how normal arguments, varargs, and kwargs work together:
def various_args(arg, varargs, *kwargs):
print 'arg:', arg
for value in varargs:
print 'vararg:', value
for name, value in sorted(kwargs.items()):
print 'kwarg:', name, value
Using normal arguments, varargs, and kwargs together | |||||
---|---|---|---|---|---|
Test Case | Action | Argument | Argument | Argument | Argument |
Positional | Various Args | hello | world | # Logs 'arg: hello' and 'vararg: world'. | |
Named | Various Args | arg=value | # Logs 'arg: value'. | ||
Kwargs | Various Args | a=1 | b=2 | c=3 | # Logs 'kwarg: a 1', 'kwarg: b 2' and 'kwarg: c 3'. |
Various Args | c=3 | a=1 | b=2 | # Same as above. Order does not matter. | |
Positional and kwargs | Various Args | 1 | 2 | kw=3 | # Logs 'arg: 1', 'vararg: 2' and 'kwarg: kw 3'. |
Named and kwargs | Various Args | arg=value | hello=world | # Logs 'arg: value' and 'kwarg: hello world'. | |
Various Args | hello=world | arg=value | # Same as above. Order does not matter. |
For a real world example of using a signature exactly like in the above example, see Run Process and Start Keyword keywords in the Process library.
Free keyword arguments with Java
Starting from Robot Framework 2.8.3, also Java libraries support the free keyword arguments syntax. Java itself has no kwargs syntax, but keywords can have java.util.Map as the last argument to specify that they accept kwargs.
If a Java keyword accepts kwargs, Robot Framework will automatically pack all arguments in name=value syntax at the end of the keyword call into a Map and pass it to the keyword. For example, following example keywords can be used exactly like the previous Python examples:
public void exampleKeyword(Map<String, String> stuff):
for (String key: stuff.keySet())
System.out.println(key + " " + stuff.get(key));
public void variousArgs(String arg, List<String> varargs, Map<String, Object> kwargs):
System.out.println("arg: " + arg);
for (String varg: varargs)
System.out.println("vararg: " + varg);
for (String key: kwargs.keySet())
System.out.println("kwarg: " + key + " " + kwargs.get(key));
Note
The type of the kwargs argument must be exactly java.util.Map, not any of its sub types.
Note
Similarly as with the varargs support, a keyword supporting kwargs cannot have more than one signature.
Argument types
Normally keyword arguments come to Robot Framework as strings. If keywords require some other types, it is possible to either use variables or convert strings to required types inside keywords. With Java keywords base types are also coerced automatically.
Argument types with Python
Because arguments in Python do not have any type information, there is no possibility to automatically convert strings to other types when using Python libraries. Calling a Python method implementing a keyword with a correct number of arguments always succeeds, but the execution fails later if the arguments are incompatible. Luckily with Python it is simple to convert arguments to suitable types inside keywords:
def connect_to_host(address, port=25):
port = int(port)
# ...
Argument types with Java
Arguments to Java methods have types, and all the base types are handled automatically. This means that arguments that are normal strings in the test data are coerced to correct type at runtime. The types that can be coerced are:
- integer types (byte, short, int, long)
- floating point types (float and double)
- the boolean type
- object versions of the above types e.g. java.lang.Integer
The coercion is done for arguments that have the same or compatible type across all the signatures of the keyword method. In the following example, the conversion can be done for keywords doubleArgument and compatibleTypes, but not for conflictingTypes.
public void doubleArgument(double arg) {}
public void compatibleTypes(String arg1, Integer arg2) {}
public void compatibleTypes(String arg2, Integer arg2, Boolean arg3) {}
public void conflictingTypes(String arg1, int arg2) {}
public void conflictingTypes(int arg1, String arg2) {}
The coercion works with the numeric types if the test data has a string containing a number, and with the boolean type the data must contain either string true or false. Coercion is only done if the original value was a string from the test data, but it is of course still possible to use variables containing correct types with these keywords. Using variables is the only option if keywords have conflicting signatures.
Using automatic type coercion | ||||
---|---|---|---|---|
Test Case | Action | Argument | Argument | Argument |
Coercion | Double Argument | 3.14 | ||
Double Argument | 2e16 | # scientific notation | ||
Compatible Types | Hello, world! | 1234 | ||
Compatible Types | Hi again! | -10 | true | |
No Coercion | Double Argument | ${3.14} | ||
Conflicting Types | 1 | ${2} | # must use variables | |
Conflicting Types | ${1} | 2 |
Starting from Robot Framework 2.8, argument type coercion works also with Java library constructors.
Using decorators
When writing static keywords, it is sometimes useful to modify them with Python's decorators. However, decorators modify function signatures, and can confuse Robot Framework's introspection when determining which arguments keywords accept. This is especially problematic when creating library documentation with Libdoc and when using RIDE. To avoid this issue, either do not use decorators, or use the handy decorator module to create signature-preserving decorators.
4.1.4 Communicating with Robot Framework
After a method implementing a keyword is called, it can use any mechanism to communicate with the system under test. It can then also send messages to Robot Framework's log file, return information that can be saved to variables and, most importantly, report if the keyword passed or not.
Reporting keyword status
Reporting keyword status is done simply using exceptions. If an executed method raises an exception, the keyword status is FAIL, and if it returns normally, the status is PASS.
The error message shown in logs, reports and the console is created from the exception type and its message. With generic exceptions (for example, AssertionError, Exception, and RuntimeError), only the exception message is used, and with others, the message is created in the formatExceptionType: Actual message.
Starting from Robot Framework 2.8.2, it is possible to avoid adding the exception type as a prefix to failure message also with non generic exceptions. This is done by adding a special ROBOT_SUPPRESS_NAME attribute with value True to your exception.
Python:
class MyError(RuntimeError):
ROBOT_SUPPRESS_NAME = True
Java:
public class MyError extends RuntimeException {
public static final boolean ROBOT_SUPPRESS_NAME = true;
}
In all cases, it is important for the users that the exception message is as informative as possible.
HTML in error messages
Starting from Robot Framework 2.8, it is also possible have HTML formatted error messages by starting the message with text HTML:
raise AssertionError("HTML <a href='robotframework.org'>Robot Framework</a> rulez!!")
This method can be used both when raising an exception in a library, like in the example above, and when users provide an error message in the test data.
Cutting long messages automatically
If the error message is longer than 40 lines, it will be automatically cut from the middle to prevent reports from getting too long and difficult to read. The full error message is always shown in the log message of the failed keyword.
Tracebacks
The traceback of the exception is also logged using DEBUG log level. These messages are not visible in log files by default because they are very rarely interesting for normal users. When developing libraries, it is often a good idea to run tests using --loglevel DEBUG.
Stopping test execution
Starting from Robot Framework 2.5, it is possible to fail a test case so that the whole test execution is stopped. This is done simply by having a special ROBOT_EXIT_ON_FAILURE attribute with True value set on the exception raised from the keyword. This is illustrated in the examples below.
Python:
class MyFatalError(RuntimeError):
ROBOT_EXIT_ON_FAILURE = True
Java:
public class MyFatalError extends RuntimeException {
public static final boolean ROBOT_EXIT_ON_FAILURE = true;
}
Continuing test execution despite of failures
Starting from Robot Framework 2.5, it is possible to continue test execution even when there are failures. The way to signal this from test libraries is adding a special ROBOT_CONTINUE_ON_FAILURE attribute with True value to the exception used to communicate the failure. This is demonstrated by the examples below.
Python:
class MyContinuableError(RuntimeError):
ROBOT_CONTINUE_ON_FAILURE = True
Java:
public class MyContinuableError extends RuntimeException {
public static final boolean ROBOT_CONTINUE_ON_FAILURE = true;
}
Logging information
Exception messages are not the only way to give information to the users. In addition to them, methods can also send messages to log files simply by writing to the standard output stream (stdout) or to the standard error stream (stderr), and they can even use different log levels. Another, and often better, logging possibility is using the programmatic logging APIs.
By default, everything written by a method into the standard output is written to the log file as a single entry with the log level INFO. Messages written into the standard error are handled similarly otherwise, but they are echoed back to the original stderr after the keyword execution has finished. It is thus possible to use the stderr if you need some messages to be visible on the console where tests are executed.
Using log levels
To use other log levels than INFO, or to create several messages, specify the log level explicitly by embedding the level into the message in the format LEVEL Actual log message, where LEVEL must be in the beginning of a line and LEVEL is one of the available logging levels TRACE, DEBUG,INFO, WARN, FAIL and HTML.
Warnings
Messages with WARN level are automatically written into the console and into separate Test Execution Errors section in log files. This makes warnings more visible than other messages and allows using them for reporting important but non-critical problems to users.
Logging HTML
Everything normally logged by the library will be converted into a format that can be safely represented as HTML. For example, <b>foo</b> will be displayed in the log exactly like that and not as foo. If libraries want to use formatting, links, display images and so on, they can use a special pseudo log level HTML. Robot Framework will write these messages directly into the log with the INFO level, so they can use any HTML syntax they want. Notice that this feature needs to be used with care, because, for example, one badly placed </table> tag can ruin the log file quite badly.
When using the public logging API, various logging methods have optional html attribute that can be set to True to enable logging in HTML format.
Timestamps
By default messages logged via the standard output or error streams get their timestamps when the executed keyword ends. This means that the timestamps are not accurate and debugging problems especially with longer running keywords can be problematic.
Starting from Robot Framework 2.6, keywords have a possibility to add an accurate timestamp to the messages they log if there is a need. The timestamp must be given as milliseconds since the Unix epoch and it must be placed after the log level separated from it with a colon:
INFO:1308435758660 Message with timestamp
HTML:1308435758661 <b>HTML</b> message with timestamp
As illustrated by the examples below, adding the timestamp is easy both using Python and Java. If you are using Python, it is, however, even easier to get accurate timestamps using the programmatic logging APIs. A big benefit of adding timestamps explicitly is that this approach works also with the remote library interface.
Python:
import time
def example_keyword():
print 'INFO:%d Message with timestamp' % (time.time()*1000)
Java:
public void exampleKeyword() {
System.out.println("INFO:" + System.currentTimeMillis() + " Message with timestamp");
}
Logging to console
If libraries need to write something to the console they have several options. As already discussed, warnings and all messages written to the standard error stream are written both to the log file and to the console. Both of these options have a limitation that the messages end up to the console only after the currently executing keyword finishes. A bonus is that these approaches work both with Python and Java based libraries.
Another option, that is only available with Python, is writing messages to sys.stdout or sys.stderr. When using this approach, messages are written to the console immediately and are not written to the log file at all:
import sys
def my_keyword(arg):
sys.stdout.write('Got arg %s\n' % arg)
The final option is using the public logging API:
from robot.api import logger
def log_to_console(arg):
logger.console('Got arg %s' % arg)
def log_to_console_and_log_file(arg)
logger.info('Got arg %s' % arg, also_console=True)
Logging example
In most cases, the INFO level is adequate. The levels below it, DEBUG and TRACE, are useful for writing debug information. These messages are normally not shown, but they can facilitate debugging possible problems in the library itself. The WARN level can be used to make messages more visible and HTML is useful if any kind of formatting is needed.
The following examples clarify how logging with different levels works. Java programmers should regard the code print 'message' as pseudocode meaning System.out.println("message");.
print 'Hello from a library.'
print 'WARN Warning from a library.'
print 'INFO Hello again!'
print 'This will be part of the previous message.'
print 'INFO This is a new message.'
print 'INFO This is <b>normal text</b>.'
print 'HTML This is <b>bold</b>.'
print 'HTML <a href="http://robotframework.org">Robot Framework</a>'
16:18:42.123 | INFO | Hello from a library. |
---|---|---|
16:18:42.123 | WARN | Warning from a library. |
16:18:42.123 | INFO | Hello again!This will be part of the previous message. |
16:18:42.123 | INFO | This is a new message. |
16:18:42.123 | INFO | This is <b>normal text</b>. |
16:18:42.123 | INFO | This is bold. |
16:18:42.123 | INFO | Robot Framework |
Programmatic logging APIs
Programmatic APIs provide somewhat cleaner way to log information than using the standard output and error streams. Currently these interfaces are available only to Python bases test libraries.
Public logging API
Robot Framework 2.6 has a new Python based logging API for writing messages to the log file and to the console. Test libraries can use this API like logger.info('My message') instead of logging through the standard output like print 'INFO My message'. In addition to a programmatic interface being a lot cleaner to use, this API has a benefit that the log messages have accurate timestamps.
The public logging API is thoroughly documented as part of the API documentation at https://robot-framework.readthedocs.org. Below is a simple usage example:
from robot.api import logger
def my_keyword(arg):
logger.debug('Got argument %s' % arg)
do_something()
logger.info('<i>This</i> is a boring example', html=True)
logger.console('Hello, console!')
An obvious limitation is that test libraries using this logging API have a dependency to Robot Framework. Before version 2.8.7 Robot also had to be running for the logging to work. Starting from Robot Framework 2.8.7 if Robot is not running the messages are redirected automatically to Python's standard logging module.
Using Python's standard logging module
In addition to the new public logging API, Robot Framework 2.6 also added a built-in support to Python's standard logging module. This works so that all messages that are received by the root logger of the module are automatically propagated to Robot Framework's log file. Also this API produces log messages with accurate timestamps, but logging HTML messages or writing messages to the console are not supported. A big benefit, illustrated also by the simple example below, is that using this logging API creates no dependency to Robot Framework.
import logging
def my_keyword(arg):
logging.debug('Got argument %s' % arg)
do_something()
logging.info('This is a boring example')
The logging module has slightly different log levels than Robot Framework. Its levels DEBUG and INFO are mapped directly to the matching Robot Framework log levels and WARNING and everything above is mapped to WARN. Custom levels below DEBUG are mapped to DEBUG and everything betweenDEBUG and WARNING is mapped to INFO.
Logging during library initialization
Libraries can also log during the test library import and initialization. These messages do not appear in the log file like the normal log messages, but are instead written to the syslog. This allows logging any kind of useful debug information about the library initialization. Messages logged using the WARN level are also visible in the test execution errors section in the log file.
Logging during the import and initialization is possible both using the standard output and error streams and the programmatic logging APIs. Both of these are demonstrated below.
Java library logging via stdout during initialization:
public class LoggingDuringInitialization {
public LoggingDuringInitialization() {
System.out.println("INFO Initializing library");
}
public void keyword() {
// ...
}
}
Python library logging using the logging API during import:
from robot.api import logger
logger.debug("Importing library")
def keyword():
# ...
Note
If you log something during initialization, i.e. in Python init or in Java constructor, the messages may be logged multiple times depending on the test library scope.
Note
The support for writing log messages to the syslog during the library initialization is a new feature in Robot Framework 2.6.
Returning values
The final way for keywords to communicate back to the core framework is returning information retrieved from the system under test or generated by some other means. The returned values can be assigned to variables in the test data and then used as inputs for other keywords, even from different test libraries.
Values are returned using the return statement both from the Python and Java methods. Normally, one value is assigned into one scalar variable, as illustrated in the example below. This example also illustrates that it is possible to return any objects and to use extended variable syntax to access object attributes.
from mymodule import MyObject
def return_string():
return "Hello, world!"
def return_object(name):
return MyObject(name)
Return one value from keywords | ||
---|---|---|
${string} = | Return String | |
Should Be Equal | ${string} | Hello, world! |
${object} = | Return Object | Robot |
Should Be Equal | ${object.name} | Robot |
Keywords can also return values so that they can be assigned into several scalar variables at once, into a list variable, or into scalar variables and a list variable. All these usages require that returned values are Python lists or tuples or in Java arrays, Lists, or Iterators.
def return_two_values():
return 'first value', 'second value'
def return_multiple_values():
return ['a', 'list', 'of', 'strings']
Returning multiple values | |||
---|---|---|---|
${var1} | ${var2} = | Return Two Values | |
Should Be Equal | ${var1} | first value | |
Should Be Equal | ${var2} | second value | |
@{list} = | Return Two Values | ||
Should Be Equal | @{list}[0] | first value | |
Should Be Equal | @{list}[1] | second value | |
${s1} | ${s2} | @{li} = | Return Multiple Values |
Should Be Equal | ${s1} ${s2} | a list | |
Should Be Equal | @{li}[0] @{li}[1] | of strings |
Communication when using threads
If a library uses threads, it should generally communicate with the framework only from the main thread. If a worker thread has, for example, a failure to report or something to log, it should pass the information first to the main thread, which can then use exceptions or other mechanisms explained in this section for communication with the framework.
This is especially important when threads are run on background while other keywords are running. Results of communicating with the framework in that case are undefined and can in the worst case cause a crash or a corrupted output file. If a keyword starts something on background, there should be another keyword that checks the status of the worker thread and reports gathered information accordingly.
Messages logged by non-main threads using the normal logging methods from programmatic logging APIs are silently ignored starting from Robot Framework 2.6.2.
There is also a BackgroundLogger in separate robotbackgroundlogger project, with a similar API as the standard robot.api.logger. Normal logging methods will ignore messages from other than main thread, but the BackgroundLogger will save the background messages so that they can be later logged to Robot's log.
4.1.5 Distributing test libraries
Documenting libraries
A test library without documentation about what keywords it contains and what those keywords do is rather useless. To ease maintenance, it is highly recommended that library documentation is included in the source code and generated from it. Basically, that means using docstrings with Python and Javadoc with Java, as in the examples below.
class MyLibrary:
"""This is an example library with some documentation."""
def keyword_with_short_documentation(self, argument):
"""This keyword has only a short documentation"""
pass
def keyword_with_longer_documentation(self):
"""First line of the documentation is here.
Longer documentation continues here and it can contain
multiple lines or paragraphs.
"""
pass
/**
* This is an example library with some documentation.
*/
public class MyLibrary {
/**
* This keyword has only a short documentation
*/
public void keywordWithShortDocumentation(String argument) {
}
/**
* First line of the documentation is here.
* Longer documentation continues here and it can contain
* multiple lines or paragraphs.
*/
public void keywordWithLongerDocumentation() {
}
}
Both Python and Java have tools for creating an API documentation of a library documented as above. However, outputs from these tools can be slightly technical for some users. Another alternative is using Robot Framework's own documentation tool Libdoc. This tool can create a library documentation from both Python and Java libraries using the static library API, such as the ones above, but it also handles libraries using the dynamic library API and hybrid library API.
The first line of a keyword documentation is used for a special purpose and should contain a short overall description of the keyword. It is used as a short documentation, for example as a tool tip, by Libdoc and also shown in the test logs. However, the latter does not work with Java libraries using the static API, because their documentations are lost in compilation and not available at runtime.
By default documentation is considered to follow Robot Framework's documentation formatting rules. This simple format allows often used styles like bold and italic, tables, lists, links, etc. Starting from Robot Framework 2.7.5, it is possible to use also HTML, plain text andreStructuredText formats. See Specifying documentation format section for information how to set the format in the library source code and Libdoc chapter for more information about the formats in general.
Note
If you want to use non-ASCII characters in the documentation of Python libraries, you must either use UTF-8 as your source code encoding or create docstrings as Unicode.
Testing libraries
Any non-trivial test library needs to be thoroughly tested to prevent bugs in them. Of course, this testing should be automated to make it easy to rerun tests when libraries are changed.
Both Python and Java have excellent unit testing tools, and they suite very well for testing libraries. There are no major differences in using them for this purpose compared to using them for some other testing. The developers familiar with these tools do not need to learn anything new, and the developers not familiar with them should learn them anyway.
It is also easy to use Robot Framework itself for testing libraries and that way have actual end-to-end acceptance tests for them. There are plenty of useful keywords in the BuiltIn library for this purpose. One worth mentioning specifically is Run Keyword And Expect Error, which is useful for testing that keywords report errors correctly.
Whether to use a unit- or acceptance-level testing approach depends on the context. If there is a need to simulate the actual system under test, it is often easier on the unit level. On the other hand, acceptance tests ensure that keywords do work through Robot Framework. If you cannot decide, of course it is possible to use both the approaches.
Packaging libraries
After a library is implemented, documented, and tested, it still needs to be distributed to the users. With simple libraries consisting of a single file, it is often enough to ask the users to copy that file somewhere and set the library search path accordingly. More complicated libraries should be packaged to make the installation easier.
Since libraries are normal programming code, they can be packaged using normal packaging tools. With Python, good options include distutils, contained by Python's standard library, and the newer setuptools. A benefit of these tools is that library modules are installed into a location that is automatically in the library search path.
When using Java, it is natural to package libraries into a JAR archive. The JAR package must be put into the library search path before running tests, but it is easy to create a start-up script that does that automatically.
Deprecating keywords
Sometimes there is a need to replace existing keywords with new ones or remove them altogether. Just informing the users about the change may not always be enough, and it is more efficient to get warnings at runtime. To support that, Robot Framework has a capability to mark keywordsdeprecated. This makes it easier to find old keywords from the test data and remove or replace them.
Keywords are deprecated by starting their documentation with DEPRECATED. When these keywords are executed, a warning containing rest of the short documentation is written both into the console and into separate Test Execution Errors section in log files. For example, if following keyword is executed there will be a warning like shown below in the log file.
def example_keyword(argument):
"""DEPRECATED Use keyword Other Keyword
instead.
This keyword does something to given argument
and returns the result.
"""
return do_something(argument)
20080911 16:00:22.650 | WARN | Keyword 'SomeLibrary.Example Keyword' is deprecated. Use keyword Other Keyword instead. |
---|---|---|
This deprecation system works with most test libraries and also with user keywords. The only exception are keywords implemented in a Java test library that uses the static library interface because their documentation is not available at runtime. With such keywords, it possible to use user keywords as wrappers and deprecate them.
There is a plan to implement a tool that can use the deprecation information for automatically replacing deprecated keywords. The tool will most likely get the name of the new keyword from the documentation so that it searches words inside backticks (`). Thus it would find _Other Keyword_from the earlier example. Note that Libdoc also automatically creates internal links using the same syntax.
4.1.6 Dynamic library API
The dynamic API is in most ways similar to the static API. For example, reporting the keyword status, logging, and returning values works exactly the same way. Most importantly, there are no differences in importing dynamic libraries and using their keywords compared to other libraries. In other words, users do not need to know what APIs their libraries use.
Only differences between static and dynamic libraries are how Robot Framework discovers what keywords a library implements, what arguments and documentation these keywords have, and how the keywords are actually executed. With the static API, all this is done using reflection (except for the documentation of Java libraries), but dynamic libraries have special methods that are used for these purposes.
One of the benefits of the dynamic API is that you have more flexibility in organizing your library. With the static API, you must have all keywords in one class or module, whereas with the dynamic API, you can, for example, implement each keyword as a separate class. This use case is not so important with Python, though, because its dynamic capabilities and multi-inheritance already give plenty of flexibility, and there is also possibility to use the hybrid library API.
Another major use case for the dynamic API is implementing a library so that it works as proxy for an actual library possibly running on some other process or even on another machine. This kind of a proxy library can be very thin, and because keyword names and all other information is got dynamically, there is no need to update the proxy when new keywords are added to the actual library.
This section explains how the dynamic API works between Robot Framework and dynamic libraries. It does not matter for Robot Framework how these libraries are actually implemented (for example, how calls to the run_keyword method are mapped to a correct keyword implementation), and many different approaches are possible. However, if you use Java, you may want to examine JavalibCore before implementing your own system. This collection of reusable tools supports several ways of creating keywords, and it is likely that it already has a mechanism that suites your needs.
Getting keyword names
Dynamic libraries tell what keywords they implement with the get_keyword_names method. The method also has the alias getKeywordNames that is recommended when using Java. This method cannot take any arguments, and it must return a list or array of strings containing the names of the keywords that the library implements.
If the returned keyword names contain several words, they can be returned separated with spaces or underscores, or in the camelCase format. For example, ['first keyword', 'second keyword'], ['firstkeyword', 'second_keyword'], and ['firstKeyword', 'secondKeyword'] would all be mapped to keywords _First Keyword and Second Keyword.
Dynamic libraries must always have this method. If it is missing, or if calling it fails for some reason, the library is considered a static library.
Running keywords
Dynamic libraries have a special run_keyword (alias runKeyword) method for executing their keywords. When a keyword from a dynamic library is used in the test data, Robot Framework uses the library's run_keyword method to get it executed. This method takes two or three arguments. The first argument is a string containing the name of the keyword to be executed in the same format as returned by get_keyword_names. The second argument is a list or array of arguments given to the keyword in the test data.
The optional third argument is a dictionary (map in Java) that gets possible free keyword arguments (**kwargs) passed to the keyword. See free keyword arguments with dynamic libraries section for more details about using kwargs with dynamic test libraries.
After getting keyword name and arguments, the library can execute the keyword freely, but it must use the same mechanism to communicate with the framework as static libraries. This means using exceptions for reporting keyword status, logging by writing to the standard output or by using provided logging APIs, and using the return statement in run_keyword for returning something.
Every dynamic library must have both the get_keyword_names and run_keyword methods but rest of the methods in the dynamic API are optional. The example below shows a working, albeit trivial, dynamic library implemented in Python.
class DynamicExample:
def get_keyword_names(self):
return ['first keyword', 'second keyword']
def run_keyword(self, name, args):
print "Running keyword '%s' with arguments %s." % (name, args)
Getting keyword arguments
If a dynamic library only implements the getkeyword_names and run_keyword methods, Robot Framework does not have any information about the arguments that the implemented keywords need. For example, both _First Keyword and Second Keyword in the example above could be used with any number of arguments. This is problematic, because most real keywords expect a certain number of keywords, and under these circumstances they would need to check the argument counts themselves.
Dynamic libraries can tell Robot Framework what arguments the keywords it implements expect by using the get_keyword_arguments (alias getKeywordArguments) method. This method takes the name of a keyword as an argument, and returns a list or array of strings containing the arguments accepted by that keyword.
Similarly as static keywords, dynamic keywords can require any number of arguments, have default values, and accept variable number of arguments and free keyword arguments. The syntax for how to represent all these different variables is explained in the following table. Note that the examples use Python syntax for lists, but Java developers should use Java lists or String arrays instead.
Representing different arguments with get_keyword_arguments | |||
---|---|---|---|
Expected arguments | How to represent | Examples | Limits (min/max) |
No arguments | Empty list. | [] | 0/0 |
One or more argument | List of strings containing argument names. | ['one_argument'] | 1/1 |
Default values for arguments | Default values separated from names with =. Default values are always considered to be strings. | ['arg=default value'] | 0/1 |
Variable number of arguments (varargs) | Last (or second last with kwargs) argument has * before its name. | ['*varargs'] | 0/any |
Free keyword arguments (kwargs) | Last arguments has ** before its name. | ['**kwargs'] | 0/0 |
When the get_keyword_arguments is used, Robot Framework automatically calculates how many positional arguments the keyword requires and does it support free keyword arguments or not. If a keyword is used with invalid arguments, an error occurs and run_keyword is not even called.
The actual argument names and default values that are returned are also important. They are needed for named argument support and the Libdoc tool needs them to be able to create a meaningful library documentation.
If get_keyword_arguments is missing or returns None or null for a certain keyword, that keyword gets an argument specification accepting all arguments. This automatic argument spec is either [varargs, **kwargs] or [varargs], depending does run_keyword support kwargs by having three arguments or not.
Getting keyword documentation
The final special method that dynamic libraries can implement is get_keyword_documentation (alias getKeywordDocumentation). It takes a keyword name as an argument and, as the method name implies, returns its documentation as a string.
The returned documentation is used similarly as the keyword documentation string with static libraries implemented with Python. The main use case is getting keywords' documentations into a library documentation generated by Libdoc. Additionally, the first line of the documentation (until the first \n) is shown in test logs.
Getting general library documentation
The get_keyword_documentation method can also be used for specifying overall library documentation. This documentation is not used when tests are executed, but it can make the documentation generated by Libdoc much better.
Dynamic libraries can provide both general library documentation and documentation related to taking the library into use. The former is got by calling getkeyworddocumentation with special value intro, and the latter is got using value __init. How the documentation is presented is best tested with Libdoc in practice.
Python based dynamic libraries can also specify the general library documentation directly in the code as the docstring of the library class and its init method. If a non-empty documentation is got both directly from the code and from the get_keyword_documentation method, the latter has precedence.
Note
Getting general library documentation is supported in Robot Framework 2.6.2 and newer.
Named argument syntax with dynamic libraries
Starting from Robot Framework 2.8, also the dynamic library API supports the named argument syntax. Using the syntax works based on the argument names and default values got from the library using the get_keyword_arguments method.
For the most parts, the named arguments syntax works with dynamic keywords exactly like it works with any other keyword supporting it. The only special case is the situation where a keyword has multiple arguments with default values, and only some of the latter ones are given. In that case the framework fills the skipped optional arguments based on the default values returned by the get_keyword_arguments method.
Using the named argument syntax with dynamic libraries is illustrated by the following examples. All the examples use a keyword Dynamic that has been specified to have argument specification [arg1, arg2=xxx, arg3=yyy]. The last column shows the arguments that the keyword is actually called with.
Using named argument syntax with a dynamic keyword | |||||
---|---|---|---|---|---|
Test Case | Action | Argument | Argument | Argument | Called With |
Only positional | Dynamic | a | # [a] | ||
Dynamic | a | b | # [a, b] | ||
Dynamic | a | b | c | # [a, b, c] | |
Named | Dynamic | a | arg2=b | # [a, b] | |
Dynamic | a | b | arg3=c | # [a, b, c] | |
Dynamic | a | arg2=b | arg3=c | # [a, b, c] | |
Dynamic | arg1=a | arg2=b | arg3=c | # [a, b, c] | |
Fill skipped | Dynamic | a | arg3=c | # [a, xxx, c] |
Free keyword arguments with dynamic libraries
Starting from Robot Framework 2.8.2, dynamic libraries can also support free keyword arguments (**kwargs). A mandatory precondition for this support is that the run_keyword method takes three arguments: the third one will get kwargs when they are used. Kwargs are passed to the keyword as a dictionary (Python) or Map (Java).
What arguments a keyword accepts depends on what get_keyword_arguments returns for it. If the last argument starts with **, that keyword is recognized to accept kwargs.
Using the free keyword argument syntax with dynamic libraries is illustrated by the following examples. All the examples use a keyword Dynamic that has been specified to have argument specification [arg1=xxx, arg2=yyy, **kwargs]. The last column shows the arguments that the keyword is actually called with.
Using free keyword arguments with a dynamic keyword | |||||
---|---|---|---|---|---|
Test Case | Action | Argument | Argument | Argument | Called With |
No arguments | Dynamic | # [], {} | |||
Only positional | Dynamic | a | # [a], {} | ||
Dynamic | a | b | # [a, b], {} | ||
Only kwargs | Dynamic | a=1 | # [], {a: 1} | ||
Dynamic | a=1 | b=2 | c=3 | # [], {a: 1, b: 2, c: 3} | |
Positional and kwargs | Dynamic | a | b=2 | # [a], {b: 2} | |
Dynamic | a | b=2 | c=3 | # [a], {b: 2, c: 3} | |
Named and kwargs | Dynamic | arg1=a | b=2 | # [a], {b: 2} | |
Dynamic | arg2=a | b=2 | c=3 | # [xxx, a], {b: 2, c: 3} |
Summary
All special methods in the dynamic API are listed in the table below. Method names are listed in the underscore format, but their camelCase aliases work exactly the same way.
All special methods in the dynamic API | ||
---|---|---|
Name | Arguments | Purpose |
get_keyword_names | Return names of the implemented keywords. | |
run_keyword | name, arguments, kwargs | Execute the specified keyword with given arguments. kwargs is optional. |
get_keyword_arguments | name | Return keywords' argument specifications. Optional method. |
get_keyword_documentation | name | Return keywords' and library's documentation. Optional method. |
It is possible to write a formal interface specification in Java as below. However, remember that libraries do not need to implement any explicit interface, because Robot Framework directly checks with reflection if the library has the required get_keyword_names and run_keyword methods or their camelCase aliases. Additionally, get_keyword_arguments and get_keyword_documentation are completely optional.
public interface RobotFrameworkDynamicAPI {
List<String> getKeywordNames();
Object runKeyword(String name, List arguments);
Object runKeyword(String name, List arguments, Map kwargs);
List<String> getKeywordArguments(String name);
String getKeywordDocumentation(String name);
}
Note
In addition to using List, it is possible to use also arrays like Object[] or String[].
A good example of using the dynamic API is Robot Framework's own Remote library.
4.1.7 Hybrid library API
The hybrid library API is, as its name implies, a hybrid between the static API and the dynamic API. Just as with the dynamic API, it is possible to implement a library using the hybrid API only as a class.
Getting keyword names
Keyword names are got in the exactly same way as with the dynamic API. In practice, the library needs to have the get_keyword_names or getKeywordNames method returning a list of keyword names that the library implements.
Running keywords
In the hybrid API, there is no run_keyword method for executing keywords. Instead, Robot Framework uses reflection to find methods implementing keywords, similarly as with the static API. A library using the hybrid API can either have those methods implemented directly or, more importantly, it can handle them dynamically.
In Python, it is easy to handle missing methods dynamically with the getattr method. This special method is probably familiar to most Python programmers and they can immediately understand the following example. Others may find it easier to consult Python Reference Manual first.
from somewhere import external_keyword
class HybridExample:
def get_keyword_names(self):
return ['my_keyword', 'external_keyword']
def my_keyword(self, arg):
print "My Keyword called with '%s'" % arg
def getattr(self, name):
if name == 'external_keyword':
return external_keyword
raise AttributeError("Non-existing attribute '%s'" % name)
Note that getattr does not execute the actual keyword like run_keyword does with the dynamic API. Instead, it only returns a callable object that is then executed by Robot Framework.
Another point to be noted is that Robot Framework uses the same names that are returned from get_keyword_names for finding the methods implementing them. Thus the names of the methods that are implemented in the class itself must be returned in the same format as they are defined. For example, the library above would not work correctly, if get_keyword_names returned My Keyword instead of my_keyword.
The hybrid API is not very useful with Java, because it is not possible to handle missing methods with it. Of course, it is possible to implement all the methods in the library class, but that brings few benefits compared to the static API.
Getting keyword arguments and documentation
When this API is used, Robot Framework uses reflection to find the methods implementing keywords, similarly as with the static API. After getting a reference to the method, it searches for arguments and documentation from it, in the same way as when using the static API. Thus there is no need for special methods for getting arguments and documentation like there is with the dynamic API.
Summary
When implementing a test library in Python, the hybrid API has the same dynamic capabilities as the actual dynamic API. A great benefit with it is that there is no need to have special methods for getting keyword arguments and documentation. It is also often practical that the only real dynamic keywords need to be handled in getattr and others can be implemented directly in the main library class.
Because of the clear benefits and equal capabilities, the hybrid API is in most cases a better alternative than the dynamic API when using Python. One notable exception is implementing a library as a proxy for an actual library implementation elsewhere, because then the actual keyword must be executed elsewhere and the proxy can only pass forward the keyword name and arguments.
A good example of using the hybrid API is Robot Framework's own Telnet library.
4.1.8 Using Robot Framework's internal modules
Test libraries implemented with Python can use Robot Framework's internal modules, for example, to get information about the executed tests and the settings that are used. This powerful mechanism to communicate with the framework should be used with care, though, because all Robot Framework's APIs are not meant to be used by externally and they might change radically between different framework versions.
Available APIs
Starting from Robot Framework 2.7, API documentation is hosted separately at the excellent Read the Docs service. If you are unsure how to use certain API or is using them forward compatible, please send a question to mailing list.
Using BuiltIn library
The safest API to use are methods implementing keywords in the BuiltIn library. Changes to keywords are rare and they are always done so that old usage is first deprecated. One of the most useful methods is replace_variables which allows accessing currently available variables. The following example demonstrates how to get ${OUTPUT_DIR} which is one of the many handy automatic variables. It is also possible to set new variables from libraries using set_test_variable, set_suite_variable and set_global_variable.
import os.path
from robot.libraries.BuiltIn import BuiltIn
def do_something(argument):
output = do_something_that_creates_a_lot_of_output(argument)
outputdir = BuiltIn().replace_variables('${OUTPUTDIR}')
path = os.path.join(outputdir, 'results.txt')
f = open(path, 'w')
f.write(output)
f.close()
print 'HTML Output written to <a href="results.txt">results.txt</a>'
The only catch with using methods from BuiltIn is that all run_keyword method variants must be handled specially. Methods that use run_keyword methods have to be registered as run keywords themselves using register_run_keyword method in BuiltIn module. This method's documentation explains why this needs to be done and obviously also how to do it.
4.1.9 Extending existing test libraries
This section explains different approaches how to add new functionality to existing test libraries and how to use them in your own libraries otherwise.
Modifying original source code
If you have access to the source code of the library you want to extend, you can naturally modify the source code directly. The biggest problem of this approach is that it can be hard for you to update the original library without affecting your changes. For users it may also be confusing to use a library that has different functionality than the original one. Repackaging the library may also be a big extra task.
This approach works extremely well if the enhancements are generic and you plan to submit them back to the original developers. If your changes are applied to the original library, they are included in the future releases and all the problems discussed above are mitigated. If changes are non-generic, or you for some other reason cannot submit them back, the approaches explained in the subsequent sections probably work better.
Using inheritance
Another straightforward way to extend an existing library is using inheritance. This is illustrated by the example below that adds new Title Should Start With keyword to the SeleniumLibrary. This example uses Python, but you can obviously extend an existing Java library in Java code the same way.
from SeleniumLibrary import SeleniumLibrary
class ExtendedSeleniumLibrary(SeleniumLibrary):
def title_should_start_with(self, expected):
title = self.get_title()
if not title.startswith(expected):
raise AssertionError("Title '%s' did not start with '%s'"
% (title, expected))
A big difference with this approach compared to modifying the original library is that the new library has a different name than the original. A benefit is that you can easily tell that you are using a custom library, but a big problem is that you cannot easily use the new library with the original. First of all your new library will have same keywords as the original meaning that there is always conflict. Another problem is that the libraries do not share their state.
This approach works well when you start to use a new library and want to add custom enhancements to it from the beginning. Otherwise other mechanisms explained in this section are probably better.
Using other libraries directly
Because test libraries are technically just classes or modules, a simple way to use another library is importing it and using its methods. This approach works great when the methods are static and do not depend on the library state. This is illustrated by the earlier example that uses Robot Framework's BuiltIn library.
If the library has state, however, things may not work as you would hope. The library instance you use in your library will not be the same as the framework uses, and thus changes done by executed keywords are not visible to your library. The next section explains how to get an access to the same library instance that the framework uses.
Getting active library instance from Robot Framework
Robot Framework 2.5.2 added new BuiltIn keyword Get Library Instance that can be used to get the currently active library instance from the framework itself. The library instance returned by this keyword is the same as the framework itself uses, and thus there is no problem seeing the correct library state. Although this functionality is available as a keyword, it is typically used in test libraries directly by importing the BuiltIn library class as discussed earlier. The following example illustrates how to implement the same Title Should Start With keyword as in the earlier example aboutusing inheritance.
from robot.libraries.BuiltIn import BuiltIn
def title_should_start_with(expected):
seleniumlib = BuiltIn().get_library_instance('SeleniumLibrary')
title = seleniumlib.get_title()
if not title.startswith(expected):
raise AssertionError("Title '%s' did not start with '%s'"
% (title, expected))
This approach is clearly better than importing the library directly and using it when the library has a state. The biggest benefit over inheritance is that you can use the original library normally and use the new library in addition to it when needed. That is demonstrated in the example below where the code from the previous examples is expected to be available in a new library SeLibExtensions.
Using library and another library that extends it | |||
---|---|---|---|
Settings | Value | Value | Value |
Library | SeleniumLibrary | ||
Library | SeLibExtensions |
Test Case | Action | Argument | Argument |
---|---|---|---|
Example | Open Browser | http://example | # SeleniumLibrary |
Title Should Start With | Example | # SeLibExtensions |
Libraries using dynamic or hybrid API
Test libraries that use the dynamic or hybrid library API often have their own systems how to extend them. With these libraries you need to ask guidance from the library developers or consult the library documentation or source code.