可插拔黑盒测试工具包
项目描述
概述
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: true 或 false
如果为 true,则跳过此测试用例。
- if: 变量、变量列表或 Python 表达式
对于 变量名称,只有在变量已定义且不为假时才运行此测试用例。
对于 变量列表,只有在至少有一个变量已定义且不为假时才运行此测试用例。
在Python表达式上,如果表达式评估为真,则运行此测试用例。您可以在表达式中使用任何条件变量。
- 除非:变量、变量列表或Python表达式
在变量名上,如果变量已定义且不为假,则跳过此测试用例。
在变量列表上,如果至少有一个变量已定义且不为假,则跳过此测试用例。
在Python表达式上,如果表达式评估为真,则跳过此测试用例。您可以在表达式中使用任何条件变量。
- 忽略:真、假或正则表达式
在true上,允许实际输出和预期输出不等,测试用例必须无错误地执行。
在正则表达式上,在比较之前预处理实际输出和预期输出。
对输出数据进行正则表达式匹配,找到所有匹配项。
如果正则表达式不包含()子组,则从输出中删除所有匹配项。
如果正则表达式包含一个或多个()子组,则从输出中删除子组的内容。
正则表达式使用MULTILINE和VERBOSE标志编译。
示例
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。
条件变量
此测试用例定义了一个条件变量。
变量可用于if和unless子句来有条件地启用或禁用测试用例。变量也可以通过全局字典__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:文件名
保存覆盖率数据的位置。
- timid:false 或 true(可选)
使用更简单的跟踪函数。
- branch:false 或 true(可选)
启用分支覆盖率。
- 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
装饰器的参数必须是一个遵守以下规则的类
输入和输出记录的结构使用嵌套类 Input 和 Output 来描述。记录字段使用 Field 描述符指定。
要创建一个测试用例,测试工具会创建该类的实例。类的构造函数接受三个参数
- ctl
测试工具控制器。它用于用户交互、报告测试成功或失败,以及作为条件变量的存储。
- input
输入记录。
- output
预期的输出记录或 None。
通过调用其 __call__() 方法执行测试用例。此方法必须运行测试用例,生成新的输出记录,并将其与给定的预期输出记录进行比较。
如果预期和实际输出记录不一致
在 check 模式下,方法必须报告失败。
在 train 模式下,方法可以请求用户允许更新预期输出。如果预期输出要更新,则方法应返回新的输出记录。
PBBT还提供了两个混合类:BaseCase 和 MatchCase。这些类实现了大多数常见测试类型所需的必要功能。
让我们回顾一下 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): [...]
此测试类型既有输入也有输出记录,这些记录通过嵌套类Input和Output进行描述。输入记录包含一个字段sql,它是一个要执行的SQL查询。输出记录包含两个字段:sql和rows。字段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]
在这里,INPUT和OUTPUT是分别包含输入和输出测试数据的文件。
以下选项可用
- -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]部分定义。以下参数被识别:extend、input、output、define、suite、train、purge、max_errors、quiet。
- pbbt.yaml
此文件必须是一个包含以下键的YAML文件:extend、input、output、define、suite、train、purge、max-errors、quiet。
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 是否是包含类型为 T1、T2 等的字段元组。
- ```python
伪类型 isinstance(X, ...):检查 X 是否是键类型为 T1、值类型为 T2 的字典。
- ```python
与 with 子句一起使用以验证嵌套块引发给定类型的异常。
- ```python
验证函数调用 fn(*args, **kwds) 是否引发给定类型的异常。
- ```python
将给定的类注册为测试类型。
- ```python
描述输入或输出记录的字段。
- check
字段值的类型。
- default
如果字段缺失,则默认值。如果未设置,则字段为必需的。
- order
如果设置,允许您覆盖默认字段顺序。
- hint
字段的简短描述。
- ```python
所有输入和输出记录的基类。
Test 装饰器将嵌套的 Input 和 Output 类转换为 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。
项目详情
下载文件
下载适合您平台的应用程序。如果您不确定选择哪个,请了解更多关于安装包的信息。