策略简介
本章我们将使用迄今为止你所学到的关于pytest的所有知识,为Cards项目创建测试策略--软件测试中 "写什么测试 "的部分。
我们将从定义我们的测试套件的目标开始。然后,我们将看看Cards的软件架构是如何影响我们的测试策略的,并受到测试需求的影响。然后,我们可以开始选择和优先考虑哪些功能需要测试。一旦我们知道哪些功能需要测试,我们就可以生个所需的测试案例列表。所有这些有条不紊的计划真的不需要很长时间,并将有助于产生体面的初始测试套件。
确定测试范围
安全
性能
负载
输入验证
卡片项目是为个人或小团队使用的。即便如此,在现实中,上述所有的担忧都适用于这个项目,尤其是随着项目的发展。那么对于一个初始测试套件,我们应该做多少测试呢?这里有合理的开始。
- 测试用户可见功能的行为。
推迟对当前设计的安全、性能和负载测试。目前的设计是将数据库存储在用户的主目录中。当/如果转移到一个有多个用户的共享位置时,这些问题肯定会更加重要。
当Cards是一个单用户应用程序时,输入验证也不那么重要。然而,我也不希望在使用该应用时出现堆栈痕迹,所以我们应该测试古怪的输入,至少在CLI层面。
所有的项目都需要有功能或特性测试。然而,即使仅仅是功能测试,我们也需要决定哪些功能需要测试,以何种优先级测试。然后对于每个功能,我们需要决定测试案例。
使用有条不紊的方法使所有这些变得相当简单。我们将以Cards项目为例,进行所有这些工作。我们将从确定功能的优先次序开始,然后生成测试用例。但首先,让我们看看你的项目的软件架构如何影响你选择的测试策略。
考虑软件架构
软件架构涉及到你的项目的软件是如何组织的,有哪些API可用,接口是什么,代码的复杂性在哪里,模块化等等。在测试方面,我们需要知道我们需要测试系统的多少,以及入口点是什么。
举个简单的例子,假设我们要测试的代码存在于一个模块中,打算在命令行上使用,除了打印输出外,没有任何交互式组件,也没有API。另外,它不是用 Python 写的。那么我们就没有选择了。我们唯一的选择是把它作为一个黑盒子来测试。我们将让我们的测试代码用不同的参数和状态调用它,并观察输出。
如果代码是用 Python 写的,并且是可导入的,我们可以通过调用模块内的函数来测试它的不同部分,那么我们就有了选择。我们仍然可以像以前一样测试它,作为一个黑盒子。但是如果我们想的话,我们也可以单独测试里面的函数。
这个概念的扩展性很好。如果被测试的软件被设计成一个有很多子模块的Python包,我们仍然可以在CLI层面进行测试,或者我们可以放大一点,测试模块,或者我们可以进一步放大,测试模块中的功能。再放大一点,我们有更大的系统,被设计成相互作用的子系统,每个子系统可能有多个包和模块。
所有这些都会在很多方面影响我们的测试策略。
我们应该在哪个层面上进行测试?最上面的用户界面?更底层?子系统?所有级别?
在不同层次进行测试有多容易?UI测试通常是最困难的,但也可能更容易与客户的功能挂钩。单个功能的测试可能更容易实现,但更难与客户需求挂钩。
谁负责不同层次和每个层次的测试?如果你提供的是子系统,你是否只负责这个子系统?是否有其他人在做系统测试?如果是这样,这是个简单的选择:测试你自己的子系统。然而,至少要参与知道在系统层面上的测试内容才好。
让我们把事情简化一下。假设你和你的团队负责整个系统,并且你的软件是分层建立的。你在顶层有一个UI,它的逻辑很薄,调用API层,并调用系统中的其他东西。其余的代码可能是一个巨大的单一文件或精心设计的子系统和模块。
然后,你基本上可以针对API做系统测试,并对用户界面做一些最基本的测试,以确保它正确地调用API。然后,你可以在用户界面层面做一些高级测试,作为系统测试,并将你的测试工作集中在API上。
这种简化的系统就是我们所拥有的Cards。Cards项目分三层实现:(1)住在cli.py的CLI,(2)住在api.py的API,和(3)住在db.py的数据库层。
CLI是在cli.py中实现的。它依赖于两个第三方软件包。Typer 是一个构建 CLI 的工具,而 Rich可以做很多伟大的富文本终端的工作,但我们只是用它来做漂亮的表格。CLI是有意的,尽可能的薄,几乎所有的逻辑都交给了API。
与底层数据库的交互在db.py中处理。它有一个第三方的依赖,TinyDB,它是底层数据库。它也是尽可能的薄。
cli.py 和 db.py 都尽可能地薄,有几个原因。
通过API测试大部分的系统和逻辑。第三方依赖被隔离在一个文件中。
隔离第三方包会带来几个好处。如果由于这些依赖的接口变化而需要改变,这些变化将被隔离在一个文件中。这甚至可能包括用其他东西替换掉依赖关系。例如,如果我们想尝试一个不同的数据库后端,我们可以用db.py作为入口创建一个测试套件,改变数据库,并在db.py中做任何必要的适配器修改。
在Cards中,保持cli.py稀疏的主要原因是允许大部分的测试是针对API的。对于db.py,主要原因是允许对我们对任何底层数据库的期望进行隔离测试。
这与测试策略有什么关系?有几个方面。
因为CLI的逻辑很薄,我们可以通过API来测试大部分的东西。
测试CLI足以验证它调用正确的API入口点,这就足够了。
因为数据库的交互被隔离在db.py中,如果我们觉得有必要,我们可以在那一层增加子系统测试。
即使我们通过API进行测试,我们也希望将测试工作集中在可见的最终用户行为上,而不是迷失在测试实现中。因此,这里有一个可行的卡片测试策略。
测试用户可以访问的功能--在CLI中可见的功能。
通过API测试这些功能,而不是通过CLI。
测试CLI足以验证它与API的连接是否正确。
这似乎是一个不错的开始。我们可以暂时不对数据库进行单独的测试。
评估要测试的功能
在我们创建要测试的案例之前,我们首先需要评估要测试哪些功能。当你有很多功能和特性需要测试时,你必须确定开发测试的优先次序。至少要有一个大致的顺序概念。
我一般根据以下因素来确定要测试的功能的优先次序。
最近的--新的功能,新的代码区域,最近被修复、重构或其他方式修改的新功能
核心--你的产品的独特销售主张(USPs)。为了使产品有用,必须继续工作的基本功能。
风险--应用程序中构成更多风险的区域,如对客户重要但开发团队不经常使用的区域,或使用你不太信任的第三方代码的部分。
有问题的--经常发生故障的功能,或者经常收到针对它的缺陷报告
专业性--只有少数人了解的功能或算法
Cards有一个有限的功能集。以下是终端用户可见的功能。
$ cards --help
Usage: cards [OPTIONS] COMMAND [ARGS]...
Cards is a small command line task tracking application.
Options:
--help Show this message and exit.
Commands:
add Add a card to db.
config List the path to the Cards db.
count Return number of cards in db.
delete Remove card in db with given id.
finish Set a card state to 'done'.
list List cards in db.
start Set a card state to 'in prog'.
update Modify a card in db with given id with new info.
version Return version of cards application
因为我们把卡片项目当作一个需要测试的遗留系统,这些标准中有些比其他的更有帮助。
- 核心
添加、计数、删除、结束、列表、开始和更新都是核心功能。
配置和版本似乎不太重要。
- 风险
第三方软件包是用于CLI的Typer和用于数据库的TinyDB。围绕我们对这些组件的使用进行一些重点测试将是审慎的。我们对Typer的使用将在我们测试CLI的时候进行测试。我们对TinyDB的使用将在所有其他测试中进行测试,由于db.py隔离了我们与TinyDB的交互,如果有必要,我们可以在该层创建测试。
而且因为功能集很小,我们实际上会测试所有的Cards项目。然而,即使是对功能的这种快速分析也有助于我们提出我们的策略。
- 彻底测试核心功能。
- 用至少一个测试案例来测试非核心功能。
- 隔离地测试CLI。
现在让我们按照这个计划,生成测试用例。
创建测试用例
与确定测试策略的目标和范围一样,如果你采取有条不紊的方法,生成测试用例也会更容易。对于生成一组初始的测试用例,这些标准将是有帮助的。
- 从一个非琐碎的、"快乐路径 "的测试案例开始。
- 然后看一下代表以下内容的测试用例
- 有趣的输入集。
- 有趣的起始状态。
- 有趣的结束状态,或
- 所有可能的错误状态。
这些测试用例中有些会重叠。如果一个测试用例满足了上述标准中的一个以上,那也没关系。让我们通过一些卡片的功能来了解它。
对于count,一个快乐的路径测试案例可能是,"空的数据库,计数返回0"。有三个元素的数据库,count返回3。有意思的输入集合是什么?没有。count不接受任何参数。
有意思的起始状态是什么?我想说。
- 空的数据库
- 一个项目
- 多于一个项目
有趣的结束状态?没有。计数并不修改数据库。
错误状态?也没有,我想不出来。
所以,对于count,我们有这些测试案例。
- 空的数据库计数
- 一个项目的计数
- 一个以上项目的计数
因为最后一个测试满足了我们的快乐路径测试用例,所以我们可以把它留在这三个。
实际上,快乐路径经常被其他标准产生的其他测试用例之一所满足。那么,为什么我们要特别想到一个非琐碎的、幸福路径的测试用例呢?我们应该出于几个原因。首先,如果我们很匆忙,我们可以只创建非琐碎的,快乐路径的测试用例,为我们测试的每个功能创建一个。这不是一个彻底的测试套件。然而,在测试系统的很大一部分时,它是相当有效的,而且工作量很小。很多时候,我从这里开始,在开发过程中建立了更多的测试案例。
从快乐路径开始的第二个原因是,它使思考其他标准变得非常容易。如果你从所有可能出错的地方开始,你可能会忘记测试那些正确的案例。
$ cards add --help
Usage: cards add [OPTIONS] SUMMARY...
Add a card to db.
Arguments:
SUMMARY... [required]
Options:
-o, --owner TEXT
--help Show this message and exit.
一个非琐碎的、快乐的路径案例可能是向非空的数据库添加卡片。摘要是必需的,而传入的所有者是可选的。所以我们应该同时测试摘要和测试摘要加所有者。如果我们没有传入摘要呢?这将属于错误条件。就像所有者的文本是空的一样。如果我们添加了卡片,而这个卡片的摘要和所有者与一个已经存在的卡片相匹配,怎么办?这应该被允许还是拒绝作为错误状态?这个问题强调了在开发过程中编写测试的一些价值,或者至少在行为和API太远,不能轻易改变而不影响现有用户之前。行为应该是什么?卡片应用程序允许重复。但无论哪种答案都是合理的。不过,我们还是应该对其进行测试。
下面是我们对添加的测试案例。
- 添加到空数据库,有摘要
- 添加到非空的数据库,有摘要
- 添加有摘要和所有者设置的卡片
- 添加一张缺失摘要的卡片
- 添加重复的卡片
现在来看删除:
$ cards delete --help
Usage: cards delete [OPTIONS] CARD_ID
Remove card in db with given id.
Arguments:
CARD_ID [required]
Options:
--help Show this message and exit.
对于一个非琐碎的、快乐的路径测试案例,让我们从一个以上的卡片开始并删除一个。唯一的输入是卡片的ID。有趣的选项可以是一个存在的ID和一个不存在的ID。有趣的起始状态可以是空的,有我们要删除的卡的非空的,以及没有卡的非空的。结束状态终于作为有用的标准出现了,因为删除的动作可以使我们从非空到空。对于错误条件,我认为不存在的卡的删除是真正的唯一条件。
下面是我们对删除的测试案例。
- 有多个卡的数据库中删除一个
- 删除最后一张卡
- 删除不存在的卡片
到目前为止,我们有添加、删除和计数的测试用例。让我们一起看一下开始和结束。因为这些函数改变的是单个卡片的状态,所以看卡片的状态比看数据库的状态更有意思。卡片的可能状态是 "todo","in progressive",和 "done"。所有这些看起来都很有趣。像删除一样,你传入一个你想开始或结束的卡片的ID。我们应该测试现有的ID和不存在的ID。这给我们带来了这些新的测试案例。
- 从 "todo"、"in progressive "和 "done "状态开始
- 开始一个无效的ID
- 从 "待办事项"、"正在进行 "和 "已完成 "状态中结束
- 完成一个无效的ID
我们还剩下更新、列表、配置和版本。如果你想练习这种技术,我鼓励你在继续阅读之前,先自己尝试一下,看看你的列表是否与我的不同。
下面是我为剩下的功能想出的办法。
- 更新卡片的主人
- 更新卡片的摘要
- 同时更新卡的主人和摘要
- 更新一个不存在的卡片
- 从一个空的数据库中列表
- 从一个非空的数据库中获取列表
- config返回正确的数据库路径
- 版本返回正确的版本
这是一套相当不错的测试案例,可以从这里开始。注意,这些并不是详细的测试描述。当我们实现这些测试案例时,可能会出现关于正确行为的问题。这很好。这些问题往往会引发沟通、设计的清晰度和API的完整性。他们也可以帮助确定文档中的漏洞。
最初的测试用例列表也是不完整的。当我们通过测试编写工作时,我们不可避免地会想出更多的测试用例。如果你和团队一起工作,这也是获得团队反馈的好时机。在这个阶段,测试用例的非正式性质允许对行为进行讨论,而不会迷失在代码的细节中。
可能仍然有一些缺失的信息,需要完成测试的编写。例如,如果预期有一个异常,那么它将是什么具体的异常?缺少信息是可以的,特别是在被测试的代码的API还没有最终确定的情况下。如果你在这个阶段与团队中的领域专家讨论测试用例列表,当你在编写测试时遇到具体问题时,他们会准备好回答。
在检查测试功能和生成初始测试用例列表的计划工作之后,你可能想马上开始写测试。然而,暂停一下并写下我们到目前为止所做的工作是个好主意。
编写测试策略
在本章的早些时候,我们决定大部分的测试将通过API进行。CLI将被测试到足以确保它正确地调用API。我们将暂时放弃对数据库的测试。如果我们想有一套对迁移到新的数据库包有用的测试,我们可以以后再进行。
即使对我们的测试策略进行了快速的总结,一旦我们进入了测试,就很容易忘记细节。因此,我非常喜欢把测试策略写下来,以便以后可以参考。如果你和团队一起工作,即使只有你们两个人,写下它也是特别重要的。
下面是目前的卡片测试策略。
测试那些可以通过终端用户界面,即CLI访问的行为和功能。
尽可能地通过API测试这些功能。
测试CLI,以验证API对所有功能的正确调用。
彻底测试以下核心功能:添加、计数、删除、结束、列表、开始和更新。
包括对配置和版本的粗略测试。
用针对db.py的子系统测试来测试我们对TinyDB的使用。
另外,我们不会在这里列出,但如果你在文档或内部维基或其他地方与团队分享策略,一定要包括初始测试案例列表。
我们知道随着测试的进行,我们可能会扩展这个初始策略。每当我们觉得需要改变时,这就是与团队讨论改变的好时机。
花时间写下要测试的功能,最初的测试用例列表,以及测试策略是前期的时间,但当我们快速实施测试时,它很快就会得到回报,这是下一步。