跳转到主要内容

可插拔黑盒测试工具包

项目描述

概述

PBBT是一个用于黑盒测试的回归测试框架。它适用于测试具有良好定义的输入和输出接口的复杂软件组件。

  input    +----------+   output
o--------> | Software | --------->o
           +----------+

在黑盒测试中,一个测试用例是输入和预期输出数据的组合。测试框架使用给定的输入执行软件,并验证产生的输出是否与预期输出一致。

黑盒测试可以实现为许多不同类型的软件。例如,

  • 数据库系统: 输入是一个SQL语句,输出是一组记录;

  • Web服务: 输入是一个HTTP请求,输出是一个HTTP响应;

  • 命令行工具:输入是一系列命令行参数和stdin,输出是stdout

  • 图形用户界面应用程序:有几种可能的方法;例如,输入可以是一系列用户动作,输出可以是一个特定小部件的状态。

PBBT 是一个 Python 库和应用,它允许您

  • 使用内置的测试类型来测试命令行脚本和 Python 代码;

  • 注册自定义测试类型;

  • 以简洁的 YAML 格式准备测试输入;

  • 训练 模式下,运行测试用例并记录预期的输出;

  • 检查 模式下,运行测试用例并验证产生的输出是否与预先记录的预期输出一致。

PBBT 是一个在 MIT 许可下发布的免费软件。PBBT 由 Clark C. Evans 和 Kirill Simonov 来自 Prometheus Research, LLC 创建。

使用PBBT

要安装 PBBT,您可以使用 pip 软件包管理器

# pip install pbbt

此命令从 Python 包索引 下载并安装 PBBT 的最新版本。安装成功后,您应该能够导入 pbbt Python 模块并运行 pbbt 命令行工具。

要开始使用 PBBT,您需要创建一个包含输入数据的文件。例如,创建包含以下内容的 input.yaml

py: |
  print "Hello, World!"

此文件是 YAML 格式,它是一种类似于 JSON 的数据序列化语言,实际上是 JSON 的超集。上面的文件可以用 JSON 表示为

{ "py": "print \"Hello, World!\"\n" }

有关 YAML 语法和语义的说明,请参阅 http://yaml.org/

接下来,以 训练 模式执行 PBBT 以生成预期的输出数据。运行

$ pbbt input.yaml output.yaml --train

并在被要求时接受新的输出。PBBT 将将输出数据写入 output.yaml

py: print-hello-world
stdout: |
  Hello, World!

现在您可以在 检查 模式下启动 PBBT,其中它执行测试用例并验证预期和实际输出数据是否一致

$ pbbt input.yaml output.yaml

要将更多测试用例添加到 input.yaml,您需要将其转换为 测试套件

title: My Tests
tests:
- py: |
    print "Hello, World!"
- sh: echo Hello, World!

现在文件包含一个测试套件 My Tests,其中包含两个测试用例:一个与前面的例子相同,另一个执行 shell 命令 echo Hello, World!

sh: echo Hello, World!

此测试用例的输出是 shell 命令产生的 stdout。要记录预期输出,请再次以训练模式运行 pbbt

内置测试类型

开箱即用,PBBT 支持一组预定义的测试类型

  • 测试 Python 代码;

  • 测试 shell 命令;

  • 文件操作测试。

还有特殊的测试类型

  • 测试套件;

  • 包含;

  • 条件变量。

  • 通向其他测试系统的门户。

每个测试类型定义了输入和输出记录的结构,即必需和可选字段以及字段值的类型。在本节中,我们列出所有可用的测试类型并描述它们的输入字段。

常见字段

以下可选字段对所有有意义的测试类型都可用

skip: truefalse

如果为 true,则跳过此测试用例。

if: 变量、变量列表或 Python 表达式

对于 变量名称,只有在变量已定义且不为假时才运行此测试用例。

对于 变量列表,只有在至少有一个变量已定义且不为假时才运行此测试用例。

在Python表达式上,如果表达式评估为真,则运行此测试用例。您可以在表达式中使用任何条件变量。

除非:变量、变量列表或Python表达式

在变量名上,如果变量已定义且不为假,则跳过此测试用例。

在变量列表上,如果至少有一个变量已定义且不为假,则跳过此测试用例。

在Python表达式上,如果表达式评估为真,则跳过此测试用例。您可以在表达式中使用任何条件变量。

忽略:真、假或正则表达式

true上,允许实际输出和预期输出不等,测试用例必须无错误地执行。

在正则表达式上,在比较之前预处理实际输出和预期输出。

  1. 对输出数据进行正则表达式匹配,找到所有匹配项。

  2. 如果正则表达式不包含()子组,则从输出中删除所有匹配项。

  3. 如果正则表达式包含一个或多个()子组,则从输出中删除子组的内容。

正则表达式使用MULTILINEVERBOSE标志编译。

示例

title: Integration with MySQL
if: has_mysql
tests:
- set:
    MYSQL_HOST: localhost
    MYSQL_PORT: 3306
  unless: [MYSQL_HOST, MYSQL_PORT]
- read: /etc/mysql/my.cnf
  if: MYSQL_HOST == 'localhost'
- py: test-scalar-types.py
  ignore: |
    ^Today:.(\d\d\d\d)-(\d\d)-(\d\d)$
- py: test-array-types.py
  skip: true    # No array type in MySQL

测试套件

测试套件是一系列测试用例的集合。

套件可能包含其他套件,因此所有测试套件形成一个树状结构。从套件标识符形成的路径可以唯一地定位任何套件。我们使用文件系统表示法来表示套件路径(例如/path/to/suite)。

字段

title:文本

套件的标题。

suite:标识符(可选)

套件的标识符。如果未设置,则从标题生成。

tests:输入记录列表

套件的内容。

output:路径(可选)

如果设置,则从给定的文件中加载套件的预期输出。

示例

title: All Tests
suite: all
output: output.yaml
tests:
- py: ./test/core.py
- py: ./test/ext.py
- title: Database Tests
  tests:
  - py: ./test/sqlite.py
  - py: ./test/pgsql.py
  - py: ./test/mysql.py

在此示例中,Database Tests套件的路径为/all/database-tests

条件变量

此测试用例定义了一个条件变量。

变量可用于ifunless子句来有条件地启用或禁用测试用例。变量也可以通过全局字典__pbbt__在Python测试中设置或读取。

条件变量也可以使用命令行中的-D选项设置。

设置条件变量会影响同一测试套件中所有后续的测试用例。变量值在退出套件时重置。

字段

set:变量或变量字典

在变量名上,将给定变量的值设置为True

在字典上,设置给定变量的值。

示例

title: MySQL Tests
tests:
- set: MYSQL
- set:
    MYSQL_HOST: localhost
    MYSQL_PORT: 3306
  unless: [MYSQL_HOST, MYSQL_PORT]
- py: |
    # Determine the version of the MySQL server
    import MySQLdb
    connection = MySQLdb.connect(host=__pbbt__['MYSQL_HOST'],
                                 port=int(__pbbt__['MYSQL_PORT']),
                                 db='mysql')
    cursor = connection.cursor()
    cursor.execute("""SELECT VERSION()""")
    version_string = cursor.fetchone()[0]
    version = tuple(map(int, version_string.split('-')[0].split('.')))
    __pbbt__['MYSQL_VERSION'] = version
- py: test-ddl.py
- py: test-dml.py
- py: test-select.py
- py: test-new-features.py
  if: MYSQL_VERSION >= (5, 5)

包含测试

此测试用例从文件加载并执行测试用例。

字段

include:路径

要加载的文件。文件应包含YAML格式的输入测试记录。

示例

title: All Tests
tests:
- include: test/core.yaml
- include: test/ext.yaml
- include: test/sqlite.yaml
- include: test/pgsql.yaml
- include: test/mysql.yaml

Python代码

此测试用例执行Python代码并生成stdout

字段

py:路径或Python代码

在Python代码上,要执行的源代码。

在文件名上,包含要执行源代码的文件。

stdin:文本(可选)

标准输入的内容。

except:异常类型(可选)

如果设置,表示代码预期会引发给定类型的异常。

示例

title: Python tests
tests:
- py: hello.py
- py: &sum |
    # Sum of two numbers
    import sys
    a = int(sys.stdin.readline())
    b = int(sys.stdin.readline())
    c = a+b
    sys.stdout.write("%s\n" % c)
  stdin: |
    2
    2
- py: *sum
  stdin: |
    1
    -5
- py: *sum
  stdin: |
    one
    three
  except: ValueError

请注意,我们使用 YAML 锚点(用 &sum 表示)和别名(用 *sum 表示)来在多个测试中使用相同的代码片段。

Shell命令

此测试用例执行一个 shell 命令并产生 stdout

字段

sh:带参数列表的命令或可执行文件

要执行的 shell 命令。

stdin:文本(可选)

标准输入的内容。

cd:路径(可选)

在执行命令之前,将当前工作目录更改为指定的路径。

environ:变量字典(可选)

将给定的变量添加到命令环境。

exit:整数(可选)

预期的退出代码;默认为 0

示例

title: Shell tests
tests:
- sh: echo Hello, World!
- sh: cat
  stdin: |
    Hello, World!
- sh: [cat, /etc/shadow]
  exit: 1   # Permission denied

写入文件

此测试用例创建一个具有给定内容的文件。

字段

write:路径

要创建的文件。

data:文本

文件内容。

示例

write: test/tmp/data.txt
data: |
    Hello, World!

从文件读取

此测试的输出是文件的内容。

字段

read:路径

要读取的文件。

示例

read: test/tmp/data.txt

删除文件

此测试用例删除一个文件。如果文件不存在,则不会出错。

字段

rm:路径或路径列表

要删除的文件。

示例

rm: test/tmp/data.txt

创建目录

此测试用例创建一个目录。

如果需要,也将创建父目录。如果目录已存在,则不会出错。

字段

mkdir:路径

要创建的目录。

示例

mkdir: test/tmp

删除目录

此测试用例删除一个目录及其所有内容。

如果目录不存在,则不会出错。

字段

rmdir:路径

要删除的目录。

示例

rmdir: test/tmp

Doctest

此测试用例在一系列文件上执行 doctest

字段

doctest:路径模式

包含 doctest 会话的文件。

示例

doctest: test/test_*.rst

Unittest

此测试用例执行 unittest 测试套件。

字段

unittest:路径模式

包含 unittest 测试的文件。

示例

unittest: test/test_*.py

Pytest

此测试用例执行 py.test 测试套件。

必须安装来自 https://pytest.cn/pytest 包。

字段

pytest:路径模式

包含 py.test 测试的文件。

示例

pytest: test/test_*.py

覆盖率

此测试用例开始对 Python 代码进行覆盖率。

必须安装来自 http://nedbatchelder.com/code/coverage/coverage 包。

字段

coverage:文件名或 None

配置文件路径。

data_file:文件名

保存覆盖率数据的位置。

timidfalsetrue(可选)

使用更简单的跟踪函数。

branchfalsetrue(可选)

启用分支覆盖率。

source:包名或文件路径(可选)

要测量的源文件或包。

include:文件模式

要测量的文件。

omit:文件模式

要排除的文件。

示例

coverage:
source: pbbt
branch: true

覆盖率检查

此测试用例停止覆盖率并报告测量摘要。

字段

coverage-check:浮点数

预期的覆盖率百分比。

示例

coverage-check: 99.0

覆盖率报告

此测试用例停止覆盖率并将覆盖率报告保存到文件。

字段

coverage-report:目录

保存报告的位置。

示例

coverage-report: coverage

自定义测试类型

在本节中,我们讨论如何将自定义测试类型添加到 PBBT。

假设我们想要通过运行一系列SQL查询并验证是否得到预期的输出来测试一个SQL数据库。为了实现这种测试方案,我们需要一种方法来

  • 打开到数据库的连接;

  • 执行一个SQL语句并生成一系列记录。

输入文件可能看起来像这样

title: Database tests
tests:
# Remove the database file left after the last testing session.
- rm: test.sqlite
# Create a new SQLite database.
- connect: test.sqlite
# Run a series of SQL commands.
- sql: |
    SELECT 'Hello, World!';
- sql: |
    CREATE TABLE student (
        id      INTEGER PRIMARY KEY,
        name    TEXT NOT NULL,
        gender  CHAR(1) NOT NULL
                CHECK (gender in ('f', 'i', 'm')),
        dob     DATE NOT NULL
    );
- sql: |
    INSERT INTO student (id, name, gender, dob)
    VALUES (1001, 'Linda Wright', 'f', '1988-10-03'),
           (1002, 'Beth Thompson', 'f', '1988-01-24'),
           (1003, 'Mark Melton', 'm', '1984-06-05'),
           (1004, 'Judy Flynn', 'f', '1986-09-02'),
           (1005, 'Jonathan Bouchard', 'm', '1982-02-12');
- sql: |
    SELECT *
    FROM student
    ORDER BY dob;
- sql: |
    SELECT name
    FROM student
    WHERE id = 1003;

在这个输入文件中,我们使用了两种新的测试用例类型

连接

指定连接到SQLite数据库。

sql

指定要执行的SQL语句。

我们将编写一个PBBT扩展来实现这些测试类型。

创建一个名为 sql.py 的文件,内容如下

from pbbt import Test, Field, BaseCase, MatchCase, listof
import sqlite3, traceback, csv, StringIO

@Test
class ConnectCase(BaseCase):

    class Input:
        connect = Field(str)

    def check(self):
        self.state['connect'] = None
        try:
            self.state['connect'] = sqlite3.connect(self.input.connect)
        except:
            self.ui.literal(traceback.format_exc())
            self.ui.warning("exception occurred while"
                            " connecting to the database")
            self.ctl.failed()
        else:
            self.ctl.passed()

@Test
class SQLCase(MatchCase):

    class Input:
        sql = Field(str)

    class Output:
        sql = Field(str)
        rows = Field(listof(listof(object)))

    def render(self, output):
        stream = StringIO.StringIO()
        writer = csv.writer(stream, lineterminator='\n')
        writer.writerows(output.rows)
        return stream.getvalue()

    def run(self):
        connection = self.state.get('connect')
        if not connection:
            self.ui.warning("database connection is not defined")
            return
        rows = []
        try:
            cursor = connection.cursor()
            cursor.execute(self.input.sql)
            for row in cursor.fetchall():
                rows.append(list(row))
        except:
            self.ui.literal(traceback.format_exc())
            self.ui.warning("exception occurred while"
                            " executing a SQL query")
            connection.rollback()
            new_output = None
        else:
            connection.commit()
            new_output = self.Output(sql=self.input.sql, rows=rows)
        return new_output

要使用此扩展,请将参数 -E sql.py 添加到所有PBBT调用中。例如

$ pbbt -E sql.py input.yaml output.yaml --train

现在我们将逐行解释 sql.py 的内容。

第一行导入了我们将要使用的某些类和装饰器

from pbbt import Test, Field, BaseCase, MatchCase

要注册一个测试类型,使用 @Test 装饰器。以下是最通用的模板

@Test
class CustomCase(object):

    class Input:
        some_field = Field(...)
        [...]

    class Output:
        some_field = Field(...)
        [...]

    def __init__(self, ctl, input, output):
        self.ctl = ctl
        self.input = input
        self.output = output

    def __call__(self):
        [...]
        return new_output

装饰器的参数必须是一个遵守以下规则的类

  • 输入和输出记录的结构使用嵌套类 InputOutput 来描述。记录字段使用 Field 描述符指定。

  • 要创建一个测试用例,测试工具会创建该类的实例。类的构造函数接受三个参数

    ctl

    测试工具控制器。它用于用户交互、报告测试成功或失败,以及作为条件变量的存储。

    input

    输入记录。

    output

    预期的输出记录或 None

  • 通过调用其 __call__() 方法执行测试用例。此方法必须运行测试用例,生成新的输出记录,并将其与给定的预期输出记录进行比较。

    如果预期和实际输出记录不一致

    • check 模式下,方法必须报告失败。

    • train 模式下,方法可以请求用户允许更新预期输出。如果预期输出要更新,则方法应返回新的输出记录。

PBBT还提供了两个混合类:BaseCaseMatchCase。这些类实现了大多数常见测试类型所需的必要功能。

让我们回顾一下 connect 测试类型,它由 ConnectCase 类实现

@Test
class ConnectCase(BaseCase):

    class Input:
        connect = Field(str)

    def check(self):
        [...]

ConnectCase 继承自 BaseCase,这对于不产生输出数据且仅为其副作用而执行的测试是合适的。嵌套的 Input 类定义用于声明输入记录的字段。在这种情况下,输入记录只有一个文本字段 connect,其中包含数据库的名称。

BaseCase 继承的测试类型必须重写抽象方法 check()

import sqlite3, traceback
[...]

@Test
class ConnectCase(BaseCase):
    [...]

    def check(self):
        self.state['connect'] = None
        try:
            self.state['connect'] = sqlite3.connect(self.input.connect)
        except:
            self.ui.literal(traceback.format_exc())
            self.ui.warning("exception occurred while"
                            " connecting to the database")
            self.ctl.failed()
        else:
            self.ctl.passed()

此代码尝试创建一个新的数据库连接并将其存储为条件变量 connect。如果尝试失败,它将调用 ui.literal() 来显示异常跟踪回溯,并调用 ctl.failed() 来报告测试失败。否则,将调用 ctl.passed() 来表示测试成功。

接下来,让我们回顾一下 sql 测试类型

@Test
class SQLCase(MatchCase):

    class Input:
        sql = Field(str)

    class Output:
        sql = Field(str)
        rows = Field(listof(listof(object)))

    def run(self):
        [...]

    def render(self, output):
        [...]

此测试类型既有输入也有输出记录,这些记录通过嵌套类InputOutput进行描述。输入记录包含一个字段sql,它是一个要执行的SQL查询。输出记录包含两个字段:sqlrows。字段sql包含相同的SQL查询,用于将输出记录与相应的输入记录进行匹配。字段rows包含由SQL查询生成的输出行列表。

SQLCase类继承自MatchCase,后者是用于产生文本输出的测试类型的混合类。从MatchCase继承的测试类型必须重写两个方法

run()

执行测试用例并返回生成的输出记录。

render(output)

将输出记录转换为可打印形式。

SQLCase中,render()通过将输出行转换为CSV格式实现

import csv, StringIO
[...]

@Test
class SQLCase(MatchCase):
    [...]

    def render(self, output):
        stream = StringIO.StringIO()
        writer = csv.writer(stream, lineterminator='\n')
        writer.writerows(output.rows)
        return stream.getvalue()

方法run()的实现如下

@Test
class SQLCase(MatchCase):
    [...]

    def run(self):
        connection = self.state.get('connect')
        if not connection:
            self.ui.warning("database connection is not defined")
            return
        rows = []
        try:
            cursor = connection.cursor()
            cursor.execute(self.input.sql)
            for row in cursor.fetchall():
                rows.append(list(row))
        except:
            self.ui.literal(traceback.format_exc())
            self.ui.warning("exception occurred while"
                            " executing a SQL query")
            connection.rollback()
            new_output = None
        else:
            connection.commit()
            new_output = self.Output(sql=self.input.sql, rows=rows)
        return new_output

命令行界面

用法

pbbt [<options>] INPUT [OUTPUT]

在这里,INPUTOUTPUT是分别包含输入和输出测试数据的文件。

以下选项可用

-h--help

显示用法信息并退出。

-q--quiet

只显示警告和错误。

-T--train

以训练模式运行测试。

-P--purge

清除过时的输出数据。

-M N--max-errors N

N个测试失败后停止;0表示“永不”。

-D VAR-D VAR=VALUE--define VAR--define VAR=VALUE

设置条件变量。

-E FILE-E MODULE--extend FILE--extend MODULE

从文件或Python模块加载PBBT扩展。

-S ID--suite ID

运行特定的测试套件。

PBBT还可以从以下文件读取配置

setup.cfg

此文件采用INI格式,其中PBBT设置在[pbbt]部分定义。以下参数被识别:extendinputoutputdefinesuitetrainpurgemax_errorsquiet

pbbt.yaml

此文件必须是一个包含以下键的YAML文件:extendinputoutputdefinesuitetrainpurgemax-errorsquiet

PBBT还可以作为Distutils命令执行

python setup.py pbbt

在这种情况下,PBBT配置可以在setup.cfg中指定或通过命令行参数。

API参考

```python

伪类型 isinstance(X, ...):检查 X 是否是类型 T 的实例或等于 None

```python

伪类型 isinstance(X, ...):检查 X 是否是类型 T1 的实例或类型 T2 的实例等。

```python

伪类型 isinstance(X, ...):检查 X 是否等于给定的值之一。

```python

伪类型 isinstance(X, ...):检查 X 是否是类型为 T 的元素列表。

```python

伪类型 isinstance(X, ...):检查 X 是否是包含类型为 T1T2 等的字段元组。

```python

伪类型 isinstance(X, ...):检查 X 是否是键类型为 T1、值类型为 T2 的字典。

```python

with 子句一起使用以验证嵌套块引发给定类型的异常。

```python

验证函数调用 fn(*args, **kwds) 是否引发给定类型的异常。

```python

将给定的类注册为测试类型。

```python

描述输入或输出记录的字段。

check

字段值的类型。

default

如果字段缺失,则默认值。如果未设置,则字段为必需的。

order

如果设置,允许您覆盖默认字段顺序。

hint

字段的简短描述。

```python

所有输入和输出记录的基类。

Test 装饰器将嵌套的 InputOutput 类转换为 Record 子类。

以下方法可以或被重写

classmethod __recognizes__(keys)

检查键集是否与记录类型兼容。

默认实现检查键集中是否包含所有必需的记录字段。

classmethod __load__(mapping)

从记录键和值的映射生成记录实例。

__dump__()

生成字段键和值的列表。

__complements__(other)

检查两个记录是否为同一测试用例的互补输入和输出记录。

__clone__(**kv_fields)

使用给定字段的新值复制记录。

__str__()

生成可打印的表示。

```python

测试工具。

测试工具对象作为构造函数的第一个参数传递给测试用例。测试用例对象可以使用以下方法和属性。

属性

ui

提供用户交互服务。

training

如果设置,工具处于训练模式。

purging

如果设置,工具处于清除模式。

quiet

如果设置,仅显示警告和错误。

halted

如果设置,则测试过程已停止。

state

条件变量。

selection

选择的套件。

方法

passed(text=None)

证明当前测试用例已通过。

failed(text=None)

证明当前测试用例失败。

updated(text=None)

证明当前测试用例的输出数据已更新。

halt(text=None)

终止测试过程。

load_input(path)

从指定的文件中加载输入测试数据。

load_output(path)

从指定的文件中加载输出测试数据。

dump_output(path, data)

将输出测试数据保存到指定的文件。

run(case)

执行一个测试用例。

__call__(input_path, output_path)

使用给定的输入和输出运行测试过程。

pbbt.locate(record)

找到给定记录的位置。

pbbt.Location(filename, line)

输入或输出记录在YAML文档中的位置。

pbbt.run(input_path, output_path=None, **configuration)

从给定文件中加载测试数据并运行测试。

配置

ui

用户交互控制器。

variables

预定义的条件变量。

targets

选择的套件。

training

将工具集设置为训练模式。

purging

清除过时的输出数据。

max_errors

在工具集停止之前允许的最大失败次数。

quiet

仅显示警告和错误。

pbbt.main()

实现了pbbt命令行工具。

pbbt.BaseCase

适用于大多数测试类型的模板类。

子类需要重写以下方法

check()

在检查模式下运行测试用例。

train()

在训练模式下运行测试用例。默认实现简单地调用check()

pbbt.MatchCase

适用于产生输出的测试类型的模板类。

子类需要重写以下方法

run()

执行用例;返回产生的输出。

render(output)

将输出记录转换为文本。

pbbt.UI

用户交互服务的抽象类。

方法

part()

开始一个新的部分。

section()

开始一个子部分。

header(text)

显示部分标题。

notice(text)

显示通知。

warning(text)

显示警告。

error(text)

显示错误。

literal(text)

显示纯文本。

choice(text, *choices)

询问单选题。

pbbt.ConsoleUI(stdin=None, stdout=None, stderr=None)

实现控制台输入/输出的UI。

pbbt.SilentUI(backend)

实现用于--quiet选项的UI。

项目详情


下载文件

下载适合您平台的应用程序。如果您不确定选择哪个,请了解更多关于安装包的信息。

源分布

pbbt-0.2.0.tar.gz (46.8 kB 查看哈希值)

上传时间

构建分布

pbbt-0.2.0-py3-none-any.whl (33.0 kB 查看哈希值)

上传时间 Python 3

由以下提供支持

AWSAWS云计算和安全赞助商DatadogDatadog监控FastlyFastlyCDNGoogleGoogle下载分析MicrosoftMicrosoftPSF赞助商PingdomPingdom监控SentrySentry错误日志StatusPageStatusPage状态页面