4.1创建测试库
Robot Framework的实际测试功能由测试库提供。有许多现有的库,其中一些甚至与核心框架捆绑在一起,但仍然经常需要创建新的库。这个任务并不复杂,因为正如本章所示,Robot Framework的库API简单明了。
4.1.1简介
支持的编程语言
Robot Framework本身是用Python编写的,自然测试库扩展它可以使用相同的语言实现。在Jython上运行框架时,也可以使用Java实现库。纯Python代码适用于Python和Jython,假设它不使用Jython上不可用的语法或模块。使用Python时,也可以使用Python C API使用C实现库,尽管使用ctypes模块与Python库中的C代码交互通常更容易 。
使用这些本机支持的语言实现的库也可以充当使用其他编程语言实现的功能的包装。这种方法的一个很好的例子是远程库,另一种广泛使用的方法是将外部脚本或工具作为单独的进程运行。
小费
机器人框架测试库的Python教程开发人员 涵盖了足够的Python语言,可以开始使用它来编写测试库。它还包含一个简单的示例库和测试用例,您可以在计算机上执行和调查。
不同的测试库API
Robot Framework有三种不同的测试库API。
静态API
最简单的方法是使用一个模块(在Python中)或一个类(在Python或Java中),其方法直接映射到 关键字名称。关键字也采用与实现它们的方法相同的参数。关键字报告带有异常的失败,通过写入标准输出进行记录,并使用return语句返回值。
动态API
动态库是实现获取它们实现的关键字名称的方法的类,以及使用给定参数执行命名关键字的另一种方法。要实现的关键字的名称以及它们的执行方式可以在运行时动态确定,但报告状态,记录和返回值的方式与静态API类似。
混合API
这是静态API和动态API之间的混合体。库是一种类,它使用一种方法来告知它们实现了哪些关键字,但这些关键字必须直接可用。除了发现实现哪些关键字之外的其他所有内容与静态API中的类似。
本章将介绍所有这些API。一切都基于静态API的工作原理,因此首先讨论它的功能。如何动态库API和混合库API从它的不同,然后在自己的章节中讨论。
本章中的示例主要是关于使用Python,但对于仅限Java的开发人员来说,它们也应该易于理解。在API有差异的少数情况下,两个用法都用适当的例子来解释。
4.1.2创建测试库类或模块
测试库可以实现为Python模块和Python或Java类。
测试库名称
导入库时使用的测试库的名称与实现它的模块或类的名称相同。例如,如果您有一个Python模块MyLibrary(即文件MyLibrary.py),它将创建一个名为MyLibrary的库 。类似地,Java类YourLibrary当它不在任何包中时,会创建一个具有该名称的库。
Python类总是在一个模块中。如果实现库的类的名称与模块的名称相同,则Robot Framework允许在导入库时删除模块名称。例如,类MyLib中在MyLib.py 文件可以被用作与所述名称的库MyLib中。如果模块名称和类名称不同,则必须使用模块和类名称(例如mymodule.MyLibrary)使用库。
必须使用全名来使用非默认包中的Java类。例如,类MyLib中在 com.mycompany.myproject包必须与名称导入 com.mycompany.myproject.MyLib。
小费
如果库名称非常长,例如当Java包名称很长时,建议使用WITH NAME语法为库提供更简单的别名。
为测试库提供参数
作为类实现的所有测试库都可以使用参数。这些参数在库名称后面的Setting表中指定,当Robot Framework创建导入库的实例时,它将它们传递给它的构造函数。作为模块实现的库不能接受任何参数,因此尝试使用这些参数会导致错误。
库所需的参数数量与库的构造函数接受的参数数量相同。参数的默认值和变量数与关键字参数的工作方式类似,但Java库没有变量参数支持。传递给库的参数以及库名称本身可以使用变量指定,因此可以更改它们,例如,从命令行更改它们。
Setting | Value | Value | Value |
---|---|---|---|
Library | MyLibrary | 10.0.0.1 | 8080 |
Library | AnotherLib | ${VAR} |
示例实现,第一个在Python中,第二个在Java中,用于上面示例中使用的库:
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 ...
}
}
}
测试库范围
作为类实现的库可以具有内部状态,可以通过关键字和库的构造函数的参数来更改。由于状态会影响关键字的实际行为,因此务必确保一个测试用例中的更改不会意外影响其他测试用例。这种依赖关系可能会产生难以调试的问题,例如,当添加新的测试用例并且它们使用库不一致时。
Robot Framework尝试使测试用例彼此独立:默认情况下,它为每个测试用例创建测试库的新实例。但是,这种行为并不总是令人满意的,因为有时测试用例应该能够共享一个共同的状态。此外,所有库都没有状态,并且根本不需要创建它们的新实例。
测试库可以控制何时使用类属性ROBOT_LIBRARY_SCOPE创建新库。此属性必须是字符串,并且可以具有以下三个值:
- 测试用例
- 为每个测试用例创建一个新实例。可能的套件设置和套件拆解共享另一个实例。这是默认值。
- 测试套房
- 为每个测试套件创建一个新实例。从测试用例文件创建并包含测试用例的最低级别测试套件具有自己的实例,而更高级别的套件都可以获得自己的实例来进行可能的设置和拆卸。
- 全球
- 在整个测试执行期间只创建一个实例,并由所有测试用例和测试套件共享。从模块创建的库始终是全局的。
当TEST SUITE或GLOBAL范围与具有状态的测试库一起使用时,建议库具有一些用于清理状态的特殊关键字。然后可以使用此关键字,例如,在套件设置或拆卸中,以确保下一个测试套件中的测试用例可以从已知状态开始。例如, SeleniumLibrary使用GLOBAL范围来启用在不同测试用例中使用相同的浏览器而无需重新打开它,并且它还具有Close All Browsers关键字,可以轻松关闭所有打开的浏览器。
使用TEST SUITE范围的示例Python库:
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
使用GLOBAL范围的示例Java库:
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;
}
}
指定库版本
当测试库被使用时,Robot Framework会尝试确定其版本。然后将此信息写入syslog 以提供调试信息。库文档工具 libdoc还将此信息写入其生成的关键字文档中。
从属性ROBOT_LIBRARY_VERSION读取版本信息 ,类似于从ROBOT_LIBRARY_SCOPE读取测试库作用域。如果 ROBOT_LIBRARY_VERSION不存在,则尝试从__version__属性读取信息。这些属性必须是类或模块属性,具体取决于库是作为类还是模块实现的。对于Java库,必须将version属性声明为static final。
使用__version__的示例Python模块:
__version__ = '0.1' def keyword():
pass
使用ROBOT_LIBRARY_VERSION的Java类:
public class VersionExample { public static final String ROBOT_LIBRARY_VERSION = "1.0.2"; public void keyword() {
}
}
4.1.3创建静态关键字
哪些方法被视为关键字
使用静态库API时,Robot Framework使用反射来查找库类或模块实现的公共方法。它将排除以下划线开头的所有方法,并且对于Java库,也会忽略仅在java.lang.Object中实现的方法 。所有未被忽略的方法都被视为关键字。例如,下面的Python和Java库实现了单个关键字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();
}
}
当库作为Python模块实现时,也可以通过使用Python的__all__属性来限制关键字的方法 。如果使用__all__,则只有其中列出的方法可以是关键字。例如,下面的库实现关键字示例关键字和第二示例。如果没有__all__,它还会实现关键字 Not Exposed As Keyword和Current Thread。__all__最重要的用法是确保导入的辅助方法(例如下面示例中的current_thread)不会被意外地公开为关键字。
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
注意
从Robot Framework 2.5.5开始,可以支持__all__属性。
关键字名称
将测试数据中使用的关键字名称与方法名称进行比较,以找到实现这些关键字的方法。名称比较不区分大小写,并且忽略空格和下划线。例如,方法hello映射到关键字名称 Hello,hello甚至hello。类似地, do_nothing和doNothing方法都可以用作测试数据中的 Do Nothing关键字。
示例Python库作为MyLibrary.py文件中的模块实现:
def hello(name):
print "Hello, %s!" % name def do_nothing():
pass
在MyLibrary.java文件中作为类实现的示例Java库:
public class MyLibrary { public void hello(String name) {
System.out.println("Hello, " + name + "!");
} public void doNothing() {
} }
下面的示例说明了如何使用上面的示例库。如果您想自己尝试,请确保库位于库搜索路径中。
设置 | 值 | 值 | 值 |
---|---|---|---|
图书馆 | 我的图书馆 |
测试用例 | 行动 | 争论 | 争论 |
---|---|---|---|
我的测试 | 没做什么 | ||
你好 | 世界 |
Setting | Value | Value | Value |
---|---|---|---|
Library | MyLibrary |
Test Case | Action | Argument | Argument |
---|---|---|---|
My Test | Do Nothing | ||
Hello | world |
关键字参数
使用静态和混合API,关键字所需的参数数量的信息直接来自实现它的方法。使用动态库API的库具有共享此信息的其他方法,因此本节与它们无关。
最常见也是最简单的情况是关键字需要确切数量的参数。在这种情况下,Python和Java方法都只是完全采用这些参数。例如,实现不带参数的关键字的方法也不带参数,实现带有一个参数的关键字的方法也需要一个参数,依此类推。
示例Python关键字使用不同数量的参数:
def no_arguments():
print "Keyword got no arguments" def one_argument(arg):
print "Keyword got one argument '%s'" % arg def multiple_arguments(a1, a2, a3):
print "Keyword got three arguments '%s', '%s' and '%s'" % (a1, a2, a3)
关键字的默认值
关键字使用的某些参数具有默认值通常很有用。Python和Java具有不同的语法来处理方法的默认值,并且在为Robot Framework创建测试库时可以使用这些语言的自然语法。
Python的默认值
在Python中,方法始终只有一个实现,并且在方法签名中指定了可能的默认值。所有Python程序员都熟悉的语法如下所示:
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)
上面的第一个示例关键字可以使用零个参数或一个参数。如果没有给出参数,则arg获取 默认值。如果有一个参数,则arg获取该值,并且使用多个参数调用该关键字失败。在第二个示例中,始终需要一个参数,但第二个和第三个参数具有默认值,因此可以使用带有一到三个参数的关键字。
测试用例 | 行动 | 争论 | 争论 | 争论 |
---|---|---|---|---|
默认 | 一个默认值 | |||
一个默认值 | 争论 | |||
多个默认值 | 要求arg | |||
多个默认值 | 要求arg | 可选的 | ||
多个默认值 | 要求arg | 可选1 | 可选2 |
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 |
Java的默认值
在Java中,一种方法可以具有多个具有不同签名的实现。Robot Framework将所有这些实现视为一个关键字,可以与不同的参数一起使用。因此,此语法可用于提供对默认值的支持。下面的示例说明了这一点,它在功能上与早期的Python示例相同:
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");
}
可变数量的参数
Robot Framework还支持带有任意数量参数的关键字。与默认值类似,在Python和Java中,测试库中使用的实际语法也不同。
使用Python的可变数量的参数
Python支持接受任意数量参数的方法。相同的语法在库中有效,并且如下面的示例所示,它也可以与指定参数的其他方式结合使用:
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
测试用例 | 行动 | 争论 | 争论 | 争论 |
---|---|---|---|---|
可变参数 | 任何争论 | |||
任何争论 | 争论 | |||
任何争论 | arg 1 | arg 2 | arg 2 | |
... | arg 4 | arg 5 | ||
一个必需 | 要求arg | |||
一个必需 | 要求arg | 另一个arg | 完后还有 | |
也是默认值 | 需要 | |||
也是默认值 | 需要 | 这两个 | 有默认值 | |
也是默认值 | 1 | 2 | 3 | |
... | 4 | 五 | 6 |
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 |
使用Java的可变数量的参数
Robot支持Java varargs语法,用于定义可变数量的参数。Robot还会将一个数组转换为关键字签名中的最后一个参数,作为可变数量的参数列表。在这种情况下,关键字的所有剩余参数都会打包到列表中。如果关键字使用的参数小于签名中的实际参数数量,则此方法也适用 - 在这种情况下,末尾的数组将为空。
机器人对Java中可变数量的参数的支持有一个限制:它仅在方法只有一个签名时才有效。因此,不可能使Java关键字具有默认值和可变数量的参数。当然,仍然可以使用许多必需的参数,如下例所示:
public void anyNumberOfArguments(String... varargs) {
System.out.println("Got arguments:");
for (String arg: varargs) {
System.out.println(arg);
}
} public void oneArgumentRequired(String required, String... others) {
System.out.println("Required: " + required + "\nOthers:");
for (String arg: others) {
System.out.println(arg);
}
} public void usingAnArray(String[] args) {
System.out.println("Got arguments:");
for (String arg: args) {
System.out.println(arg);
}
}
参数类型
通常,关键字参数作为字符串来到Robot Framework。如果关键字需要其他类型,则可以使用 变量或将字符串转换为关键字中的必需类型。使用 Java关键字时,基类型也会自动强制执行。
Python的参数类型
因为Python中的参数没有任何类型信息,所以在使用Python库时不可能自动将字符串转换为其他类型。调用实现具有正确数量的参数的关键字的Python方法总是成功,但如果参数不兼容则执行失败。幸运的是,在关键字中将参数转换为合适的类型很简单:
def connect_to_host(address, port=25):
port = int(port)
# ...
使用Java的参数类型
Java方法的参数有类型,并且所有基类型都是自动处理的。这意味着测试数据中作为普通字符串的参数在运行时被强制转换为正确的类型。可以强制的类型是:
- 整数类型(byte,short,int,long)
- 浮点类型(浮点数和双精度数)
- 在布尔类型
- 上述类型的对象版本,例如java.lang.Integer
对于在关键字方法的所有签名中具有相同或兼容类型的参数,执行强制。在以下示例中,可以对关键字doubleArgument 和compatibleTypes进行转换,但不能对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) {}
如果测试数据具有包含数字的字符串,则强制与数字类型一起使用,并且对于布尔类型,数据必须包含字符串true或false。强制仅在原始值是测试数据的字符串时才会完成,但当然仍然可以使用包含这些关键字的正确类型的变量。如果关键字具有冲突的签名,则使用变量是唯一的选择。
测试用例 | 行动 | 争论 | 争论 | 争论 |
---|---|---|---|---|
强迫 | 双重论证 | 3.14 | ||
双重论证 | 2E16 | # 科学计数法 | ||
兼容类型 | 你好,世界! | 1234 | ||
兼容类型 | 你好,我们又见面了! | -10 | 真正 | |
没有强制 | 双重论证 | $ {} 3.14 | ||
冲突类型 | 1 | $ {2} | #必须使用变量 | |
冲突类型 | $ {1} | 2 |
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 |
4.1.4与机器人框架进行通信
调用实现关键字的方法后,可以使用任何机制与被测系统进行通信。然后,它还可以向Robot Framework的日志文件发送消息,返回可以保存到变量的信息,最重要的是,报告关键字是否通过。
报告关键字状态
报告关键字状态只需使用例外即可完成。如果执行的方法引发异常,则关键字status为FAIL,如果正常返回,则状态为PASS。
日志,报告和控制台中显示的错误消息是根据异常类型及其消息创建的。对于通用异常(例如,AssertionError,Exception和 RuntimeError),仅使用异常消息,而对于其他异常消息,将以ExceptionType:Actual消息的格式创建消息。在这两种情况下,对于用户而言,异常消息尽可能地提供信息是很重要的。
如果错误消息超过40行,它将从中间自动切断,以防止报告过长且难以阅读。完整错误消息始终显示在failed关键字的日志消息中。
还使用DEBUG 日志级别记录异常的回溯。默认情况下,这些消息在日志文件中不可见,因为普通用户很少感兴趣。在开发库时,使用--loglevel DEBUG运行测试通常是个好主意。
停止测试执行
从Robot Framework 2.5开始,可能会使测试用例失败,从而 停止整个测试执行。这只需要通过一个特殊的ROBOT_EXIT_ON_FAILURE属性来完成,该 属性在从关键字引发的异常上设置了True值。这在以下示例中说明。
Python:
class MyFatalError(RuntimeError):
ROBOT_EXIT_ON_FAILURE = True
Java:
public class MyFatalError extends RuntimeException {
public static final boolean ROBOT_EXIT_ON_FAILURE = true;
}
尽管失败,仍继续执行测试
从Robot Framework 2.5开始,即使出现故障,也可以继续测试执行。从测试库发出信号的方法是向用于传达故障的异常添加一个特殊的ROBOT_CONTINUE_ON_FAILURE 属性,其值为True。这通过以下实施例证明。
Python:
class MyContinuableError(RuntimeError):
ROBOT_CONTINUE_ON_FAILURE = True
Java:
public class MyContinuableError extends RuntimeException {
public static final boolean ROBOT_CONTINUE_ON_FAILURE = true;
}
记录信息
异常消息不是向用户提供信息的唯一方式。除此之外,方法还可以通过写入标准输出流(stdout)或标准错误流(stderr)将消息发送到日志文件,甚至可以使用不同的 日志级别。另一种(通常更好的)日志记录可能性是使用编程日志API。
默认情况下,方法写入标准输出的所有内容都将作为日志级别为INFO的单个条目写入日志文件 。写入标准错误的消息将以类似方式处理,但在关键字执行完成后它们将回显到原始stderr。因此,如果您需要在执行测试的控制台上显示某些消息,则可以使用stderr。
使用日志级别
要使用除INFO以外的其他日志级别,或创建多个消息,请通过以* LEVEL *实际日志消息的格式将级别嵌入到消息中来明确指定日志级别,其中 * LEVEL *必须位于行的开头并且LEVEL是可用的日志记录级别TRACE,DEBUG, INFO,WARN和HTML之一。
警告
具有WARN级别的消息将自动写入控制台并写入日志文件中的单独“测试执行错误”部分。这使得警告比其他消息更加明显,并允许使用它们向用户报告重要但非关键的问题。
记录HTML
通常由库记录的所有内容都将转换为可以安全地表示为HTML的格式。例如, <b> foo </ b>将在日志中完全显示,而不是foo。如果库想要使用格式化,链接,显示图像等,它们可以使用特殊的伪日志级 HTML。Robot Framework会将这些消息直接写入具有INFO级别的日志中,因此他们可以使用他们想要的任何HTML语法。请注意,需要谨慎使用此功能,因为,例如,一个位置不佳的</ table>标记可能会严重破坏日志文件。
使用公共日志记录API时,各种日志记录方法都具有可选的html属性,可以将其设置为True 以启用HTML格式的日志记录。
时间戳
默认情况下,通过标准输出或错误流记录的消息会在执行的关键字结束时获取其时间戳。这意味着时间戳不准确,调试问题尤其是运行时间较长的关键字可能会出现问题。
从Robot Framework 2.6开始,关键字可以在需要时为他们记录的消息添加准确的时间戳。时间戳必须以Unix纪元以来的毫秒为单位给出,并且必须在使用冒号与日志级别分隔后放置:
*INFO:1308435758660* Message with timestamp
*HTML:1308435758661* <b>HTML</b> message with timestamp
如下面的示例所示,使用Python和Java添加时间戳都很容易。如果您使用的是Python,则使用编程日志API获取准确的时间戳会更容易。明确添加时间戳的一大好处是这种方法也适用于远程库接口。
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");
}
登录到控制台
如果库需要向控制台写一些东西,他们有几个选项。如前所述,警告和写入标准错误流的所有消息都写入日志文件和控制台。这两个选项都有一个限制,即只有在当前正在执行的关键字完成后,消息才会结束到控制台。值得一提的是,这些方法适用于Python和基于Java的库。
另一个仅适用于Python的选项是将消息写入sys .__ stdout__或sys .__ stderr__。使用此方法时,消息会立即写入控制台,并且根本不会写入日志文件:
import sys def my_keyword(arg):
sys.__stdout__.write('Got arg %s\n' % arg)
最后一个选项是使用公共日志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)
记录示例
在大多数情况下,INFO水平是足够的。它下面的级别 DEBUG和TRACE对于编写调试信息很有用。这些消息通常不会显示,但它们可以帮助调试库本身可能出现的问题。该WARN级别可以用来使信息更加明显和HTML是否需要任何一种格式是非常有用的。
以下示例阐明了使用不同级别进行日志记录的方式。Java程序员应该将代码打印'message' 视为伪代码,意思是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 |
程序化日志API
与使用标准输出和错误流相比,程序化API提供了更简洁的日志信息记录方式。目前,这些接口仅适用于Python基础测试库。
公共日志API
Robot Framework 2.6有一个新的基于Python的日志记录API,用于将消息写入日志文件和控制台。测试库可以像logger.info('我的消息')一样使用这个API,而不是像print'* INFO * My message'那样记录标准输出。除了使用更清晰的编程接口之外,此API还有一个好处,即日志消息具有准确的时间戳。一个明显的限制是使用此日志记录API的测试库依赖于Robot Framework。
公共日志API被记录为API文档的一部分,但这是一个简单的用法示例:
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!')
使用Python的标准日志记录模块
除了新的公共日志API之外,Robot Framework 2.6还为Python的标准日志记录模块添加了内置支持。这样可以使模块的根记录器接收的所有消息自动传播到Robot Framework的日志文件。此API 也会生成具有准确时间戳的日志消息,但不支持记录HTML消息或将消息写入控制台。下面的简单示例说明了一个很大的好处,即使用此日志记录API不会创建与Robot Framework的依赖关系。
import logging def my_keyword(arg):
logging.debug('Got argument %s' % arg)
do_something()
logging.info('This is a boring example')
该日志模块具有比机器人框架略有不同的日志级别。它的DEBUG和INFO级别直接映射到匹配的Robot Framework日志级别和WARNING ,上面的所有内容都映射到WARN。DEBUG下面的自定义级别 映射到DEBUG,DEBUG和WARNING之间的所有内容都 映射到INFO。
在库初始化期间记录
库也可以在测试库导入和初始化期间记录。这些消息不像普通日志消息那样出现在日志文件中,而是写入syslog。这允许记录有关库初始化的任何有用的调试信息。使用WARN级别记录的消息也会 在日志文件的测试执行错误部分中显示。
使用标准输出和错误流以及编程日志API可以在导入和初始化期间进行 日志记录。以下都说明了这两点。
初始化期间通过stdout记录Java库:
public class LoggingDuringInitialization { public LoggingDuringInitialization() {
System.out.println("*INFO* Initializing library");
} public void keyword() {
// ...
}
}
导入期间使用日志记录API的Python库日志记录:
from robot.api import logger logger.debug("Importing library") def keyword():
# ...
注意
如果您在初始化期间记录某些内容,即在Python __init__或Java构造函数中,则可能会多次记录消息,具体取决于测试库范围。
注意
在库初始化期间支持将日志消息写入syslog是Robot Framework 2.6中的一项新功能。
回归价值观
关键字与核心框架进行通信的最终方式是返回从被测系统检索到的信息或通过其他方式生成的信息。返回的值可以分配给测试数据中的变量,然后用作其他关键字的输入,甚至可以用于不同的测试库。
使用Python和Java方法中的return语句返回值。通常,一个值被分配到一个 标量变量中,如下面的示例所示。此示例还说明可以返回任何对象并使用 扩展变量语法来访问对象属性。
from mymodule import MyObject def return_string():
return "Hello, world!" def return_object(name):
return MyObject(name)
${string} = | Return String | |
Should Be Equal | ${string} | Hello, world! |
${object} = | Return Object | Robot |
Should Be Equal | ${object.name} | Robot |
关键字还可以返回值,以便可以将它们一次分配到多个标量变量中,分配到列表变量中,或者分配到标量变量和列表变量中。所有这些用法都要求返回的值是Python列表或元组,或者是Java数组,列表或迭代器。
def return_two_values():
return 'first value', 'second value' def return_multiple_values():
return ['a', 'list', 'of', 'strings']
${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 |
使用线程时的通信
如果库使用线程,它通常应该仅从主线程与框架通信。例如,如果工作线程无法报告或记录某些内容,则应首先将信息传递给主线程,然后主线程可以使用本节中说明的异常或其他机制与框架进行通信。
当线程在后台运行而其他关键字正在运行时,这一点尤为重要。在这种情况下与框架通信的结果是未定义的,并且在工作情况下可能导致崩溃或损坏的输出文件。如果关键字在后台启动某些内容,则应该有另一个关键字来检查工作线程的状态并相应地报告收集的信息。
注意
从Robot Framework 2.6.2开始,将静默忽略非主线程使用编程日志API记录的消息。
4.1.5分发测试库
记录库
没有关于它包含哪些关键字以及这些关键字做什么的文档的测试库是没有用的。为了便于维护,强烈建议库文档包含在源代码中并从中生成。基本上,这意味着使用带Python的docstrings和Java的Javadoc,如下面的示例所示。
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() {
} }
Python和Java都有用于创建如上所述的库的API文档的工具。但是,对于某些用户来说,这些工具的输出可能略有技术性。另一种方法是使用Robot Framework自己的文档工具libdoc。此工具可以使用静态库API(例如上面的API)从Python和Java库创建库文档,但它也使用动态库API和混合库API处理库。
关键字文档的第一行用于特殊目的,应包含关键字的简短总体描述。它被用作一个简短的文档,例如libdoc的工具提示,也可以在测试日志中显示。但是,后者不能与使用静态API的Java库一起使用,因为它们的文档在编译时丢失,在运行时不可用。
注意
如果要在Python库的文档中使用非ASCII字符,则必须使用UTF-8作为源代码编码,或者将docstrings创建为Unicode。
测试库
任何非平凡的测试库都需要进行全面测试,以防止其中的错误。当然,此测试应该是自动化的,以便在更改库时轻松重新运行测试。
Python和Java都有出色的单元测试工具,它们非常适合测试库。与将其用于其他测试相比,将它们用于此目的没有重大差异。熟悉这些工具的开发人员不需要学习任何新东西,不熟悉它们的开发人员也应该学习它们。
使用Robot Framework本身也很容易测试库,这种方式对它们进行实际的端到端验收测试。为此,BuiltIn库中有许多有用的关键字。值得一提的是运行关键字和期望错误,这对于测试关键字是否正确报告错误非常有用。
是否使用单元或验收级别的测试方法取决于上下文。如果需要模拟测试中的实际系统,则在单元级别上通常更容易。另一方面,验收测试确保关键字通过Robot Framework工作。如果您无法决定,当然可以使用这两种方法。
包装库
在实施,记录和测试库之后,仍然需要将其分发给用户。对于由单个文件组成的简单库,通常要求用户在某处复制该文件并相应地设置库搜索路径。应该打包更复杂的库以使安装更容易。
由于库是普通的编程代码,因此可以使用普通的打包工具进行打包。使用Python,很好的选项包括Python标准库包含的 distutils和更新的 setuptools。这些工具的一个好处是库模块安装在库搜索路径中自动的位置。
使用Java时,将库打包到JAR存档中是很自然的。 在运行测试之前,必须将JAR包放入库搜索路径,但是很容易创建一个自动执行该操作的启动脚本。
弃用关键字
有时需要用新的关键字替换现有的关键字或完全删除它们。仅通知用户有关更改的信息可能并不总是足够,并且在运行时获取警告更有效。为了支持这一点,Robot Framework能够标记已弃用的关键字。这样可以更轻松地从测试数据中查找旧关键字并删除或替换它们。
通过使用* DEPRECATED *启动文档来弃用关键字 。执行这些关键字时,会将包含其余简短文档的警告写入 控制台,并写入日志文件中的单独“测试执行错误”部分。例如,如果执行了以下关键字,则会在日志文件中显示如下所示的警告。
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.。 |
此弃用系统适用于大多数测试库以及 用户关键字。唯一的例外是在使用静态库接口的Java测试库中实现的关键字,因为它们的文档在运行时不可用。使用这样的关键字,可以将用户关键字用作包装并弃用它们。
计划实施一种工具,可以使用弃用信息自动替换已弃用的关键字。该工具很可能从文档中获取新关键字的名称,以便它在反引号(`)中搜索单词。因此,它会从前面的例子中找到其他关键字。请注意,libdoc还使用相同的语法自动创建内部链接。
4.1.6动态库API
动态API在很多方面类似于静态API。例如,报告关键字状态,日志记录和返回值的方式完全相同。最重要的是,与其他库相比,导入动态库和使用其关键字没有区别,因此您甚至不需要知道库使用哪些API。
静态库和动态库之间的唯一区别在于Robot Framework如何发现库实现的关键字,它们具有哪些参数和文档以及这些关键字是如何实际执行的。使用静态API,所有这些都是使用反射完成的(Java库文档除外),但动态库具有用于这些目的的特殊方法。
动态API的一个好处是您可以更灵活地组织库。使用静态API,您可以在一个类(或模块)中包含所有关键字,而使用动态API,您可以根据需要将每个关键字实现为单独的类。这个用例对Python来说并不那么重要,因为它的动态功能和多继承已经提供了很大的灵活性,混合库API通常是更好的选择。
动态API的另一个主要用例是实现库,以便它只是某个其他计算机或其他JVM上的实际库的代理。这种代理库可能非常薄,并且因为关键字名称是动态获取的,所以在将新关键字添加到实际库中时无需更新代理。
本节介绍动态API如何在Robot Framework和动态库之间工作。Robot Framework如何实际实现这些库并不重要(例如,如何将对run_keyword方法的调用映射到正确的关键字实现),并且可以采用许多不同的方法。但是,如果使用Java,则可能需要在实现自己的系统之前检查 JavalibCore。这个可重用工具集合支持多种创建关键字的方法,它可能已经有一个满足您需求的机制。
获取关键字名称
动态库通过get_keyword_names方法告知它们实现了哪些关键字 。该方法还具有在编写Java时建议使用的别名 getKeywordNames。此方法不能接受任何参数,并且必须返回包含库实现的关键字名称的字符串列表(在Python中)或字符串数组(在Java中)。
如果返回的关键字名称包含多个单词,则可以使用空格或下划线或camelCase格式返回它们。例如,['first keyword','second keyword'], ['first_keyword','second_keyword']和 ['firstKeyword','secondKeyword']都会产生关键字 First Keyword和Second Keyword。
动态库必须始终具有此方法。如果它丢失,或者由于某种原因调用它失败,则库被视为静态库。
运行关键字
动态库有一个特殊的run_keyword(别名 runKeyword)方法来执行它们的关键字。当在测试数据中使用动态库中的关键字时,Robot Framework使用库的run_keyword方法来执行它。此方法有两个参数。第一个参数是一个字符串,其中包含要以与get_keyword_names返回的格式相同的格式执行的关键字的名称。第二个参数是给测试数据中的关键字的参数列表(Java中的对象数组)。
在获得关键字名称和参数之后,它可以*地执行关键字,但它必须使用相同的机制与框架作为静态库进行通信。这意味着使用异常来报告关键字状态,通过写入标准输出进行日志记录,并使用run_keyword中的return语句 返回内容。
每个动态库都必须同时具有get_keyword_names和 run_keyword方法。动态API中的其余方法是可选的,因此下面的示例显示了一个有效的(虽然是微不足道的)动态库。
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)
获取关键字参数
如果动态库仅实现get_keyword_names和 run_keyword方法,则Robot Framework不具有有关实现的关键字所需参数的任何信息。例如,上例中的第一关键字和第二关键字都可以与任意数量的参数一起使用。这是有问题的,因为大多数真正的关键字都需要一定数量的关键字,在这种情况下,他们需要自己检查参数计数。
动态库可以使用get_keyword_arguments(别名getKeywordArguments)方法告诉Robot Framework它实现的关键字实际上期望的参数 。此方法将关键字的名称作为参数,并返回包含该关键字接受的参数的字符串列表(Java中的字符串数组)。
与静态关键字类似,动态关键字可以要求任意数量的参数,具有默认值并接受可变数量的参数。下表说明了如何表示所有这些不同情况的语法。请注意,这些示例使用Python字符串列表,但Java开发人员应该能够将它们转换为字符串数组。
Expected arguments | How to represent | Examples | Min / Max |
---|---|---|---|
No arguments | Empty list. | [] | 0/0 |
One or more argument | List of strings containing argument names. | ['one_argument'], ['a1', 'a2', 'a3'] | 1/1, 3/3 |
Default values for arguments | Default values separated from names with =. Default values are always considered to be strings. | ['arg=default value'], ['a', 'b=1', 'c=2'] | 0/1, 1/3 |
Variable number of arguments | Last argument has * before its name. | ['*arguments'], ['a', 'b=42', '*rest'] | 0/any, 1/any |
使用get_keyword_arguments时,Robot Framework会自动计算关键字所需的参数数量。如果关键字与无效数量的参数一起使用,则会发生错误,甚至不会调用run_keyword。上表的最后一列显示了根据提供的示例计算的最小和最大参数计数。
执行测试时,实际参数名称无关紧要,因为只有参数计数与Robot Framework有关。另一方面,如果使用libdoc工具来记录库,则文档中会显示参数,在这种情况下,它们需要具有有意义的名称。
获取关键字文档
动态库可以实现的最终特殊方法是 get_keyword_documentation(别名为 getKeywordDocumentation)。它将关键字名称作为参数,并且正如方法名称所暗示的那样,将其文档作为字符串返回。
返回的文档与使用Python实现的静态库的关键字文档字符串类似。主要用例是将关键字的文档记录到使用libdoc生成的库文档中。此外,文档的第一行(直到第一行\ n)显示在测试日志中。
获取通用库文档
该get_keyword_documentation方法也可以用于指定整体库文档。执行测试时不使用此文档,但它可以使libdoc生成的文档更好。
动态库可以提供与使用库相关的通用库文档和文档。前者是通过使用特殊值 __intro__调用get_keyword_documentation得到的,后者是使用值 __init__获得的。如何使用libdoc在实践中最好地测试文档的呈现方式。
基于Python的动态库还可以直接在代码中将通用库文档指定为库类的文档字符串或其__init__方法。如果直接从代码和get_keyword_documentation方法获得非空文档 ,则后者具有更高的优先级。
注意
Robot Framework 2.6.2及更高版本支持获取通用库文档。
摘要
动态API中的所有特殊方法都列在下表中。方法名称以下划线格式列出,但它们的camelCase别名的工作方式完全相同。
Name | Arguments | Purpose |
---|---|---|
get_keyword_names | Return names of the implemented keywords. | |
run_keyword | name, arguments | Execute the specified keyword with given arguments. |
get_keyword_arguments | name | Return keywords' argument specifications. Optional. |
get_keyword_documentation | name | Return keywords' and library's documentation. Optional. |
可以用Java编写正式的接口规范,如下所示。但是,请记住库不需要实现任何显式接口,因为如果库具有所需的get_keyword_names和 run_keyword方法,Robot Framework会直接检查反射。此外, get_keyword_arguments和get_keyword_documentation 是完全可选的。
public interface RobotFrameworkDynamicAPI { String[] getKeywordNames(); Object runKeyword(String name, Object[] arguments); String[] getKeywordArguments(String name); String getKeywordDocumentation(String name); }
使用动态API的一个很好的例子是Robot Framework自己的 远程库。
4.1.7混合库API
顾名思义,混合库API是静态API和动态API之间的混合体。与动态API一样,可以仅使用混合API作为类来实现库。
获取关键字名称
关键字名称的获取方式与动态API完全相同。实际上,库需要使用 get_keyword_names或getKeywordNames方法返回库实现的关键字名称列表。
运行关键字
在混合API中,没有用于执行关键字的run_keyword方法。相反,Robot Framework使用反射来查找实现关键字的方法,与静态API类似。使用混合API的库可以直接实现这些方法,或者更重要的是,它可以动态处理它们。
在Python中,使用__getattr__方法可以轻松地动态处理缺少的 方法。大多数Python程序员可能都熟悉这种特殊方法,他们可以立即理解以下示例。其他人可能会发现首先参考Python参考手册更容易。
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)
请注意,__ getattr__不会像动态API 那样执行run_keyword这样的实际关键字 。相反,它只返回一个可调用的对象,然后由Robot Framework执行。
另一点需要注意的是,Robot Framework使用与get_keyword_names相同的名称来查找实现它们的方法。因此,必须以与定义它们相同的格式返回在类本身中实现的方法的名称。例如,如果get_keyword_names返回My Keyword而不是 my_keyword,则上面的库将无法正常工作 。
混合API对Java不是很有用,因为它不可能用它来处理丢失的方法。当然,可以实现库类中的所有方法,但与静态API相比,这几乎没有什么好处。
获取关键字参数和文档
使用此API时,Robot Framework使用反射来查找实现关键字的方法,与静态API类似。在获得对该方法的引用之后,它将从中搜索参数和文档,其方式与使用静态API时相同。因此,不需要像动态API那样获取参数和文档的特殊方法。
摘要
在Python中实现测试库时,混合API具有与实际动态API相同的动态功能。它的一大好处是没有必要使用特殊的方法来获取关键字参数和文档。唯一真正的动态关键字需要在__getattr__中处理,而其他关键字可以直接在主库类中实现。
由于具有明显的优势和相同的功能,在使用Python时,混合API在大多数情况下是比动态API更好的替代方案。一个值得注意的例外是将库实现为其他地方的实际库实现的代理,因为实际的关键字必须在别处执行,代理只能传递关键字名称和参数。
使用混合API的一个很好的例子是Robot Framework自己的 Telnet库。
4.1.8使用Robot Framework的内部模块
使用Python实现的测试库可以使用Robot Framework的内部模块,例如,获取有关已执行测试和所使用设置的信息。但是,应该谨慎使用这种与框架通信的强大机制,因为所有Robot Framework的API都不是外部使用的,它们可能会在不同的框架版本之间发生根本变化。
可用的API
从Robot Framework 2.7开始,API文档在优秀的Read the Docs服务中单独托管。如果您不确定如何使用某些API或使用它们向前兼容,请将问题发送到邮件列表。
使用BuiltIn库
最安全的API是在BuiltIn库中实现关键字的方法 。对关键字的更改很少见,并且总是这样做,以便首先弃用旧用法。最有用的方法之一是replace_variables,它允许访问当前可用的变量。以下示例演示了如何获取 $ {OUTPUT_DIR},这是许多方便的自动变量之一。也可以使用set_test_variable,set_suite_variable和 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>'
使用BuiltIn方法的唯一方法是必须特别处理所有 run_keyword方法变体。使用run_keyword方法的方法必须使用BuiltIn模块中的register_run_keyword 方法自行注册为运行关键字。这个方法的文档解释了为什么需要这样做,显然也是如何做到这一点。
4.1.9扩展现有的测试库
本节介绍了如何向现有测试库添加新功能以及如何在您自己的库中使用它们的不同方法。
修改原始源代码
如果您可以访问要扩展的库的源代码,则可以自然地直接修改源代码。这种方法的最大问题是,您可能很难在不影响更改的情况下更新原始库。对于用户而言,使用具有与原始功能不同的功能的库也可能会令人困惑。重新包装库也可能是一项重大的额外任务。
如果增强功能是通用的,并且您计划将它们提交给原始开发人员,则此方法非常有效。如果您的更改应用于原始库,则它们将包含在将来的版本中,并且可以减轻上面讨论的所有问题。如果更改是非泛型的,或者由于某些其他原因无法将其提交回来,则后续部分中解释的方法可能会更好。
使用继承
扩展现有库的另一种直接方法是使用继承。下面的示例说明了这一点,它为SeleniumLibrary添加了新的 Title At Start With关键字。此示例使用Python,但您显然可以用相同的方式在Java代码中扩展现有Java库。
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))
与修改原始库相比,这种方法的一个重大区别是新库的名称与原始库的名称不同。一个好处是,您可以轻松地告诉您正在使用自定义库,但一个大问题是您无法轻松地将新库与原始库一起使用。首先,您的新库将具有与原始含义相同的关键字,即始终 存在冲突。另一个问题是图书馆不共享其状态。
当您开始使用新库并希望从头开始添加自定义增强功能时,此方法很有效。否则,本节中解释的其他机制可能更好。
直接使用其他库
因为测试库在技术上只是类或模块,所以使用另一个库的简单方法是导入它并使用其方法。当方法是静态的并且不依赖于库状态时,这种方法很有效。前面使用Robot Framework的BuiltIn库的示例说明了这一点。
但是,如果图书馆有州,那么事情可能无法发挥作用。您在库中使用的库实例将与框架使用的库实例不同,因此库中不会显示由执行的关键字完成的更改。下一节将介绍如何访问框架使用的同一个库实例。
从Robot Framework获取活动库实例
Robot Framework 2.5.2添加了新的BuiltIn关键字Get Library Instance,可用于从框架本身获取当前活动的库实例。此关键字返回的库实例与框架本身使用的相同,因此查看正确的库状态没有问题。虽然此功能可作为关键字使用,但它通常通过导入BuiltIn库类直接在测试库中使用,如前所述。以下示例说明了如何实现与前面关于使用继承的示例相同的Title Should Start With关键字。
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))
这种方法明显优于直接导入库并在库具有状态时使用它。继承的最大好处是您可以正常使用原始库,并在需要时使用新库。这在下面的示例中进行了演示,其中前面示例中的代码预计可在新库SeLibExtensions中使用。
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 |
使用动态或混合API的库
使用动态或混合库API的测试库通常有自己的系统如何扩展它们。使用这些库,您需要向库开发人员咨询或查阅库文档或源代码。