lyyyuna 的小花园

动静中之动, by

RSS

Robot Framework 新版教程 - 创建测试库

发表于 2026-04

创建测试库

Robot Framework 的实际测试能力由测试库(test library)提供。虽然已经有许多现成的库,其中一些甚至与核心框架捆绑在一起,但仍然经常需要创建新的库。这项任务并不太复杂,因为正如本章所阐述的那样,Robot Framework 的库 API 简单且直接。

简介

支持的编程语言

Robot Framework 本身使用 Python 编写,因此扩展它的测试库自然也可以使用同一语言来实现。也可以使用 C 语言通过 Python C API 来实现库,不过使用 ctypes 模块从 Python 库与 C 代码交互通常更容易。

使用 Python 实现的库也可以作为其他编程语言实现的功能的包装器。这种方式的一个很好的例子是 Remote library,另一种广泛使用的方式是将外部脚本或工具作为独立进程运行。

不同的库 API

Robot Framework 有两种不同的库 API。

静态 API(Static API)

最简单的方式是拥有一个模块或类,其中的函数/方法直接映射到关键字名称。关键字也接受与实现它们的方法相同的参数。关键字通过异常报告失败,通过写入标准输出进行日志记录,并使用 return 语句返回值。

动态 API(Dynamic API)

动态库是实现了一个方法来获取它们所实现的关键字名称、一个方法来用给定参数执行指定关键字、以及各种可选方法来提供关于已实现关键字和库本身更多信息的类。要实现的关键字名称以及它们如何执行可以在运行时动态确定,但报告状态、日志记录和返回值的方式与静态 API 类似。

本章集中介绍静态 API,另有单独的章节介绍动态库 API。

创建测试库类或模块

测试库可以实现为 Python 模块或类。

库名

使用测试库一节中所讨论的,库可以通过名称或路径导入:

*** Settings ***
Library    MyLibrary
Library    module.LibraryClass
Library    path/AnotherLibrary.py

当通过名称导入库时,库模块必须在模块搜索路径中,名称可以指向库模块或库类。当名称直接指向库类时,名称必须采用 modulename.ClassName 这样的格式。通过路径导入总是指向模块。

即使库导入指向模块(无论是通过名称还是路径),在以下情况中,模块中的类(而非模块本身)将被用作库:

  1. 如果模块包含一个与模块同名的类。该类可以在模块中实现,也可以导入到模块中。

    这使得可以使用简单的名称如 MyLibrary 导入库,而不必同时指定模块和类如 module.MyLibraryMyLibrary.MyLibrary。当通过路径导入库时,甚至无法直接引用库类,只能自动使用导入模块中的类。

  2. 如果模块包含恰好一个使用 @library 装饰器装饰的类。在这种情况下,类需要在模块中实现,而非导入到模块中。

    这种方式与前一种具有相同的好处,但它还允许类名与模块名不同。

    使用 @library 装饰器实现此用途是 Robot Framework 7.2 的新功能。

提示: 如果库名非常长,在导入时给它取一个更简单的别名通常是个好主意。

向库提供参数

所有以类实现的测试库都可以接受参数。这些参数在导入库时在库名之后指定,当 Robot Framework 创建导入库的实例时,会将它们传递给构造函数。以模块实现的库不能接受任何参数。

库所需的参数数量与库的 __init__ 方法接受的参数数量相同。默认值、参数转换以及其他类似功能的工作方式与关键字参数相同。传递给库的参数以及库名本身都可以使用变量指定,因此可以修改它们,例如从命令行修改。

*** Settings ***
Library    MyLibrary     10.0.0.1    8080
Library    AnotherLib    ${ENVIRONMENT}

上述示例中使用的库的示例实现:

from example import Connection

class MyLibrary:

    def __init__(self, host, port=80):
        self.connection = Connection(host, port)

    def send_message(self, message):
        self.connection.send(message)
class AnotherLib:

    def __init__(self, environment):
        self.environment = environment

    def do_something(self):
        if self.environment == 'test':
            do_something_in_test_environment()
        else:
            do_something_in_other_environments()

如果一个库在同一个套件中使用不同参数多次导入,需要给它一个自定义名称,否则后续的导入会被忽略:

*** Settings ***
Library    MyLibrary     10.0.0.1    8080    AS    RemoteLibrary
Library    MyLibrary     127.0.0.1    AS    LocalLibrary

*** Test Cases ***
Example
    RemoteLibrary.Send Message    Hello!
    LocalLibrary.Send Message    Hi!

库作用域

以类实现的库可以拥有内部状态,这些状态可以被关键字和库构造函数的参数改变。由于状态可能影响关键字的实际行为,因此确保一个测试用例中的更改不会意外影响其他测试用例非常重要。这类依赖可能会导致难以调试的问题,例如当添加新的测试用例并且它们不一致地使用库时。

Robot Framework 尝试保持测试用例之间相互独立:默认情况下,它为每个测试用例创建测试库的新实例。然而,这种行为并不总是理想的,因为有时测试用例应该能够共享公共状态。此外,并非所有库都有状态,为它们创建新实例完全没有必要。

测试库可以通过类属性 ROBOT_LIBRARY_SCOPE 来控制何时创建新的库实例。这个属性必须是字符串,可以有以下三个值:

TEST

为每个测试用例创建一个新实例。可能存在的 suite setup 和 suite teardown 共享另一个实例。

在 Robot Framework 3.2 之前,这个值是 TEST CASE,但现在推荐使用 TEST。因为所有无法识别的值都被视为与 TEST 相同,所以两个值在所有版本中都有效。出于同样的原因,如果库更侧重于 RPA 用途而非测试,也可以使用 TASK 值。如果未设置 ROBOT_LIBRARY_SCOPE 属性,TEST 也是默认值。

SUITE

为每个测试套件创建一个新实例。从测试用例文件创建的、包含测试用例的最低级别测试套件拥有自己的实例,更高级别的套件也各自获得自己的实例以用于可能的 setup 和 teardown。

在 Robot Framework 3.2 之前,这个值是 TEST SUITE。该值仍然有效,但目标为 Robot Framework 3.2 及更新版本的库推荐使用 SUITE

GLOBAL

在整个测试执行期间只创建一个实例,并且被所有测试用例和测试套件共享。从模块创建的库始终是全局的。

注意: 如果一个库使用不同的参数被多次导入,则无论作用域如何,每次都会创建一个新实例。

SUITEGLOBAL 作用域与有状态的库一起使用时,建议库拥有一些特殊的关键字来清理状态。这个关键字然后可以在 suite setup 或 teardown 中使用,以确保下一个测试套件中的测试用例可以从已知状态开始。例如,SeleniumLibrary 使用 GLOBAL 作用域来允许在不同的测试用例中使用同一个浏览器而不必重新打开它,并且它还有 Close All Browsers 关键字来方便地关闭所有已打开的浏览器。

使用 SUITE 作用域的示例库:

class ExampleLibrary:
    ROBOT_LIBRARY_SCOPE = 'SUITE'

    def __init__(self):
        self._counter = 0

    def count(self):
        self._counter += 1
        print(self._counter)

    def clear_count(self):
        self._counter = 0

库版本

当测试库被使用时,Robot Framework 会尝试确定其版本。这些信息随后被写入 syslog 以提供调试信息。库文档工具 Libdoc 也会将这些信息写入它生成的关键字文档中。

版本信息从 ROBOT_LIBRARY_VERSION 属性读取,类似于库作用域从 ROBOT_LIBRARY_SCOPE 读取。如果 ROBOT_LIBRARY_VERSION 不存在,会尝试从 __version__ 属性读取信息。这些属性必须是类属性或模块属性,取决于库是以类还是模块实现的。

一个使用 __version__ 的模块示例:

__version__ = '0.1'

def keyword():
    pass

文档格式

库文档工具 Libdoc 支持多种格式的文档。如果你想使用 Robot Framework 自有的文档格式以外的格式,可以在源代码中使用 ROBOT_LIBRARY_DOC_FORMAT 属性来指定格式,类似于作用域和版本通过各自的 ROBOT_LIBRARY_* 属性设置。

文档格式的可选值(不区分大小写)为 ROBOT(默认)、HTMLTEXT(纯文本)和 reST(reStructuredText)。使用 reST 格式需要在生成文档时安装 docutils 模块。

下面的示例通过使用 reStructuredText 格式来说明文档格式的设置。更多关于测试库文档的一般信息,参见记录库一节和 Libdoc 章节。

"""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

库作为 listener

Listener 接口允许外部 listener 获取有关测试执行的通知。它们在套件、测试和关键字开始和结束等事件时被调用。有时在测试库中获取此类通知也很有用,它们可以使用 ROBOT_LIBRARY_LISTENER 属性注册一个自定义 listener。这个属性的值应该是要使用的 listener 实例,可以是库本身。

更多信息和示例参见 Libraries as listeners 一节。

@library 装饰器

配置以类实现的库的一种便捷方式是使用 robot.api.deco.library 类装饰器。它允许通过可选参数 scopeversionconverterdoc_formatlistener 分别配置库的作用域、版本、自定义参数转换器、文档格式和 listener。使用这些参数时,它们会自动设置相应的 ROBOT_LIBRARY_SCOPEROBOT_LIBRARY_VERSIONROBOT_LIBRARY_CONVERTERSROBOT_LIBRARY_DOC_FORMATROBOT_LIBRARY_LISTENER 属性:

from robot.api.deco import library

from example import Listener


@library(scope='GLOBAL', version='3.2b1', doc_format='reST', listener=Listener())
class Example:
    ...

@library 装饰器还通过将 ROBOT_AUTO_KEYWORDS 参数默认设置为 False 来禁用自动关键字发现。这意味着必须使用 @keyword 装饰器来装饰方法以将其暴露为关键字。如果只需要这个行为而不需要进一步配置,装饰器也可以不带括号使用:

from robot.api.deco import library


@library
class Example:
    ...

如果需要,可以使用 auto_keywords 参数启用自动关键字发现:

from robot.api.deco import library


@library(scope='GLOBAL', auto_keywords=True)
class Example:
    ...

@library 装饰器只在使用了相应的参数 scopeversionconvertersdoc_formatlistener 时才设置类属性 ROBOT_LIBRARY_SCOPEROBOT_LIBRARY_VERSIONROBOT_LIBRARY_CONVERTERSROBOT_LIBRARY_DOC_FORMATROBOT_LIBRARY_LISTENERROBOT_AUTO_KEYWORDS 属性始终被设置,其存在可以作为 @library 装饰器已被使用的标志。当属性被设置时,它们会覆盖可能存在的类属性。

当一个类被 @library 装饰器装饰后,即使库导入只指向包含它的模块,它也会被用作库。无论类名是否与模块名匹配,都是如此。

注意: @library 装饰器是 Robot Framework 3.2 的新功能,converters 参数是 Robot Framework 5.0 的新功能,指定导入模块中的类应被用作库是 Robot Framework 7.2 的新功能。

创建关键字

哪些方法被视为关键字

Robot Framework 默认使用内省来找出库包含哪些属性,并将所有不以下划线开头的函数和方法视为关键字。例如,这个库实现了一个关键字 My Keyword:

def my_keyword(arg):
    return _helper(arg)

def _helper(arg):
    return arg.upper()

注意: 在 Python 中,任何以下划线开头的内容都被视为私有的,Robot Framework 遵循这一惯例。

自动将所有公共方法和函数视为关键字通常运行良好,特别是在简单情况下,但也有不希望如此的场景。例如,当以类实现库时,可能会意外地将基类中的方法也视为关键字。当以模块实现库时,导入到模块命名空间的函数成为关键字可能是更大的意外。例如,这个库如预期地实现了关键字 Example Keyword,但也意外地实现了关键字 Current Thread:

from threading import current_thread


def example_keyword():
    name = current_thread().name
    print(f"Running in thread '{name}'.")

下一节解释控制关键字发现和避免上述问题的不同方式。

控制关键字发现

本节解释如何控制哪些方法和函数成为关键字。

在基于模块的库中避免导入的公共函数

如前一节所述,对于基于模块的库,导入的函数也会成为关键字。有两种简单的方式可以调整导入本身来避免这个问题:

  1. 只导入模块,不导入函数:
import threading


def example_keyword():
    name = threading.current_thread().name
    print(f"Running in thread '{name}'.")
  1. 使用导入别名为导入的函数添加下划线前缀:
from threading import current_thread as _current_thread


def example_keyword():
    name = _current_thread().name
    print(f"Running in thread '{name}'.")

虽然这两种解决方案都很简单,但它们不够显式,存在有人重构代码时意外将函数暴露为关键字的风险。添加注释或使用下面讨论的方法来限制暴露的关键字可能是个好主意,至少在库变得更大时。

使用 @library 装饰器

对于基于类的库,禁用公共方法成为关键字的最简单方式是使用 @library 装饰器。该装饰器默认禁用自动关键字发现,要求使用 @keyword 装饰器显式标记关键字。例如,这个库只创建一个关键字 My Keyword:

from robot.api.deco import keyword, library


@library
class MyLibrary:

    @keyword
    def my_keyword(self, arg):
        return self.helper(arg)

    def helper(self, arg):
        return arg.upper()

使用 ROBOT_AUTO_KEYWORDS 属性

禁用自动关键字发现的另一种方式是将特殊的 ROBOT_AUTO_KEYWORDS 属性设置为 False,这是 @library 装饰器的替代方案。这对于无法自身被装饰的基于模块的库特别有用:

from robot.api.deco import keyword


ROBOT_AUTO_KEYWORDS = False


@keyword
def my_keyword(arg):
    return helper(arg)

def helper(arg):
    return arg.upper()

注意: @library 装饰器内部也设置了 ROBOT_AUTO_KEYWORDS 属性。

使用 @not_keyword 装饰器

模块中的函数和类中的方法可以使用 @not_keyword 装饰器显式标记为"非关键字":

from robot.api.deco import not_keyword


def my_keyword(arg):
    return helper(arg)

@not_keyword
def helper(arg):
    return arg.upper()

当库以模块实现时,可以显式调用这个装饰器来避免将导入的函数暴露为关键字:

from threading import current_thread

from robot.api.deco import not_keyword


not_keyword(current_thread)  # 不将 `current_thread` 暴露为关键字。


def example_keyword():
    thread_name = current_thread().name
    print(f"Running in thread '{thread_name}'.")

使用 __all__ 属性

Python 模块可以定义特殊的 __all__ 属性来指定它们包含哪些公共名称。如果基于模块的库有这样的属性,Robot Framework 会尊重它并且只将列出的函数视为关键字:

__all__ = ["my_keyword"]


def my_keyword(arg):
    return helper(arg)

def helper(arg):
    return arg.upper()

使用 get_keyword_names 方法

基于类的库可以使用特殊的 get_keyword_names 方法显式告诉 Robot Framework 哪些方法是关键字,该方法必须返回一个暴露的方法名列表:

class MyLibrary:

    def get_keyword_names(self):
        return ["my_keyword"]

    def my_keyword(self, arg):
        return self.helper(arg)

    def helper(self, arg):
        return arg.upper()

除了获取方法名列表之外的所有内容与其他静态库的工作方式完全相同。然而,也可以通过利用 Python 的 __getattr__ 方法动态创建实际的关键字——如果返回的方法名不存在,Python 就会调用该方法:

class MyLibrary:

    def get_keyword_names(self):
        return ["normal_keyword", "dynamic_keyword"]

    def normal_keyword(self, arg):
        print("This is a normal keyword.")

    def __getattr__(self, name):
        if name != "dynamic_keyword":
            raise AttributeError(name)

        def dynamically_created_keyword():
            print("This is a dynamically created keyword.")

        return dynamically_created_keyword

在上面的示例中,实际的关键字是在 __getattr__ 方法内部定义的。在更实际的场景中,它可以是导入的或从某个对象动态获取的。

注意: 拥有 get_keyword_names 方法但其他方面与普通静态库工作方式相同的库有时被称为混合库(hybrid library),这种 API 可以称为混合库 API。原因是使用动态库 API 的库也使用 get_keyword_names 方法来指定关键字,但它们执行关键字的方式不同。

注意: 由于历史原因,get_keyword_names 方法也可以拼写为 getKeywordNames。推荐使用前一种变体。

使用动态库 API

动态库 API 要求使用 get_keyword_names 方法显式列出已实现的关键字。这完全避免了方法或函数可能意外暴露为关键字的问题。

关键字名称

测试数据中使用的关键字名称与方法名进行比较以找到实现这些关键字的方法。名称比较不区分大小写,并且空格和下划线也被忽略。例如,方法 hello 映射到关键字名称 Hello、hello 甚至 h e l l o。类似地,do_nothingdoNothing 方法都可以用作测试数据中的 Do Nothing 关键字。

实现为模块的示例库位于 MyLibrary.py 文件中:

def hello(name):
    print(f"Hello, {name}!")

def do_nothing():
    pass

下面的示例说明了如何使用上面的示例库。如果你想自己尝试,请确保库在模块搜索路径中。

*** Settings ***
Library    MyLibrary

*** Test Cases ***
My Test
    Do Nothing
    Hello    world

设置自定义名称

可以为关键字暴露一个不同的名称,而不是映射到方法名的默认关键字名称。这可以通过将方法的 robot_name 属性设置为所需的自定义名称来实现:

def login(username, password):
    ...

login.robot_name = 'Login via user panel'
*** Test Cases ***
My Test
    Login Via User Panel    ${username}    ${password}

与上面的示例中显式设置 robot_name 属性不同,使用 @keyword 装饰器通常是最简单的方式:

from robot.api.deco import keyword


@keyword('Login via user panel')
def login(username, password):
    ...

不带参数使用此装饰器不会影响暴露的关键字名称,但仍然会设置 robot_name 属性。这允许在不实际更改关键字名称的情况下标记要暴露为关键字的方法。拥有 robot_name 属性的方法即使方法名本身以下划线开头也会创建关键字。

设置自定义关键字名称还可以使库关键字能够使用嵌入式参数语法接受参数。

关键字标签

库关键字和用户关键字都可以拥有标签(tag)。库关键字可以通过将方法的 robot_tags 属性设置为所需标签的列表来定义它们。与设置自定义名称类似,使用 @keyword 装饰器来设置这个属性是最简单的方式:

from robot.api.deco import keyword


@keyword(tags=['tag1', 'tag2'])
def login(username, password):
    ...

@keyword('Custom name', ['tags', 'here'])
def another_example():
    ...

另一种设置标签的方式是在关键字文档的最后一行以 Tags: 前缀给出,标签之间用逗号分隔。例如:

def login(username, password):
    """Log user in to SUT.

    Tags: tag1, tag2
    """
    ...

关键字参数

使用静态和混合 API 时,关键字需要多少参数的信息直接从实现它的方法获取。使用动态库 API 的库有其他方式共享这些信息,因此本节与它们无关。

最常见也是最简单的情况是关键字需要确定数量的参数。在这种情况下,方法简单地接受恰好那些参数。例如,一个不带参数的关键字的实现方法也不接受参数,一个带一个参数的关键字的实现方法也接受一个参数,以此类推。

接受不同数量参数的示例关键字:

def no_arguments():
    print("Keyword got no arguments.")

def one_argument(arg):
    print(f"Keyword got one argument '{arg}'.")

def three_arguments(a1, a2, a3):
    print(f"Keyword got three arguments '{a1}', '{a2}' and '{a3}'.")

关键字的默认值

关键字使用的某些参数具有默认值通常是有用的。

在 Python 中,一个方法总是只有一个实现,可能的默认值在方法签名中指定。这种语法对所有 Python 程序员来说都很熟悉,如下所示:

def one_default(arg='default'):
    print(f"Got argument '{arg}'.")


def multiple_defaults(arg1, arg2='default 1', arg3='default 2'):
    print(f"Got arguments '{arg1}', '{arg2}' and '{arg3}'.")

上面的第一个示例关键字可以使用零个或一个参数。如果没有给出参数,arg 获取值 default。如果有一个参数,arg 获取该值,使用多于一个参数调用关键字将会失败。在第二个示例中,总是需要一个参数,但第二个和第三个参数有默认值,因此可以使用一到三个参数来使用该关键字。

*** Test Cases ***
Defaults
    One Default
    One Default    argument
    Multiple Defaults    required arg
    Multiple Defaults    required arg    optional
    Multiple Defaults    required arg    optional 1    optional 2

可变数量的参数(*varargs

Robot Framework 也支持接受任意数量参数的关键字。

Python 支持接受任意数量参数的方法。相同的语法在库中也可以使用,并且如下面的示例所示,它也可以与其他指定参数的方式结合使用:

def any_arguments(*args):
    print("Got arguments:")
    for arg in args:
        print(arg)

def one_required(required, *others):
    print(f"Required: {required}\nOthers:")
    for arg in others:
        print(arg)

def also_defaults(req, def1="default 1", def2="default 2", *rest):
    print(req, def1, def2, rest)
*** Test Cases ***
Varargs
    Any Arguments
    Any Arguments    argument
    Any Arguments    arg 1    arg 2    arg 3    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

自由关键字参数(**kwargs

Robot Framework 支持 Python 的 **kwargs 语法。如何使用接受自由关键字参数(也称为自由命名参数)的关键字,在创建测试用例一节中讨论。在本节中,我们看看如何创建这样的关键字。

如果你已经熟悉 kwargs 在 Python 中的工作方式,理解它们在 Robot Framework 测试库中的工作方式就相当简单了。下面的示例展示了基本功能:

def example_keyword(**stuff):
    for name, value in stuff.items():
        print(name, value)
*** Test Cases ***
Keyword Arguments
    Example Keyword    hello=world        # 日志输出 'hello world'。
    Example Keyword    foo=1    bar=42    # 日志输出 'foo 1' 和 'bar 42'。

基本上,关键字调用末尾所有使用命名参数语法 name=value 且不匹配任何其他参数的参数,都作为 kwargs 传递给关键字。要避免将像 foo=quux 这样的字面值用作自由关键字参数,必须使用转义(escape)如 foo\=quux

下面的示例说明了普通参数、varargs 和 kwargs 如何协同工作:

def various_args(arg=None, *varargs, **kwargs):
    if arg is not None:
        print('arg:', arg)
    for value in varargs:
        print('vararg:', value)
    for name, value in sorted(kwargs.items()):
        print('kwarg:', name, value)
*** Test Cases ***
Positional
    Various Args    hello    world                # 日志输出 'arg: hello' 和 'vararg: world'。

Named
    Various Args    arg=value                     # 日志输出 'arg: value'。

Kwargs
    Various Args    a=1    b=2    c=3             # 日志输出 'kwarg: a 1'、'kwarg: b 2' 和 'kwarg: c 3'。
    Various Args    c=3    a=1    b=2             # 与上面相同。顺序无关紧要。

Positional and kwargs
    Various Args    1    2    kw=3                # 日志输出 'arg: 1'、'vararg: 2' 和 'kwarg: kw 3'。

Named and kwargs
    Various Args    arg=value      hello=world    # 日志输出 'arg: value' 和 'kwarg: hello world'。
    Various Args    hello=world    arg=value      # 与上面相同。顺序无关紧要。

关于使用与上面示例完全相同签名的真实示例,参见 Process 库中的 Run Process 和 Start Keyword 关键字。

仅限关键字参数

从 Robot Framework 3.1 开始,可以在不同的关键字中使用仅限命名参数(named-only argument)。这种支持由 Python 的仅限关键字参数提供。仅限关键字参数在可能的 *varargs 之后指定,或者当不需要 *varargs 时在专用的 * 标记之后指定。可能的 **kwargs 在仅限关键字参数之后指定。

示例:

def sort_words(*words, case_sensitive=False):
    key = str.lower if case_sensitive else None
    return sorted(words, key=key)

def strip_spaces(word, *, left=True, right=True):
    if left:
        word = word.lstrip()
    if right:
        word = word.rstrip()
    return word
*** Test Cases ***
Example
    Sort Words    Foo    bar    baZ
    Sort Words    Foo    bar    baZ    case_sensitive=True
    Strip Spaces    ${word}    left=False

仅限位置参数

Python 支持所谓的仅限位置参数,它可以指定一个参数只能作为位置参数(positional argument)给出,而不能作为命名参数(named argument)如 name=value 给出。仅限位置参数在普通参数之前指定,并且必须在它们之后使用特殊的 / 标记:

def keyword(posonly, /, normal):
    print(f"Got positional-only argument {posonly} and normal argument {normal}.")

上面的关键字可以这样使用:

*** Test Cases ***
Example
    # 仅限位置参数和普通参数用作位置参数。
    Keyword    foo    bar
    # 普通参数也可以使用命名方式。
    Keyword    foo    normal=bar

如果仅限位置参数使用了包含等号的值如 example=usage,即使 = 之前的部分匹配参数名,也不会被视为命名参数语法。不过这条规则仅在仅限位置参数在其正确位置使用且之前没有其他参数使用命名参数语法时才适用。

*** Test Cases ***
Example
    # 在这种情况下,仅限位置参数获取字面值 `posonly=foo`。
    Keyword    posonly=foo    normal=bar
    # 这会失败。
    Keyword    normal=bar    posonly=foo

仅限位置参数从 Robot Framework 4.0 开始完全支持。将它们用作位置参数在更早的版本中也有效,但将它们用作命名参数会在 Python 端导致错误。

参数转换

在 Robot Framework 测试数据中定义的参数默认以 Unicode 字符串的形式传递给关键字。然而,有几种方式可以使用非字符串值:

基于函数注解、使用 @keyword 装饰器指定的类型以及参数默认值的自动参数转换都是 Robot Framework 3.1 的新功能。支持的转换一节指定了这些情况下支持哪些参数转换。

在 Robot Framework 4.0 之前,只有当给定参数是字符串时才会进行自动转换。如今无论参数类型如何都会进行转换。

手动参数转换

如果没有向 Robot Framework 指定类型信息,所有不是通过变量传递的参数都将以 Unicode 字符串的形式提供给关键字。这包括以下这样的情况:

*** Test Cases ***
Example
    Example Keyword    42    False

在关键字内部总是可以转换作为字符串传递的参数。在简单情况下,这意味着使用 int()float() 将参数转换为数字,但其他类型的转换也是可能的。处理布尔值时需要注意,因为 Python 将所有非空字符串(包括字符串 False)视为真值。Robot Framework 自己的 robot.utils.is_truthy() 工具很好地处理了这个问题,因为它将 FALSENONONE(不区分大小写)视为假值:

from robot.utils import is_truthy


def example_keyword(count, case_insensitive):
    count = int(count)
    if is_truthy(case_insensitive):
        ...

关键字也可以通过 robot.api.TypeInfo 类及其 convert 方法使用 Robot Framework 的参数转换功能。如果所需的转换逻辑更复杂或需要比简单使用 int() 更好的错误报告,这会很有用。

from robot.api import TypeInfo


def example_keyword(count, case_insensitive):
    count = TypeInfo.from_type(int).convert(count)
    if TypeInfo.from_type(bool).convert(case_insensitive):
        ...

提示: 一般建议使用类型提示或其他方式指定类型,让 Robot Framework 自动处理参数转换。手动参数转换只应在特殊情况下使用。

注意: robot.api.TypeInfo 是 Robot Framework 7.0 的新功能。

使用函数注解指定参数类型

从 Robot Framework 3.1 开始,如果参数类型信息可用且类型被识别,传递给关键字的参数会自动转换。指定类型最自然的方式是使用 Python 函数注解。例如,前面示例中的关键字可以如下实现,参数将自动转换:

def example_keyword(count: int, case_insensitive: bool = True):
    if case_insensitive:
        ...

关于自动转换的类型列表以及这些类型接受哪些值,参见下面的支持的转换一节。如果一个参数拥有受支持的类型但给出了无法转换的值,将会产生错误。只注解部分参数是可以的。

使用非受支持类型注解参数不是错误,也可以将注解用于类型之外的目的。在这些情况下不会进行转换,但注解仍会显示在 Libdoc 生成的文档中。

关键字还可以使用签名末尾的 -> 表示法指定返回类型注解,如 def example() -> int:。这些信息在执行期间不会被使用,但从 Robot Framework 7.0 开始,它会被 Libdoc 显示以用于文档目的。

使用 @keyword 装饰器指定参数类型

指定显式参数类型的另一种方式是使用 @keyword 装饰器。从 Robot Framework 3.1 开始,它接受一个可选的 types 参数,可以用于将参数类型指定为将参数名映射到类型的字典,或者作为根据位置将参数映射到类型的列表。下面的两种方式展示了实现与之前示例相同关键字的方法:

from robot.api.deco import keyword


@keyword(types={'count': int, 'case_insensitive': bool})
def example_keyword(count, case_insensitive=True):
    if case_insensitive:
        ...

@keyword(types=[int, bool])
def example_keyword(count, case_insensitive=True):
    if case_insensitive:
        ...

无论使用哪种方式,都不需要为所有参数指定类型。当以列表形式指定类型时,可以使用 None 来标记某个参数没有类型信息,末尾的参数可以完全省略。例如,以下两个关键字都只为第二个参数指定了类型:

@keyword(types={'second': float})
def example1(first, second, third):
    ...

@keyword(types=[None, float])
def example2(first, second, third):
    ...

从 Robot Framework 7.0 开始,可以通过在类型字典中使用键 'return' 和适当的类型来指定关键字的返回类型。这些信息在执行期间不会被使用,但会被 Libdoc 显示以用于文档目的。

如果使用 @keyword 装饰器指定了任何类型,该关键字的注解类型信息将被忽略。将 types 设置为 None@keyword(types=None) 会完全禁用类型转换,这样默认值获取的类型信息也会被忽略。

基于默认值的隐式参数类型

如果没有通过注解或 @keyword 装饰器显式获取类型信息,Robot Framework 3.1 及更新版本会尝试根据可能的参数默认值来获取。在这个示例中,countcase_insensitive 分别获取类型 intbool

def example_keyword(count=-1, case_insensitive=True):
    if case_insensitive:
        ...

当类型信息基于默认值隐式获取时,参数转换本身不如显式获取信息时严格:

如果一个参数有显式类型和默认值,首先基于显式类型尝试转换。如果失败,则基于默认值尝试转换。在这种特殊情况下,基于默认值的转换是严格的,转换失败会导致错误。

如果不希望基于默认值进行参数转换,可以使用 @keyword 装饰器如 @keyword(types=None) 禁用整个参数转换。

注意: 在 Robot Framework 4.0 之前,只有当参数没有显式类型时才会基于默认值进行转换。

支持的转换

下表列出了 Robot Framework 3.1 及更新版本将参数转换到的类型。以下特征适用于所有转换:

注意: 如果一个参数同时拥有类型提示和默认值,首先基于类型提示尝试转换,然后如果失败,再基于默认值类型尝试。此行为将来可能会更改,以便基于默认值的转换在参数没有类型提示时进行。这将改变 arg: list = None 这样的情况中的转换行为,其中 None 转换将不再被尝试。强烈建议库创建者现在就显式指定默认值类型如 arg: list | None = None

用于指定的类型可以是具体类型(例如 list)、抽象基类(ABC)(例如 Sequence)或这些类型的子类(例如 MutableSequence)。typing 模块中映射到受支持具体类型或 ABC 的类型(例如 List)也受支持。在所有这些情况下,参数都被转换为具体类型。

除了使用实际类型(例如 int),还可以使用类型名称字符串(例如 'int')来指定类型,某些类型也有别名(例如 'integer')。类型到名称和别名的匹配不区分大小写。

"接受"列指定了哪些给定的参数类型会被转换。如果给定的参数已经具有预期的类型,则不进行转换。其他类型会导致转换失败。

类型 ABC 别名 接受 说明 示例
bool boolean str, int, float, None 字符串 TRUEYESON1 被转换为 True,空字符串以及 FALSENOOFF0 被转换为 False,字符串 NONE 被转换为 None。其他字符串和其他接受的值原样传递,允许关键字在需要时对它们进行特殊处理。所有字符串比较不区分大小写。True 和 false 字符串可以本地化。参见翻译附录了解支持的翻译。 TRUE(转换为 True), off(转换为 False), example(原样使用)
int Integral integer, long str, float 使用内置 int 函数进行转换。浮点数只有在能精确表示为整数时才被接受。例如 1.0 被接受而 1.1 不被接受。如果字符串转整数失败且类型基于默认值隐式获取,也会尝试浮点数转换。从 Robot Framework 4.1 开始,可以通过在值前添加 0x0o0b 前缀来使用十六进制、八进制和二进制数。从 Robot Framework 4.1 开始,空格和下划线可用作数字分组的视觉分隔符。从 Robot Framework 7.0 开始,表示浮点数的字符串只要其小数部分为零就被接受,这包括使用科学计数法如 1e100 42, -1, 10 000 000, 1e100, 0xFF, 0o777, 0b1010, 0xBAD_C0FFEE, ${1}, ${1.0}
float Real double str, Real 使用内置 float 函数进行转换。从 Robot Framework 4.1 开始,空格和下划线可用作数字分组的视觉分隔符。 3.14, 2.9979e8, 10 000.000 01, 10_000.000_01
Decimal str, int, float 使用 Decimal 类进行转换。当需要精确表示十进制数时,Decimal 比 float 更推荐。从 Robot Framework 4.1 开始,空格和下划线可用作数字分组的视觉分隔符。 3.14, 10 000.000 01, 10_000.000_01
str string, unicode 任何类型 所有参数被转换为 Unicode 字符串。大多数值简单地使用 str(value) 转换。一个例外是 bytes 直接映射到具有相同序数的 Unicode 码点,这意味着例如 b"hyv\xe4" 变成 "hyvä"。另一个例外是 Secret 对象被显式拒绝。Robot Framework 4.0 新功能。bytes 特殊转换和拒绝 Secret 对象是 Robot Framework 7.4 的新功能。
bytes str, bytearray 字符串被转换为 bytes,使得 256 以下的每个 Unicode 码点直接映射到匹配的字节。更高的码点不被允许。整数和整数序列直接转换为匹配的字节,必须在 0-255 范围内。支持整数和整数序列是 Robot Framework 7.4 的新功能。 字符串:good, hyvä(转换为 hyv\xe4), \x00(转换为 null 字节)。整数和整数序列:0(转换为 null 字节), [82, 70, 33](转换为 RF!
bytearray str, bytes 与 bytes 相同的转换,但结果是 bytearray。
datetime str, int, float 字符串时间戳预期为类似 ISO 8601 的格式 YYYY-MM-DD hh:mm:ss.mmmmmm,其中任何非数字字符都可以用作分隔符或完全省略分隔符。此外,只有日期部分是必须的,所有可能缺失的时间部分被视为零。特殊值 NOWTODAY(不区分大小写)可用于获取当前本地日期时间,这是 Robot Framework 7.3 的新功能。整数和浮点数被视为自 Unix epoch 以来的秒数。 2022-02-09T16:39:43.632269, 20220209 16:39, 2022-02-09, now(当前本地日期时间), TODAY(同上), ${1644417583.632269}(Epoch 时间)
date str 与 datetime 相同的时间戳转换,但所有时间部分预期被省略或为零。特殊值 NOWTODAY(不区分大小写)可用于获取当前本地日期,这是 Robot Framework 7.3 的新功能。 2018-09-12, 20180912, today(当前本地日期), NOW(同上)
timedelta str, int, float 字符串预期以 Robot Framework 支持的时间格式之一表示时间间隔:数字形式的时间、时间字符串形式或"计时器"字符串形式。整数和浮点数被视为秒数。 42(42 秒), 1 minute 2 seconds, 01:02(同上)
Path PathLike str 字符串被转换为 pathlib.Path 对象。在 Windows 上 / 自动转换为 \。Robot Framework 6.0 新功能。 /tmp/absolute/path, relative/path/to/file.ext, name.txt
Enum str 指定的类型必须是枚举(Enum 或 Flag 的子类),给定的参数必须匹配其成员名称。匹配成员名称时不区分大小写、空格、下划线和连字符,但精确匹配优先于规范化匹配。忽略连字符是 Robot Framework 7.0 的新功能。枚举文档和成员会自动显示在 Libdoc 生成的文档中。 NORTH(Direction.NORTH), north west(Direction.NORTH_WEST)
IntEnum str, int 指定的类型必须是基于整数的枚举(IntEnum 或 IntFlag 的子类),给定的参数必须匹配其成员名称或值。匹配成员名称的方式与 Enum 相同。值可以以整数形式或可转换为整数的字符串形式给出。枚举文档和成员会自动显示在 Libdoc 生成的文档中。Robot Framework 4.1 新功能。 OFF(PowerState.OFF), 1(PowerState.ON)
Literal 取决于用法 只接受指定的值。值可以是字符串、整数、bytes、布尔值、枚举和 None,使用的参数使用值类型特定的转换逻辑进行转换。字符串不区分大小写、空格、下划线和连字符,但精确匹配优先于规范化匹配。Literal 提供类似于 Enum 的功能,但不支持自定义文档。Robot Framework 7.0 新功能。 OFF, on
None str 字符串 NONE(不区分大小写)和空字符串被转换为 Python 的 None 对象。其他值会导致错误。转换空字符串是 Robot Framework 7.4 的新功能。 None
Any 任何类型 接受任何值。不进行转换。Robot Framework 6.1 新功能。
object 任何类型 接受任何值。不进行转换。Robot Framework 7.4 新功能。
list str, Sequence 将字符串和序列转换为 list。字符串必须是 Python 列表或元组字面量。使用 ast.literal_eval 函数转换,可能的元组进一步转换为列表。它们可以包含 ast.literal_eval 支持的任何值,包括列表和其他集合。如果参数是列表,则直接使用不转换。元组和其他序列转换为列表。支持元组字面量是 Robot Framework 7.4 的新功能。 ['one', 'two'], [('one', 1), ('two', 2)]
tuple str, Sequence 与 list 相同,但结果是 tuple。在 Robot Framework 7.4 之前,只支持元组字面量。 ('one', 'two')
Sequence str, Sequence 与 list 相同,但任何序列都被接受而不转换。如果使用的类型是 MutableSequence,不可变值将转换为列表。 [1, 2, 3](结果是 list), (1, 2, 3)(结果是 tuple)
set Set str, Collection 与 list 相同,但也支持集合对象和集合字面量,结果是 set。在 Robot Framework 7.4 之前,只支持集合字面量。 {1, 2, 3, 42}, set()(空集合)
frozenset str, Collection 与 set 相同,但结果是 frozenset。 {1, 2, 3, 42}, frozenset()(空集合)
dict dictionary str, Mapping 将字符串和映射转换为 dict。字符串必须是 Python 字典字面量。使用 ast.literal_eval 函数转换为 dict。它们可以包含 ast.literal_eval 支持的任何值,包括字典和其他集合。 {'a': 1, 'b': 2}, {'key': 1, 'nested': {'key': 2}}
Mapping map str, Mapping 与 dict 相同,但保留原始映射类型。如果类型是 MutableMapping,不可变值将转换为 dict
TypedDict str, Mapping 与 dict 相同,但字典项也会转换为指定的类型,并且不允许类型规范中未包含的项。Robot Framework 6.0 新功能。之前使用普通 dict 转换。 {'width': 1600, 'enabled': True}
Secret Secret 使用 Secret 类型作为类型提示确保只接受 Secret 变量作为参数。Robot Framework 7.4 新功能。

注意: 从 Robot Framework 5.0 开始,拥有转换器的类型会自动显示在 Libdoc 输出中。

注意: 在 Robot Framework 4.0 之前,大多数类型支持将字符串 NONE(不区分大小写)转换为 Python 的 None。该支持已被移除,None 转换仅在参数将 None 作为显式类型或默认值时进行。

指定多种可能的类型

可以指定一个参数有多种可能的类型。在这种情况下,参数转换按从左到右的顺序基于每种类型尝试,第一个成功转换的值被使用。如果所有转换都不成功,整个转换将失败。

Union 语法

使用函数注解时,指定参数有多种可能类型的自然语法是使用 Union

from typing import Union


def example(length: Union[int, float], padding: Union[int, str, None] = None):
    ...

使用 Python 3.10 或更新版本时,可以使用原生 union 语法int | float 代替:

def example(length: int | float, padding: int | str | None = None):
    ...

Robot Framework 7.0 增强了对 union 语法的支持,使得"字符串类型化"的 union 如 "int | float" 也可以工作。这种语法也适用于较旧的 Python 版本:

def example(length: "int | float", padding: "int | str | None" = None):
    ...

使用元组

另一种方式是将类型指定为元组。不建议在注解中使用,因为其他工具不支持该语法,但与 @keyword 装饰器配合使用效果很好:

from robot.api.deco import keyword


@keyword(types={'length': (int, float), 'padding': (int, str, None)})
def example(length, padding=None):
    ...

在上面的示例中,length 参数首先被转换为整数,如果失败则转换为浮点数。padding 首先被转换为整数,然后是字符串,最后是 None

当参数匹配其中一个类型时

如果给定的参数已经具有接受的类型之一,则不进行转换,参数原样使用。例如,如果类型为 length: int | floatlength 参数与浮点数 1.5 一起使用,它不会被转换为整数。注意,使用非字符串值如浮点数作为参数需要使用变量,如以下为 length 参数提供不同值的示例所示:

*** Test Cases ***
Conversion
    Example    10        # 参数是字符串。转换为整数。
    Example    1.5       # 参数是字符串。转换为浮点数。
    Example    ${10}     # 参数是整数。原样接受。
    Example    ${1.5}    # 参数是浮点数。原样接受。

如果接受的类型之一是字符串如 padding: int | str | None,那么当给定的参数是字符串时不进行转换。如以下为 padding 参数提供不同值的示例所示,在这些情况下通过变量传递其他类型也是可能的:

*** Test Cases ***
Conversion
    Example    1    big        # 参数是字符串。原样接受。
    Example    1    10         # 参数是字符串。原样接受。
    Example    1    ${10}      # 参数是整数。原样接受。
    Example    1    ${None}    # 参数是 `None`。原样接受。
    Example    1    ${1.5}     # 参数是浮点数。转换为整数。

如果给定的参数不具有任何接受的类型,按类型指定的顺序尝试转换。

注意: 在使用的值不匹配任何类型但可以成功转换为多种类型的情况下,类型的顺序会改变转换结果。

例如,如果类型是 float | int 且使用的值是字符串 42,结果将是浮点数 42.0 而不是整数 42。原因是字符串不匹配任何类型,并且首先尝试 float 转换。如果顺序改为 int | float,结果将是整数。

字符串 3.14 无论顺序如何都会转换为浮点数,因为 int 转换不成功。当值已经是整数或浮点数时,顺序也不影响结果,因为在这种情况下不需要转换。

处理 Anyobject

如果 Anyobject 单独用作类型提示如 arg: Anyarg: object,任何值都被接受而不转换。但在 union 中使用时的行为有所不同。

如果 Any 用在 union 中如 arg: int | Any,任何值都被接受而不转换。这允许使用 Any 作为完全禁用参数转换的逃脱手段。

另一方面,如果 object 用在 union 中如 arg: int | object,转换会尝试应用于 object 之前的类型。这允许尝试转换为某种或某些类型,但在转换失败时获取原始值。

注意: 虽然这种行为上的微妙差异可能有用,但也有些令人困惑,计划在 Robot Framework 8.0 中更改,使 Any 的行为与 object 相同。更多信息参见 issue #5571,如果你认为计划的更改不是好主意,请在该 issue 中评论。

处理无法识别的类型

如果在 union 中使用了 Robot Framework 无法识别的类型,处理方式如下:

例如,使用以下关键字,字符串 "7" 将被转换为整数,但字符串 "something" 将原样使用:

def example(argument: int | Unrecognized):
    ...

从 Robot Framework 6.1 开始,即使无法识别的类型列在已识别类型之前如 Unrecognized | int,上面的逻辑也能工作。在这种情况下也会尝试 int 转换,如果失败则参数原样传递。在更早的 Robot Framework 版本中,int 转换根本不会被尝试。

参数化类型

对于泛型,参数化语法如 list[int]dict[str, int] 也可以工作。使用此语法时,给定的值首先转换为基础类型,然后各个项转换为嵌套类型。不同泛型类型的转换规则如下:

使用原生 list[int] 语法需要 Python 3.9 或更新版本。如果需要支持更早的 Python 版本,可以使用 typing 模块中的匹配类型如 List[int] 或使用"字符串类型化"语法如 'list[int]'

注意: 泛型嵌套类型转换支持是 Robot Framework 6.0 的新功能。相同语法在更早版本中也可以工作,但参数只转换为基础类型,嵌套类型信息被忽略。

注意: "字符串类型化"参数化泛型支持是 Robot Framework 7.0 的新功能。

Secret 类型

Robot Framework 有一个自定义的 robot.api.types.Secret 类型,它封装值使其不显示在日志文件中。如果 Secret 类型用作参数类型,只接受 Secret 对象,尝试使用例如字面字符串会失败。封装的值可以通过 value 属性访问,关键字可以方便地使用它:

from example import SUT
from robot.api.types import Secret


def login_to_sut(user: str, token: Secret):
    SUT.login(user, token.value)

Secret 变量一节解释了如何在数据中、命令行以及其他地方创建 Secret 对象。在数据中涉及使用变量类型转换以及例如环境变量:

*** Variables ***
${USER}             robot
${TOKEN: Secret}    %{ROBOT_TOKEN}

*** Test Cases ***
Example
    Login to SUT    ${USER}    ${TOKEN}

关键字也可以通过使用 union 语法如 str | Secret 来接受 Secret 对象和字符串:

from example import SUT
from robot.api import logger
from robot.api.types import Secret


def input_password(password: str | Secret):
     logger.debug(f"Typing password: {password}")
     if isinstance(password, Secret):
         password = password.value
     SUT.input_password(password)

在这种情况下,重要的是不要记录或以其他方式泄露实际的秘密值。Secret 对象的字符串表示始终是 <secret>,因此在上面的示例中记录 f"Typing password: {password}" 是安全的,但在示例末尾记录就不安全了。Secret 对象的 repr()Secret(value=<secret>),因此在该字符串表示中也不会显示真实值。

在复杂类型提示中使用 Secret 类型与其他类型的工作方式类似。以下示例与上面的示例相似,但使用了带有 Secret 项的 TypedDict

from typing import TypedDict

from robot.api.types import Secret


class Credential(TypedDict):
    user: str
    token: Secret


def login_to_sut(credentials: Credential):
    SUT.login(credentials["user"], credentials["token"].value)
*** Variables ***
${TOKEN: Secret}    %{ROBOT_TOKEN}
&{CREDENTIALS}      user=robot    token=${TOKEN}

*** Test Cases ***
Example
    Login to SUT    ${CREDENTIALS}

警告: Secret 对象不会隐藏或加密它们的值。因此,所有能直接或间接通过 Robot Framework API 访问这些对象的代码都可以获取真实值。

警告: 关键字传递出去的实际秘密值可能被使用它们的外部模块或工具记录或以其他方式泄露。

注意: Secret 类型是 Robot Framework 7.4 的新功能。

自定义参数转换器

除了如前面几节所述的自动参数转换外,Robot Framework 还支持自定义参数转换。这个功能有两个主要用途:

参数转换器是函数或其他可调用对象,它们获取数据中使用的参数并在将参数传递给关键字之前将其转换为所需的格式。转换器通过将 ROBOT_LIBRARY_CONVERTERS 属性(区分大小写)设置为将所需类型映射到转换器的字典来注册到库。当以模块实现库时,此属性必须在模块级别设置;对于基于类的库,它必须是类属性。对于以类实现的库,也可以使用 @library 装饰器的 converters 参数。以下各节通过示例说明了这两种方式。

注意: 自定义参数转换器是 Robot Framework 5.0 的新功能。

覆盖默认转换器

假设我们想创建一个接受 date 对象的关键字,面向芬兰用户,那里常用的日期格式是 dd.mm.yyyy。用法可能如下所示:

*** Test Cases ***
Example
    Keyword    25.1.2022

自动参数转换支持日期,但它预期日期格式为 yyyy-mm-dd,因此不起作用。一种解决方案是创建自定义转换器并将其注册用于处理 date 转换:

from datetime import date


# 转换器函数。
def parse_fi_date(value):
    day, month, year = value.split('.')
    return date(int(year), int(month), int(day))


# 为指定类型注册转换器函数。
ROBOT_LIBRARY_CONVERTERS = {date: parse_fi_date}


# 使用自定义转换器的关键字。转换器基于参数类型解析。
def keyword(arg: date):
    print(f'year: {arg.year}, month: {arg.month}, day: {arg.day}')

转换错误

如果我们尝试使用上面的关键字传入无效参数如 invalid,它会以如下错误失败:

ValueError: Argument 'arg' got value 'invalid' that cannot be converted to date: not enough values to unpack (expected 3, got 1)

这个错误信息不够有用,也没有说明预期的格式。Robot Framework 无法自动提供更多信息,但转换器本身可以增强以验证输入。如果输入无效,转换器应该抛出一个带有适当消息的 ValueError。在这种特殊情况下,有多种方式来验证输入,但使用正则表达式可以同时验证输入在正确位置有点(.)以及日期各部分包含正确的数字位数:

from datetime import date
import re


def parse_fi_date(value):
    # 使用正则表达式验证输入,如果无效则抛出 ValueError。
    match = re.match(r'(\d{1,2})\.(\d{1,2})\.(\d{4})$', value)
    if not match:
        raise ValueError(f"Expected date in format 'dd.mm.yyyy', got '{value}'.")
    day, month, year = match.groups()
    return date(int(year), int(month), int(day))


ROBOT_LIBRARY_CONVERTERS = {date: parse_fi_date}


def keyword(arg: date):
    print(f'year: {arg.year}, month: {arg.month}, day: {arg.day}')

使用上面的转换器代码,用参数 invalid 使用关键字会以更有帮助的错误消息失败:

ValueError: Argument 'arg' got value 'invalid' that cannot be converted to date: Expected date in format 'dd.mm.yyyy', got 'invalid'.

限制值类型

默认情况下,Robot Framework 会尝试对所有给定参数使用转换器,无论其类型如何。这意味着如果之前的示例关键字使用了包含非字符串值的变量,转换代码会在 re.match 调用时失败。例如,尝试使用参数 ${42} 会失败如下:

ValueError: Argument 'arg' got value '42' (integer) that cannot be converted to date: TypeError: expected string or bytes-like object

这种错误情况自然可以在转换器代码中通过检查值类型来处理,但如果转换器只接受特定类型,通常只需限制值为该类型更简单。这只需要向转换器添加适当的类型提示:

def parse_fi_date(value: str):
    ...

注意这个类型提示不是用于在调用转换器之前转换值,它用于严格限制可以使用哪些类型。加上上面的内容后,使用 ${42} 调用关键字会失败如下:

ValueError: Argument 'arg' got value '42' (integer) that cannot be converted to date.

如果转换器可以接受多种类型,可以使用 Union 指定类型。例如,如果我们想增强关键字以也接受整数(将其视为自 Unix epoch 以来的秒数),可以像这样修改转换器:

from datetime import date
import re
from typing import Union


# 接受字符串和整数。
def parse_fi_date(value: Union[str, int]):
    # 整数单独转换。
    if isinstance(value, int):
        return date.fromtimestamp(value)
    match = re.match(r'(\d{1,2})\.(\d{1,2})\.(\d{4})$', value)
    if not match:
        raise ValueError(f"Expected date in format 'dd.mm.yyyy', got '{value}'.")
    day, month, year = match.groups()
    return date(int(year), int(month), int(day))


ROBOT_LIBRARY_CONVERTERS = {date: parse_fi_date}


def keyword(arg: date):
    print(f'year: {arg.year}, month: {arg.month}, day: {arg.day}')

转换自定义类型

之前示例的一个问题是 date 对象只能以 dd.mm.yyyy 格式给出。如果需要支持不同格式的日期(如以下示例),它将不起作用:

*** Test Cases ***
Example
    Finnish     25.1.2022
    US          1/25/2022
    ISO 8601    2022-01-22

这个问题的解决方案是创建自定义类型而不是覆盖默认的 date 转换:

from datetime import date
import re
from typing import Union

from robot.api.deco import keyword, library


# 自定义类型。扩展了现有类型,但这不是必需的。
class FiDate(date):

    # 转换器函数实现为类方法。它也可以是普通函数,
    # 但这种方式所有代码都在同一个类中。
    @classmethod
    def from_string(cls, value: str):
        match = re.match(r'(\d{1,2})\.(\d{1,2})\.(\d{4})$', value)
        if not match:
            raise ValueError(f"Expected date in format 'dd.mm.yyyy', got '{value}'.")
        day, month, year = match.groups()
        return cls(int(year), int(month), int(day))


# 另一个自定义类型。
class UsDate(date):

    @classmethod
    def from_string(cls, value: str):
        match = re.match(r'(\d{1,2})/(\d{1,2})/(\d{4})$', value)
        if not match:
            raise ValueError(f"Expected date in format 'mm/dd/yyyy', got '{value}'.")
        month, day, year = match.groups()
        return cls(int(year), int(month), int(day))


# 使用 '@library' 装饰器注册转换器。
@library(converters={FiDate: FiDate.from_string, UsDate: UsDate.from_string})
class Library:

    # 使用支持 'dd.mm.yyyy' 格式的自定义转换器。
    @keyword
    def finnish(self, arg: FiDate):
        print(f'year: {arg.year}, month: {arg.month}, day: {arg.day}')

    # 使用支持 'mm/dd/yyyy' 格式的自定义转换器。
    @keyword
    def us(self, arg: UsDate):
        print(f'year: {arg.year}, month: {arg.month}, day: {arg.day}')

    # 使用兼容 ISO-8601 的默认转换。
    @keyword
    def iso_8601(self, arg: date):
        print(f'year: {arg.year}, month: {arg.month}, day: {arg.day}')

    # 接受不同格式的日期。
    @keyword
    def any(self, arg: Union[FiDate, UsDate, date]):
        print(f'year: {arg.year}, month: {arg.month}, day: {arg.day}')

严格类型验证

如果参数本身已经是指定的类型,转换器根本不会被使用。因此很容易通过一个不接受任何值的自定义转换器来启用严格类型验证。例如,Example 关键字只接受 StrictType 实例:

class StrictType:
    pass


def strict_converter(arg):
    raise TypeError(f'Only StrictType instances accepted, got {type(arg).__name__}.')


ROBOT_LIBRARY_CONVERTERS = {StrictType: strict_converter}


def example(argument: StrictType):
    assert isinstance(argument, StrictType)

为方便起见,Robot Framework 允许将转换器设置为 None 以获得相同的效果。例如,以下代码与上面的代码行为完全相同:

class StrictType:
    pass


ROBOT_LIBRARY_CONVERTERS = {StrictType: None}


def example(argument: StrictType):
    assert isinstance(argument, StrictType)

注意: 使用 None 作为严格转换器是 Robot Framework 6.0 的新功能。更早的版本需要使用显式的转换器函数。

从转换器访问测试库

从 Robot Framework 6.1 开始,可以从转换器函数中访问库实例。这允许定义依赖于库状态的动态类型转换。例如,如果库可以配置为测试特定区域设置,你可以使用库状态来确定日期应该如何解析:

from datetime import date
import re


def parse_date(value, library):
    # 使用正则表达式验证输入,如果无效则抛出 ValueError。
    # 使用来自库状态的区域设置来确定解析格式。
    if library.locale == 'en_US':
        match = re.match(r'(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<year>\d{4})$', value)
        format = 'mm/dd/yyyy'
    else:
        match = re.match(r'(?P<day>\d{1,2})\.(?P<month>\d{1,2})\.(?P<year>\d{4})$', value)
        format = 'dd.mm.yyyy'
    if not match:
        raise ValueError(f"Expected date in format '{format}', got '{value}'.")
    return date(int(match.group('year')), int(match.group('month')), int(match.group('day')))


ROBOT_LIBRARY_CONVERTERS = {date: parse_date}


def keyword(arg: date):
    print(f'year: {arg.year}, month: {arg.month}, day: {arg.day}')

转换器函数的 library 参数是可选的,即如果转换器函数只接受一个参数,则省略 library 参数。通过使转换器函数只接受可变参数也可以达到类似效果,例如 def parse_date(*varargs)

转换器文档

关于转换器的信息会自动添加到 Libdoc 生成的输出中。这些信息包括类型名称、接受的值(如果使用类型提示指定)和文档。类型信息会自动链接到使用这些类型的所有关键字。

默认使用转换器函数的文档。如果它没有任何文档,则使用类型的文档。因此,以下两种为前面示例中的转换器添加文档的方式产生相同的结果:

class FiDate(date):

    @classmethod
    def from_string(cls, value: str):
        """Date in ``dd.mm.yyyy`` format."""
        ...


class UsDate(date):
    """Date in ``mm/dd/yyyy`` format."""

    @classmethod
    def from_string(cls, value: str):
        ...

一般建议添加文档以向用户提供更多关于转换的信息。为已有类型注册的转换器函数添加文档尤其重要,因为它们自身的文档在此上下文中可能不太有用。

@keyword 装饰器

虽然 Robot Framework 自动获取很多关于关键字的信息(如名称和参数),但有时需要进一步配置这些信息。这通常使用 robot.api.deco.keyword 装饰器来完成是最简单的。它有几个在其他地方详细解释的有用用途,这里仅作为参考列出:

@not_keyword 装饰器

robot.api.deco.not_keyword 装饰器可用于禁止函数或方法成为关键字。

使用自定义装饰器

在实现关键字时,有时使用 Python 装饰器来修改它们是有用的。然而,装饰器通常会修改函数签名,因此在确定关键字接受哪些参数时可能会迷惑 Robot Framework 的内省。这在使用 Libdoc 创建库文档和使用 RIDE 等外部工具时尤其成问题。避免此问题最简单的方式是使用 functools.wraps 装饰器来装饰装饰器本身。其他解决方案包括使用 decoratorwrapt 等外部模块,它们允许创建完全保留签名的装饰器。

注意: 对使用 functools.wraps 装饰的装饰器进行"解包"的支持是 Robot Framework 3.2 的新功能。

将参数嵌入关键字名称

库关键字也可以像用户关键字一样接受嵌入式参数(embedded argument)。本节主要介绍创建此类关键字所需的 Python 语法,嵌入式参数语法本身作为用户关键字文档的一部分进行详细介绍。

带有嵌入式参数的库关键字需要有一个自定义名称,通常使用 @keyword 装饰器设置。匹配嵌入式参数的值作为位置参数传递给实现关键字的函数或方法。如果函数或方法接受更多参数,它们可以作为普通的位置参数或命名参数传递给关键字。参数名不需要匹配嵌入式参数名,但这通常是个好的惯例。

接受嵌入式参数的关键字:

from robot.api.deco import keyword


@keyword('Select ${animal} from list')
def select_animal_from_list(animal):
    ...


@keyword('Number of ${animals} should be')
def number_of_animals_should_be(animals, count):
    ...

使用上述关键字的测试:

*** Test Cases ***
Embedded arguments
    Select cat from list
    Select dog from list

Embedded and normal arguments
    Number of cats should be    2
    Number of dogs should be    count=3

如果指定了类型信息,自动参数转换也适用于嵌入式参数:

@keyword('Add ${quantity} copies of ${item} to cart')
def add_copies_to_cart(quantity: int, item: str):
    ...

注意: 像用户关键字那样将类型信息嵌入关键字名称如 Add ${quantity: int} copies of ${item: str} to cart,库关键字不支持这种方式。

注意: 混合使用嵌入式参数和普通参数的支持是 Robot Framework 7.0 的新功能。

异步关键字

从 Robot Framework 6.1 开始,可以像普通函数一样运行原生异步函数(通过 async def 创建):

import asyncio

from robot.api.deco import keyword


@keyword
async def this_keyword_waits():
    await asyncio.sleep(5)

你可以使用 asyncio.get_running_loop()asyncio.get_event_loop() 获取事件循环的引用。修改循环的运行方式时要小心,它是一个全局资源。例如,永远不要调用 loop.close(),因为这会使得无法运行更多的协程。如果你有任何函数或资源需要事件循环,即使没有显式使用 await,你也必须将函数定义为 async 以使事件循环可用。

更多功能示例:

import asyncio
from robot.api.deco import keyword


async def task_async():
    await asyncio.sleep(5)

@keyword
async def examples():
    tasks = [task_async() for _ in range(10)]
    results = await asyncio.gather(*tasks)

    background_task = asyncio.create_task(task_async())
    await background_task

    # 如果运行在 Python 3.10 或更高版本
    async with asyncio.TaskGroup() as tg:
        task1 = tg.create_task(task_async())
        task2 = tg.create_task(task_async())

注意: Robot Framework 等待函数完成。如果你想要一个长时间运行的任务,例如使用 asyncio.create_task()。管理任务并保存引用以避免被垃圾回收是你的责任。如果事件循环关闭时任务仍在等待,会在控制台打印一条消息。

注意: 如果关键字执行因某种原因无法继续,例如信号停止,Robot Framework 会取消 async 任务及其所有子任务。其他 async 任务将继续正常运行。

与 Robot Framework 通信

在调用实现关键字的方法之后,它可以使用任何机制与被测系统通信。然后它还可以向 Robot Framework 的日志文件发送消息、返回可以保存到变量中的信息,以及最重要的——报告关键字是通过还是失败。

报告关键字状态

报告关键字状态的方式很简单——使用异常。如果执行的方法抛出异常,关键字状态为 FAIL;如果正常返回,状态为 PASS

普通的执行失败和错误可以使用标准异常如 AssertionErrorValueErrorRuntimeError 报告。然而,在后续小节中解释的一些特殊情况下需要特殊异常。

错误消息

显示在日志、报告和控制台中的错误消息由异常类型及其消息创建。对于通用异常(例如 AssertionErrorExceptionRuntimeError),只使用异常消息;对于其他异常,消息以 ExceptionType: Actual message 的格式创建。

也可以避免将异常类型作为前缀添加到失败消息中(对于非通用异常)。这通过向异常添加一个值为 True 的特殊 ROBOT_SUPPRESS_NAME 属性来实现。

Python:

class MyError(RuntimeError):
    ROBOT_SUPPRESS_NAME = True

在所有情况下,异常消息尽可能提供信息是重要的。

错误消息中的 HTML

也可以使用 HTML 格式的错误消息,方式是以文本 *HTML* 开始消息:

raise AssertionError("*HTML* <a href='robotframework.org'>Robot Framework</a> rulez!!")

这种方法既可以在库中抛出异常时使用(如上面的示例),也可以在用户在测试数据中提供错误消息时使用。

自动截断过长的消息

如果错误消息超过 40 行,它会从中间自动截断,以防止报告变得太长和难以阅读。完整的错误消息始终显示在失败关键字的日志消息中。

回溯信息

异常的回溯信息也使用 DEBUG 日志级别记录。这些消息在日志文件中默认不可见,因为对普通用户来说它们很少有意义。在开发库时,使用 --loglevel DEBUG 运行测试通常是个好主意。

Robot Framework 提供的异常

Robot Framework 提供了一些异常,库可以用于报告失败和其他事件。这些异常通过 robot.api 包暴露,包含以下内容:

Failure

报告验证失败。与使用标准 AssertionError 相比,使用此异常没有实际区别。使用此异常的主要好处是其名称与其他提供的异常一致。

Error

报告执行中的错误。与系统行为不符预期相关的失败通常应使用 Failure 异常或标准 AssertionError 报告。此异常可用于例如关键字使用不当的情况。与使用此异常和标准 RuntimeError 相比,除了与其他提供的异常一致的命名外,没有实际区别。

ContinuableFailure

报告验证失败但允许继续执行。更多信息参见下面的可继续失败一节。

SkipExecution

将执行的测试或任务标记为跳过。更多信息参见下面的跳过测试一节。

FatalError

报告停止整个执行的错误。更多信息参见下面的停止测试执行一节。

注意: 所有这些异常都是 Robot Framework 4.0 的新功能。除了跳过测试(也是 Robot Framework 4.0 的新功能)外的其他功能,在更早的版本中可以通过其他方式实现。

可继续失败

可以在有失败的情况下继续测试执行。最简单的方式是使用提供的 robot.api.ContinuableFailure 异常:

from robot.api import ContinuableFailure


def example_keyword():
    if something_is_wrong():
        raise ContinuableFailure('Something is wrong but execution can continue.')
    ...

另一种方式是创建一个自定义异常,其特殊的 ROBOT_CONTINUE_ON_FAILURE 属性设置为 True 值。下面的示例演示了这一点。

class MyContinuableError(RuntimeError):
    ROBOT_CONTINUE_ON_FAILURE = True

跳过测试

可以使用库关键字跳过测试。最简单的方式是使用提供的 robot.api.SkipExecution 异常:

from robot.api import SkipExecution


def example_keyword():
    if test_should_be_skipped():
        raise SkipExecution('Cannot proceed, skipping test.')
    ...

另一种方式是创建一个自定义异常,其特殊的 ROBOT_SKIP_EXECUTION 属性设置为 True 值。下面的示例演示了这一点。

class MySkippingError(RuntimeError):
    ROBOT_SKIP_EXECUTION = True

停止测试执行

可以使测试用例失败以至于整个测试执行被停止。最简单的方式是使用提供的 robot.api.FatalError 异常:

from robot.api import FatalError


def example_keyword():
    if system_is_not_running():
        raise FatalError('System is not running!')
    ...

除了使用 robot.api.FatalError 异常外,还可以创建一个自定义异常,其特殊的 ROBOT_EXIT_ON_FAILURE 属性设置为 True 值。下面的示例说明了这一点。

class MyFatalError(RuntimeError):
    ROBOT_EXIT_ON_FAILURE = True

日志信息

异常消息不是向用户提供信息的唯一方式。除此之外,方法还可以通过写入标准输出流(stdout)或标准错误流(stderr)向日志文件发送消息,甚至可以使用不同的日志级别。另一种(通常更好的)日志记录方式是使用编程式日志 API。

默认情况下,方法写入标准输出的所有内容作为单个条目以 INFO 日志级别写入日志文件。写入标准错误的消息在其他方面被类似处理,但在关键字执行完成后会回显到原始 stderr。因此,如果你需要一些消息在执行测试的控制台上可见,可以使用 stderr。

使用日志级别

要使用 INFO 以外的其他日志级别,或创建多条消息,需要通过将级别嵌入消息中来显式指定日志级别,格式为 *LEVEL* Actual log message。在这种格式中,*LEVEL* 必须在行首,LEVEL 必须是可用的具体日志级别之一:TRACEDEBUGINFOWARNERROR,或者伪日志级别 HTMLCONSOLE。伪级别可分别用于记录 HTML 和记录到控制台。

错误和警告

ERRORWARN 级别的消息会自动写入控制台和日志文件中单独的 Test Execution Errors 部分。这使得这些消息比其他消息更加醒目,允许使用它们向用户报告重要但非关键性的问题。

记录 HTML

库正常记录的所有内容都会被转换为可以安全表示为 HTML 的格式。例如,<b>foo</b> 会在日志中完全按照原样显示,而不是显示为 foo。如果库想使用格式化、链接、显示图片等,可以使用特殊的伪日志级别 HTML。Robot Framework 会以 INFO 级别将这些消息直接写入日志,因此它们可以使用任何 HTML 语法。注意这个特性需要谨慎使用,因为例如一个放置不当的 </table> 标签会严重破坏日志文件。

使用公共日志 API 时,各种日志方法都有可选的 html 属性,可以设置为 True 以启用 HTML 格式记录。

时间戳

默认情况下,通过标准输出或错误流记录的消息在执行的关键字结束时获取时间戳。这意味着时间戳不准确,调试问题(特别是长时间运行的关键字)可能会有问题。

关键字有可能为记录的消息添加准确的时间戳(如有需要)。时间戳必须以自 Unix epoch 以来的毫秒数给出,并且必须放在日志级别之后用冒号分隔:

*INFO:1308435758660* Message with timestamp
*HTML:1308435758661* <b>HTML</b> message with timestamp

如下面的示例所示,添加时间戳很容易。不过使用编程式日志 API 获取准确时间戳更加简单。显式添加时间戳的一大好处是这种方式也适用于远程库接口。

import time


def example_keyword():
    timestamp = int(time.time() * 1000)
    print(f'*INFO:{timestamp}* Message with timestamp')

记录到控制台

库有多种方式将消息写入控制台。如前所述,警告和所有写入标准错误流的消息会同时写入日志文件和控制台。这两种方式都有一个限制,就是消息只有在当前执行的关键字完成后才会出现在控制台上。

从 Robot Framework 6.1 开始,库可以使用伪日志级别 CONSOLE 将消息同时记录到日志文件和控制台:

def my_keyword(arg):
    print('*CONSOLE* Message both to log and to console.')

这些消息会以 INFO 级别记录到日志文件,类似于 HTML 伪日志级别。使用这种方式时,消息只在关键字执行结束后才记录到控制台。

另一种方式是将消息写入 sys.__stdout__sys.__stderr__。使用这种方式时,消息会立即写入控制台,但不会写入日志文件:

import sys


def my_keyword(arg):
    print('Message only to console.', file=sys.__stdout__)

最后一种方式是使用公共日志 API。使用这种方式时,消息也会立即写入控制台:

from robot.api import logger


def log_to_console(arg):
    logger.console('Message only to console.')

def log_to_console_and_log_file(arg):
    logger.info('Message both to log and to console.', also_console=True)

日志记录示例

在大多数情况下,INFO 级别就够用了。DEBUGTRACE 级别用于编写调试信息。这些消息通常不显示,但它们可以帮助调试库本身可能存在的问题。WARNERROR 级别可以使消息更加醒目,HTML 在需要任何格式化时很有用。级别 CONSOLE 可用于消息需要同时在控制台和日志文件中显示时。

以下示例阐明了使用不同级别记录的工作方式。

print('Hello from a library.')
print('*WARN* Warning from a library.')
print('*ERROR* Something unexpected happen that may indicate a problem in the test.')
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('*CONSOLE* This logs into console and log file.')
print('*HTML* This is <b>bold</b>.')
print('*HTML* <a href="http://robotframework.org">Robot Framework</a>')

编程式日志 API

编程式 API 提供了比使用标准输出和错误流更清洁的日志记录方式。

公共日志 API

Robot Framework 有一个基于 Python 的日志 API,用于将消息写入日志文件和控制台。测试库可以像 logger.info('My message') 这样使用此 API,而不是通过标准输出记录如 print('*INFO* My message')。除了编程接口用起来干净得多之外,此 API 还有一个好处,即日志消息具有准确的时间戳。

公共日志 API 作为 API 文档的一部分在 https://robot-framework.readthedocs.org 有详细文档。下面是一个简单的使用示例:

from robot.api import logger


def my_keyword(arg):
    logger.debug(f"Got argument '{arg}'.")
    do_something()
    logger.info('<i>This</i> is a boring example', html=True)
    logger.console('Hello, console!')

一个明显的限制是使用此日志 API 的测试库依赖于 Robot Framework。如果 Robot Framework 没有运行,消息会自动重定向到 Python 的标准 logging 模块。

使用 Python 的标准 logging 模块

除了新的公共日志 API 外,Robot Framework 还提供了对 Python 标准 logging 模块的内置支持。其工作方式是模块的根 logger 接收的所有消息会自动传播到 Robot Framework 的日志文件。此 API 也会生成具有准确时间戳的日志消息,但不支持记录 HTML 消息或将消息写入控制台。一大好处是(如下面的简单示例所示)使用此日志 API 不会产生对 Robot Framework 的依赖。

import logging


def my_keyword(arg):
    logging.debug(f"Got argument '{arg}'.")
    do_something()
    logging.info('This is a boring example')

logging 模块的日志级别与 Robot Framework 略有不同。它的 DEBUGINFOWARNINGERROR 级别直接映射到对应的 Robot Framework 日志级别,CRITICAL 映射到 ERROR。自定义日志级别映射到小于该自定义级别的最接近标准级别。例如,介于 INFOWARNING 之间的级别映射到 Robot Framework 的 INFO 级别。

库初始化期间的日志

库也可以在测试库导入和初始化期间记录日志。这些消息不会像正常日志消息那样出现在日志文件中,而是写入 syslog。这允许记录关于库初始化的任何有用的调试信息。使用 WARNERROR 级别记录的消息也会在日志文件的 test execution errors 部分可见。

导入和初始化期间的日志可以使用标准输出和错误流以及编程式日志 API。以下两种方式都进行了演示。

在导入期间使用日志 API 记录的库:

from robot.api import logger


logger.debug("Importing library")


def keyword():
    ...

注意: 如果你在初始化期间(即 Python 的 __init__ 中)记录日志,消息可能会根据库作用域被多次记录。

返回值

关键字与核心框架通信的最后一种方式是返回从被测系统获取的信息或通过其他方式生成的信息。返回的值可以在测试数据中赋值给变量,然后作为其他关键字的输入使用,甚至来自不同的测试库。

值使用方法中的 return 语句返回。通常,一个值赋给一个标量变量,如下面的示例所示。这个示例也说明了可以返回任何对象并使用扩展变量语法来访问对象属性。

from mymodule import MyObject


def return_string():
    return "Hello, world!"

def return_object(name):
    return MyObject(name)
*** Test Cases ***
Returning one value
    ${string} =    Return String
    Should Be Equal    ${string}    Hello, world!
    ${object} =    Return Object    Robot
    Should Be Equal    ${object.name}    Robot

关键字也可以返回值以便同时赋给多个标量变量、赋给列表变量,或赋给标量变量和列表变量的组合。所有这些用法都要求返回的值是列表或类似列表的对象。

def return_two_values():
    return 'first value', 'second value'

def return_multiple_values():
    return ['a', 'list', 'of', 'strings']
*** Test Cases ***
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

检测 Robot Framework 是否在运行

从 Robot Framework 6.1 开始,可以通过使用 BuiltIn 库的 robot_runningdry_run_active 属性来轻松检测 Robot Framework 是否正在运行以及干运行模式是否激活。一个相对常见的用例是,库初始化器可能希望在库不是在执行期间使用而是被例如 Libdoc 初始化时避免做某些工作:

from robot.libraries.BuiltIn import BuiltIn


class MyLibrary:

    def __init__(self):
        builtin = BuiltIn()
        if builtin.robot_running and not builtin.dry_run_active:
            # 执行一些只在真实执行期间才有意义的初始化。

关于将 BuiltIn 库用作编程 API 的更多信息,包括另一个使用 robot_running 的示例,参见使用 BuiltIn 库一节。

使用线程通信

如果库使用线程,通常应该只从主线程与框架通信。如果工作线程有例如要报告的失败或要记录的信息,它应该先将信息传递给主线程,然后主线程可以使用异常或本节中解释的其他机制与框架通信。

当线程在后台运行而其他关键字正在执行时,这一点尤其重要。在这种情况下与框架通信的结果是未定义的,最坏情况下可能导致崩溃或损坏的输出文件。如果一个关键字在后台启动了某些东西,应该有另一个关键字来检查工作线程的状态并相应地报告收集到的信息。

非主线程使用编程式日志 API 的正常日志方法记录的消息会被静默忽略。

还有一个单独的 robotbackgroundlogger 项目中的 BackgroundLogger,它有类似于标准 robot.api.logger 的 API。正常的日志方法会忽略非主线程的消息,但 BackgroundLogger 会保存后台消息,以便稍后记录到 Robot 的日志中。

分发测试库

记录库文档

一个没有关于它包含什么关键字以及这些关键字做什么的文档的测试库几乎没有用。为了便于维护,强烈建议将库文档包含在源代码中并从中生成。基本上,这意味着使用 docstrings,如下面的示例所示。

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

Python 有工具可以从上面这样记录的库创建 API 文档。然而,这些工具的输出对某些用户来说可能有些技术性。另一种选择是使用 Robot Framework 自己的文档工具 Libdoc。此工具可以从使用静态库 API(如上面的库)的库创建库文档,也能处理使用动态库 API 的库。

关键字文档的第一个逻辑行(直到第一个空行)用于特殊目的,应包含关键字的简短整体描述。Libdoc 将它用作简短文档(例如作为工具提示),也会在测试日志中显示。

默认情况下,文档被视为遵循 Robot Framework 自己的文档格式规则。这种简单格式允许常用样式如 *bold*_italic_、表格、列表、链接等。也可以使用 HTML、纯文本和 reStructuredText 格式。有关如何在库源代码中设置格式的信息参见文档格式一节,更多关于格式的一般信息参见 Libdoc 章节。

注意: 在 Robot Framework 3.1 之前,简短文档只包含关键字文档的第一个物理行。

测试库

任何非平凡的测试库都需要被充分测试以防止其中的 bug。当然,这种测试应该是自动化的,以便在库更改时可以轻松重新运行测试。

Python 有优秀的单元测试工具,它们非常适合测试库。将它们用于此目的与用于其他测试没有大的区别。熟悉这些工具的开发者不需要学习任何新东西,不熟悉的开发者无论如何都应该学习它们。

也很容易使用 Robot Framework 本身来测试库,从而为它们提供实际的端到端验收测试。BuiltIn 库中有很多有用的关键字可用于此目的。特别值得一提的是 Run Keyword And Expect Error,它用于测试关键字是否正确报告错误。

是使用单元级别还是验收级别的测试方式取决于上下文。如果需要模拟实际的被测系统,在单元级别通常更容易。另一方面,验收测试确保关键字通过 Robot Framework 正常工作。如果你无法决定,当然可以同时使用两种方式。

打包库

库实现、记录和测试之后,还需要分发给用户。对于由单个文件组成的简单库,通常让用户将该文件复制到某处并相应设置模块搜索路径就够了。更复杂的库应该被打包以使安装更容易。

由于库是普通的编程代码,可以使用普通的打包工具来打包。有关打包和分发 Python 代码的信息,参见 https://packaging.python.org/。当使用 pip 或其他工具安装这样的包时,它会自动进入模块搜索路径。

弃用关键字

有时需要用新关键字替换现有关键字或完全删除它们。仅通知用户变更可能并不总是足够的,在运行时获取警告更有效。为此,Robot Framework 有能力将关键字标记为弃用。这使得从测试数据中找到旧关键字并删除或替换它们变得更容易。

可以通过在关键字文档开头以文本 *DEPRECATED(区分大小写)开始、并且在文档第一行也有一个结束的 * 来弃用关键字。例如,*DEPRECATED**DEPRECATED.**DEPRECATED in version 1.5.* 都是有效的标记。

当执行弃用的关键字时,会记录一个弃用警告,该警告也会显示在控制台和日志文件的 Test Execution Errors 部分。弃用警告以文本 Keyword '<name>' is deprecated. 开始,后面是弃用标记之后简短文档的剩余部分(如果有的话)。例如,如果执行以下关键字,日志文件中会出现如下所示的警告。

def example_keyword(argument):
    """*DEPRECATED!!* Use keyword `Other Keyword` instead.

    This keyword does something to given ``argument`` and returns results.
    """
    return do_something(argument)

此弃用系统适用于大多数测试库以及用户关键字。

处理 Robot Framework 的超时

Robot Framework 有自己的超时机制,可以在测试或关键字花费太多时间时停止关键字执行。关于超时有两个需要注意的事项。

超时发生时进行清理

超时在技术上是使用 robot.errors.TimeoutExceeded 异常实现的,该异常可能在关键字执行期间的任何时候发生。如果关键字想要确保可能的清理活动始终被执行,它需要处理这些异常。处理异常最简单的方式可能是使用 Python 的 try/finally 结构:

def example():
    try:
        do_something()
    finally:
        do_cleanup()

上面的好处是无论异常如何都会进行清理。如果需要特殊处理超时,可以显式捕获 TimeoutExceeded。在这种情况下,重要的是之后重新抛出原始异常:

from robot.errors import TimeoutExceeded

def example():
    try:
        do_something()
    except TimeoutExceeded:
        do_cleanup()
        raise

注意: TimeoutExceeded 异常在 Robot Framework 7.3 之前名为 TimeoutError。重命名是为了避免与 Python 的同名标准异常冲突。旧名称仍然作为向后兼容的别名存在于 robot.errors 模块中,如果需要支持较旧的 Robot Framework 版本可以使用它。

允许超时停止执行

Robot Framework 的超时可以停止普通的 Python 代码,但如果代码调用了使用 C 或其他语言实现的功能,超时可能无法工作。行为良好的关键字应该避免无法被中断的长时间阻塞调用。

例如,subprocess.run 在 Windows 上无法被中断,所以以下简单关键字在那里无法被超时停止:

import subprocess


def run_command(command, *args):
    result = subprocess.run([command, *args], encoding='UTF-8')
    print(f'stdout: {result.stdout}\nstderr: {result.stderr}')

这个问题可以通过使用更低级别的 subprocess.Popen 并在短超时的循环中处理等待来避免。不过这增加了相当多的复杂性,因此在所有情况下可能不值得。

import subprocess


def run_command(command, *args):
    process = subprocess.Popen([command, *args], encoding='UTF-8',
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    while True:
        try:
            stdout, stderr = process.communicate(timeout=0.1)
        except subprocess.TimeoutExpired:
            continue
        else:
            break
    print(f'stdout: {stdout}\nstderr: {stderr}')

使用 Robot Framework 的内部模块

测试库可以使用 Robot Framework 的内部模块,例如获取有关已执行测试和使用的设置的信息。不过,这种与框架通信的强大机制应该谨慎使用,因为并非所有 Robot Framework 的 API 都旨在供外部使用,它们可能在不同的框架版本之间发生重大变化。

可用的 API

API 文档单独托管在优秀的 Read the Docs 服务上。如果你不确定如何使用某些 API 或使用它们是否具有前向兼容性,请向邮件列表发送问题。

使用 BuiltIn 库

最安全的可用 API 是 BuiltIn 库中实现关键字的方法。关键字的更改很少,并且总是先弃用旧的用法。最有用的方法之一是 replace_variables,它允许访问当前可用的变量。以下示例演示了如何获取 ${OUTPUT_DIR},它是众多实用的自动变量之一。也可以从库中使用 set_test_variableset_suite_variableset_global_variable 设置新变量。

import os.path
from robot.libraries.BuiltIn import BuiltIn


def do_something(argument):
    builtin = BuiltIn()
    output = do_something_that_creates_a_lot_of_output(argument)
    if builtin.robot_running:
        output_dir = builtin.replace_variables('${OUTPUT_DIR}')
    else:
        output_dir = '.'
    with open(os.path.join(output_dir, 'output.txt'), 'w') as file:
        file.write(output)
    print('*HTML* Output written to <a href="output.txt">output.txt</a>')

如上面的示例所示,BuiltIn 还有一个方便的 robot_running 属性用于检测 Robot Framework 是否在运行。

使用 BuiltIn 中方法的唯一注意事项是所有 run_keyword 方法变体必须被特殊处理。使用 run_keyword 方法的方法本身必须使用 BuiltIn 模块中的 register_run_keyword 方法注册为 run keywords。该方法的文档解释了为什么需要这样做以及如何做。

扩展现有测试库

本节解释向现有测试库添加新功能以及在你自己的库中使用它们的不同方式。

修改原始源代码

如果你有权访问要扩展的库的源代码,自然可以直接修改源代码。这种方式最大的问题是,在不影响你的更改的情况下更新原始库可能会很困难。对于用户来说,使用一个与原始库功能不同的库也可能令人困惑。重新打包库也可能是一项额外的大任务。

如果增强功能是通用的并且你计划将其提交回原始开发者,这种方式效果非常好。如果你的更改被应用到原始库,它们将包含在未来的发布中,上面讨论的所有问题都将得到缓解。如果更改是非通用的,或者你出于某些其他原因无法提交回去,后续小节中解释的方式可能效果更好。

使用继承

另一种直接的扩展现有库的方式是使用继承。下面的示例说明了如何向 SeleniumLibrary 添加新的 Title Should Start With 关键字。

from robot.api.deco import keyword
from SeleniumLibrary import SeleniumLibrary


class ExtendedSeleniumLibrary(SeleniumLibrary):

    @keyword
    def title_should_start_with(self, expected):
        title = self.get_title()
        if not title.startswith(expected):
            raise AssertionError(f"Title '{title}' did not start with '{expected}'.")

与修改原始库相比,这种方式的一大区别是新库与原始库有不同的名称。好处是你可以很容易地判断你在使用自定义库,但一个大问题是你无法同时轻松使用新库和原始库。一方面你的新库与原始库有相同的关键字,这意味着总是存在冲突。另一个问题是这些库不共享它们的状态。

当你开始使用新库并想从一开始就添加自定义增强功能时,这种方式效果很好。否则本节中解释的其他机制可能更好。

直接使用其他库

因为测试库在技术上只是类或模块,使用另一个库的简单方式是导入它并使用它的方法。当方法是静态的且不依赖于库状态时,这种方式效果很好。这由之前使用 Robot Framework 的 BuiltIn 库的示例所说明。

然而,如果库有状态,事情可能不会如你所期望的那样工作。你在库中使用的库实例与框架使用的不同,因此已执行关键字所做的更改对你的库不可见。下一节解释如何获取与框架使用的相同库实例的访问权限。

从 Robot Framework 获取活动库实例

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(f"Title '{title}' did not start with '{expected}'.")

与直接导入库并在库有状态时使用相比,这种方式明显更好。相对于继承的最大好处是你可以正常使用原始库,并在需要时额外使用新库。这在下面的示例中得到了演示,其中前面示例中的代码预计在名为 SeLibExtensions 的新库中可用。

*** Settings ***
Library    SeleniumLibrary
Library    SeLibExtensions

*** Test Cases ***
Example
    Open Browser    http://example      # SeleniumLibrary
    Title Should Start With    Example  # SeLibExtensions
lyyyuna 沪ICP备2025110782号-1