软件质量保障
所寫即所思|一个阿里质量人对测试的所感所悟。
Tcases能做什么?
Tcases 是一个用于设计测试用例的工具。无论何种系统,无论何种测试类型(单元测试、系统测试等),都可以使用 Tcases 来设计测试。你也可以定义被测系统所需的覆盖级别,Tcases 将生成一组满足你需求的最小测试用例集。
Tcases主要是一个用于黑盒测试设计的工具。对于此类测试,“覆盖率”的概念与诸如行覆盖、分支覆盖等结构化测试准则不同。Tcases 是以系统输入空间的覆盖率为导向的。
系统的“Input Space(输入空间)”是什么?最简单的理解方法是:应用系统的所有可能输入值的集合。这话说起来很简单,但做起来却很困难!对于除最简单的系统之外的所有系统,这个集合都非常庞大,甚至可能是无限的。你永远不可能设计出所有测试用例并运行它们。相反,你必须从输入空间的一小部分样本中选择测试用例。但是该如何选择呢?如果你的样本太大,你可能还没完成就耗尽了时间。但如果你的样本太小——将遗漏很多缺陷。
这是一个测试设计问题:在有限的测试资源下,如何最小化缺陷风险?Tcases 是解决这个问题的工具。Tcases 以简洁全面的形式定义系统的输入空间。然后,Tcases 允许用户通过指定所需的覆盖级别来控制样本子集中的测试用例数量。用户可以从基本的覆盖级别开始,Tcases 将生成一组测试用例,涵盖输入空间中的所有重要元素。然后,通过在高风险模块选择性地增加覆盖场景来改进测试。
Tcases 是如何工作的?
首先,用户需要创建一个系统输入定义文件,该文件定义了用户的系统为一组函数。对于每个系统函数,系统输入定义文件定义了描述函数输入空间的变量。
然后,用户可以创建一个生成器定义。这是另一份文档,定义了每个系统函数所需的覆盖范围。生成器定义是可选的。用户可以跳过这一步骤,仍然可以达到基本的覆盖程度。
最后,用户可以运行Tcases。Tcases是Java语言实现的,用户可以在命令行或使用IDE运行它。Tcases支持使用shell脚本来运行。用户还可以使用Maven和Tcases Maven插件来运行Tcases。使用输入定义和生成器定义,Tcases会生成一个系统测试定义。系统测试定义是一个文档,其中列出了每个系统函数的一组测试用例,这些测试用例提供了指定的覆盖级别。每个测试用例都为每个函数输入变量定义了一个特定的值。Tcases不仅生成定义成功测试用例的有效输入值,还生成用于验证预期错误处理的无效测试用例。
当然,系统测试定义并不是可以直接执行的。但是它遵循一个明确的模式,这意味着你可以使用各种转换工具将它转换为适合测试系统的形式。例如,Tcases自带一个转换器,可以将系统测试定义转换为一个Java源代码模板,用于JUnit或TestNG测试类。你也可以自动将系统测试定义转换为简单的HTML报告。
开始
安装Tcases Maven插件
要获取Tcases Maven插件的依赖信息。
<dependency>
<groupId>org.cornutum.tcases</groupId>
<artifactId>tcases-maven-plugin</artifactId>
<version>4.0.5</version>
<type>maven-plugin</type>
</dependency>
安装Tcases release版本
要获取命令行版本的Tcases,请按照以下步骤从Maven中央存储库下载Tcases二进制安装包。
-
访问 tcases-shell 的主存储库页面。
-
找到最新版本的条目并点击“Browse”。
-
要下载分发文件压缩包,请单击“tcases-shell-${version}.zip”。如果用户更喜欢压缩的TAR文件,请单击“tcases-shell-${version}.tar.gz”。
将文件解压到用户喜欢的任何目录中——现在这就是用户的“Tcases 主目录”。解压缩文件将创建一个名为“Tcases 发行目录”的子目录——以 tcases- m.n.r 的形式的子目录——其中包含 Tcases 此版本的所有文件。发行目录包含以下子目录。
-
bin:可执行的 shell 脚本,用于运行 Tcases。
-
docs :用户指南、示例和Java文档
-
lib:运行Tcases所需的所有JAR文件。
接下来再做一步就可以开始使用了:将 bin 子目录的路径添加到用户的系统中的 PATH 环境变量中。
JSON还是XML ?
所有Tcases文档的首选格式是JSON,它能够表达Tcases的所有功能,并且在本指南的所有示例中都使用了这种格式。
但是,原始版本的 Tcases 将所有文档都使用 XML 格式,并且对于旧文档仍然支持 XML 格式。有关如何使用 Tcases 与 XML 的详细信息,用户可以在本指南的原始版本中找到,其中包括如何将现有的 XML 项目转换为 JSON。
命令行运行
你可以直接使用命令行运行Tcases。你可以使用bash命令行或UNIX命令行。如果你使用的是Windows命令行,你可以使用`tcases.bat`命令文件,与上述命令的语法完全相同。
例如,用户可以快速检查一下,使用以下命令运行Tcases。
cd ${tcases-release-dir}
cd docs/examples/json
tcases < find-Input.json
理解Tcases测试结果
当你运行Tcases时会发生什么?Tcases会读取一个系统输入定义文件,该文件定义了要测试的系统功能的“输入空间”。基于此,Tcases会生成另一个文件,称为系统测试定义文件,该文件描述了一组测试用例。
尝试在示例系统输入定义上运行Tcases。下面的命令将为 find 命令示例生成测试用例,该示例将在本指南的后续部分中详细解释。
cd ${tcases-release-dir}
cd docs/examples/json
tcases < find-Input.json
最终生成的系统测试定义将输出到标准输出。以下是示例:对于 find 函数,列出了一系列测试用例定义,每个定义都为函数的所有输入变量赋了值。
{
"system": "Examples",
"find": {
"testCases": [
{
"id": 0,
"name": "pattern='empty'",
"has": {
"properties": "fileExists,fileName,patternEmpty"
},
"arg": {
"pattern": {
"value": "",
"source": "empty"
},
"fileName": {
"value": "defined"
}
},
"env": {
"file.exists": {
"value": true
},
"file.contents.linesLongerThanPattern": {
"NA": true
},
"file.contents.patternMatches": {
"NA": true
},
"file.contents.patternsInLine": {
"NA": true
}
}
},
...
]
}
}
建模输入空间
Tcases根据用户系统输入创建测试。那么,用户是如何做到的呢?
系统输入定义是一份文件,用于建模被测系统(SUT)的“输入空间”,我们称其为“建模”系统输入,因为其并没有逐一列出所有可能的输入值。相反,系统输入定义列出了所有影响系统结果的重要系统输入方面。可以将其视为描述系统“输入空间”中“变化维度”的文件。某些变化维度显而易见。如果用户正在测试“add”函数,用户知道至少有两个变化维度——两个不同的被加数。但是要找出所有关键的维度,用户可能需要进行更深入的分析。
例如,考虑如何测试一个简单的“列出文件”命令,比如 ls 命令。显然,其中一个维度的变化是给出的文件名的数量。ls 应该能够处理的不仅仅是一个文件名,而是一个包含许多文件名的列表。如果未给出任何文件名, ls 预期会有完全不同的结果。但是,每个文件名本身呢?ls 将根据文件名是否标识一个简单文件或目录而产生不同的结果。因此,每个文件名所标识的文件类型是另一个维度的变化。但这还不是全部!一些文件名可能标识实际存在的文件,但其他文件名可能是不存在的文件的假名称,这种差异对 ls 预期的行为有重大影响。因此,这里还有一个与文件名本身无关,而是与 ls 运行的环境状态有关的维度变化。
你可以看到,对输入空间进行建模需要对被测系统(SUT)进行仔细考虑。这是任何工具都无法为用户完成的工作。但是,Tcases为用户提供了一种捕捉这些知识并将其转化为有效测试用例的方法。
find 命令
要了解如何使用Tcases进行输入建模,最好的方法就是看一个实际的例子。我们将通过一个示例来解释如何使用Tcases测试“ find ”命令。
下面是 find ,用户会用哪些测试用例来测试?
用法:find 模式文件。
找到文本文件中一个或多个指定模式的实例。
文件中包含该模式的所有行都将被写入标准输出。包含该模式的一行无论该模式在该行中出现多少次,都只会被写入一次。
该模式是任何长度不超过文件中每一行最大长度的字符序列。要在模式中包含空格,必须将整个模式用引号括起来( " )。要在模式中包含引号,必须连续使用两个引号( "" )。
定义系统功能
系统输入定义描述特定的待测系统,文档的根对象如下所示:
{
"system": "Examples",
...
}
通常,被测试的系统单元具有一个或多个操作或“函数”,因此系统输入定义包含每个函数的函数定义对象。对于我们的例子,我们将为一个名为“Examples”的系统构建一个输入定义,该系统只有一个“find”函数。
{
"system": "Examples",
"find": {
...
}
}
显然,“系统”或“功能”的定义完全取决于你所测试的内容。如果你的“系统”是一个Java类,那么你的“功能”可能是它的方法。如果你的“系统”是一个应用程序,那么你的“功能”可能是用例。如果你的“系统”是一个网站,那么你的“功能”可能是页面。无论如何,输入建模的过程完全相同。
定义输入变量
对于每个需要测试的功能,用户需要定义其输入空间的所有变化维度。为了简化,Tcases 将每个这样的维度称为“变量”,并将每个基本变量表示为一个变量定义对象。
此外,函数定义对象会根据数据类型对输入变量进行分组。` find `命令有两种不同的输入变量类型。有直接的输入参数,如文件名,其输入类型为 ` arg `。还有其他因素,如文件的状态,作为间接的“环境”输入变量,其输入类型为 ` env `。
{
"system": "Examples",
"find": {
"arg": {
"pattern": {
...
},
"fileName": {
...
}
},
"env": {
...
}
}
}
其实,用于分组输入变量的输入类型名称只是一个标签,其值可以是任意的。用户可以定义任意数量的不同输入类型组。
定义输入值
对于Tcases来说,要创建一个测试用例,必须为所有输入变量赋值。它如何做到这一点呢?因为我们使用一个或多个值定义对象来描述每个输入变量的所有可能值。
默认情况下,一个值的定义描述了一个有效的值,即预期函数可以接受的值。但我们可以使用可选的 failure 属性来标识一个无效的值,并期望该值会导致函数产生某种失败的响应。Tcases使用这些输入值生成两种类型的测试用例——“成功”用例,其中所有变量都只使用有效的值;以及“失败”用例,其中恰好有一个变量使用了 failure 值。
例如,我们可以为 find 的 fileName 参数定义两个可能的值。
{
"system": "Examples",
"find": {
"arg": {
"fileName": {
"values": {
"defined": {
},
"missing": {
"failure": true
}
}
}
...
}
}
}
你对文件名的唯一选择就是“定义”或“缺失”?好问题!这里发生的事情是输入空间建模的一个重要部分。在这里列出所有可能的文件名作为值是愚蠢的。为什么?因为这无关紧要。至少对于这个特定的函数,文件名的字母和格式对函数的行为没有影响。相反,需要的是一个能够描述这个变量的值域模型,该模型可以识别对测试有意义的各类值。这是一种众所周知的测试设计技术,称为等价类划分。使用每个值定义来识别一个类的值。根据定义,该类中的所有特定值都是测试等价的。我们不需要测试它们全部——其中任何一个就可以了。
对于 fileName 变量,我们决定文件名本身的意义在于它是否存在,并选择将这两种情况分别定义为“定义”和“缺失”。但用户用于标识每个值类的名称完全由用户决定——这是用户为描述用户的测试而设计的输入模型的一部分,并且它出现在Tcases生成的测试用例定义中,以指导用户的测试实现。
定义Value Schemas
我们可以通过使用一个Value Schemas 来明确地定义值所代表的类型。
例如,对于 pattern 变量,一类有趣的值是包含多个字符的未引用字符串。当然,未引用的模式字符串不能包含任何空格或引号。用户可以使用以下模式关键词来定义 unquotedMany 值类,这些关键词描述至少包含两个字符且符合一定正则表达式的字符串。
...
"pattern": {
"values": {
"unquotedMany": {
"type": "string",
"pattern": "^[^\\s\"]+$",
"minLength": 2
},
...
}
}
...
当 ` pattern ` 是一个空字符串时会发生什么?这也是用户需要在测试用例中添加的另一个重要值。以下是如何使用 schema 关键字定义这个 ` empty ` 值的方法。
...
"pattern": {
"values": {
"empty": {
"const": ""
},
...
}
}
...
要定义一个值模式,可以使用标准JSON Schema Validation词汇表的一个子集。
为什么要定义值模式?区别在于Tcases生成的测试用例定义。如果用户的输入模型仅使用名称来定义值类,那么任何使用此值定义的测试用例都只能显示该名称。要将此测试用例转换为可执行形式,用户必须用属于该类的适当的实际值替换它。然而,如果用户使用模式来定义值类,Tcases 将自动为用户生成该值类的随机成员。而且,每个使用此值定义的测试用例都可以生成不同的实际值。这不仅可以节省用户的一些工作量,还可以通过添加更多“多余的多样性”来改进用户的测试用例。
定义Variable Schemas
你也可以在变量定义中添加结构化数据的关键字。
为什么这有用?首先,你可以使用Variable Schemas来为所有值模式定义默认值。例如,你可以指定 pattern 变量的默认类型为“字符串”。如果是这样,你就不需要为每个单独的值定义添加“type”关键字。
...
"pattern": {
"type": "string",
"values": {
"empty": {
"const": ""
},
"unquotedSingle": {
"pattern": "^[^\\s\"]$"
},
"unquotedMany": {
"pattern": "^[^\\s\"]+$",
"minLength": 2
},
"quoted": {
"pattern": "^\"[^\\s\"]+\"$",
"minLength": 2
},
...
}
}
...
需要注意的是,在一个值定义中的模式关键字会覆盖在变量模式中给出的任何默认值。值定义可以为“type”或其他模式关键字定义完全不同的值,甚至可以不定义任何模式关键字。
但在某些情况下,你只需要一个可变的模式。Tcases可以使用它自动生成所有值的定义。例如,假设用户的输入模型包含一个电子邮件地址的变量。用户可以仅使用一个模式来定义此类变量,如下所示:
{
...
"user-email-address": {
"format": "email",
"minLength": 8,
"maxLength": 32
},
...
}
使用此模式,Tcases将生成一个有效的输入模型,用于 user-email-address 变量,包括有效和无效值类,如下所示。然后,Tcases将使用该有效输入模型生成测试用例。
{
...
"user-email-address": {
"values": {
"minimumLength": {
"format": "email",
"minLength": 8,
"maxLength": 8
},
"maximumLength": {
"format": "email",
"minLength": 32,
"maxLength": 32
},
"tooShort": {
"failure": true,
"format": "email",
"maxLength": 7
},
"tooLong": {
"failure": true,
"format": "email",
"minLength": 33
},
"wrongFormat": {
"failure": true,
"format": "date-time"
}
}
}
...
}
提示:如果用户想查看 Tcases 使用的有效输入模型,请增加带有 -I 运行 tcases ,如下。
# Instead of creating test cases, just print the effective input model for the "find" project to the /tmp/test directory
tcases -I -o /tmp/test find
定义变量集
通常会发现一个逻辑输入实际上具有很多不同的特性,每一种特性都会在输入空间中产生不同的“变化维度”。例如,考虑由 find 命令搜索的文件。它存在吗?也许存在,也许不存在——这是测试必须覆盖的其中一个维度。那么它的内容呢?当然,你希望测试文件中包含匹配模式的行的情况,以及不含匹配的情况。所以,这是另一个维度的变化。规格说明指出,每个匹配的行将被精确地打印一次,即使它包含多个匹配。难道你不想测试一个包含不同匹配数量的行的文件吗?好吧,还有另一个维度的变化。一个文件——如此多的维度!
你可以将这种复杂的输入模型为一个“变量集”,使用 members 属性。使用变量集,你可以将一个单一的逻辑输入描述为一组多个变量定义的集合。变量集甚至可以包含另一个变量集,从而创建一个逻辑输入层次结构,该结构可以扩展到任意多个层次。
例如,对 find 命令的单个 file 输入可以使用以下变量集定义来建模。
{
"system": "Examples",
"find": {
...
"env": {
"file": {
"members": {
"exists": {
"values": {
...
}
},
"contents": {
"members": {
"linesLongerThanPattern": {
"values": {
...
}
},
"patternMatches": {
"values": {
...
}
},
"patternsInLine": {
"values": {
...
}
}
}
}
}
}
}
}
}
这种层次结构不就是四个变量定义吗?比如下面这样的定义:[INPUT] [OUTPUT] [CONSTANT] [PROCEDURE]
{
"system": "Examples",
"find": {
...
"env": {
"file.exists": {
"values": {
...
}
},
"file.contents.linesLongerThanPattern": {
"values": {
...
}
},
"file.contents.patternMatches": {
"values": {
...
}
},
"file.contents.patternsInLine": {
"values": {
...
}
}
}
}
}
是的,在生成测试用例时,Tcases就是按照这种方式处理的。但是,将复杂的输入定义为变量集可以使输入模型更易于创建、阅读和维护。此外,它还允许用户一次对整个变量树应用约束,这一点将在下一节中详细介绍。
定义约束:Properties 与 Conditions
我们已经学会了如何为每个测试函数的输入变量(包括具有多个维度的复杂输入变量)定义值选择。这足以让我们完成类似于以下所示的 find 命令的系统输入定义。
{
"system": "Examples",
"find": {
"arg": {
"pattern": {
"type": "string",
"maxLength": 16,
"values": {
"empty": {"const": ""},
"unquotedSingle": {"pattern": "^[^\\s\"]$"},
"unquotedMany": {"pattern": "^[^\\s\"]+$"},
"quoted": {"pattern": "^\"[^\\s\"]+\"$"},
"quotedEmpty": {"const": "\"\""},
"quotedBlanks": {"pattern": "^\"[^\\s\"]*( +[^\\s\"]*)+\"$"},
"quotedQuotes": {"pattern": "^\"[^\\s\"]*(\"{2}[^\\s\"]*)+\"$"}
}
},
"fileName": {
"type": "string",
"values": {
"defined": {},
"missing": {"failure": true}
}
}
},
"env": {
"file": {
"members": {
"exists": {
"type": "boolean",
"values": {
"true": {},
"false": {"failure": true}
}
},
"contents": {
"members": {
"linesLongerThanPattern": {
"type": "integer",
"format": "int32",
"values": {
"1": {},
"many": {"minimum": 2, "maximum": 32},
"0": {"failure": true}
}
},
"patternMatches": {
"type": "integer",
"format": "int32",
"values": {
"0": {},
"1": {},
"many": {"minimum": 2, "maximum": 16}
}
},
"patternsInLine": {
"type": "integer",
"format": "int32",
"values": {
"1": {},
"many": {"minimum": 2, "maximum": 4}
}
}
}
}
}
}
}
}
}
当我们使用这个输入文档运行Tcases时,我们会得到如下所示的测试用例定义列表:
{
"system": "Examples",
"find": {
"testCases": [
{
"id": 0,
"name": "pattern='empty'",
"arg": {
"pattern": {"value": "", "source": "empty"},
"fileName": {"value": "defined"}
},
"env": {
"file.exists": {"value": true},
"file.contents.linesLongerThanPattern": {"value": 1},
"file.contents.patternMatches": {"value": 0},
"file.contents.patternsInLine": {"value": 1}
}
},
{
"id": 1,
"name": "pattern='unquotedSingle'",
"arg": {
"pattern": {"value": "}", "source": "unquotedSingle"},
"fileName": {"value": "defined"}
},
"env": {
"file.exists": {"value": true},
"file.contents.linesLongerThanPattern": {"value": 26, "source": "many"},
"file.contents.patternMatches": {"value": 1},
"file.contents.patternsInLine": {"value": 4, "source": "many"}
}
},
...
]
}
}
但请稍等一下——这里有些地方看起来不对劲。仔细看看上面的测试用例 0。它告诉我们尝试使用一个不含测试模式实例的文件。同时,该文件还应该包含一个测试模式匹配项的行。不仅如此,要匹配的模式是空字符串——null pattern,这种模式会匹配同一行多次!简而言之,这种测试用例似乎有点不可能实现。
看看上面的第一个测试用例。它同样存在问题。对于这个测试用例,文件中应该恰好只有一条匹配的记录。而且,该记录还必须包含被匹配四次的行。这怎么可能呢?
这里发生了什么?显然,这些变量定义中描述的“变化维度”并非完全独立。相反,这些变量之间存在相互关系,限制了哪些组合的值是可行的。我们需要一种方法来定义这些关系,以便将不可行的组合从测试用例中排除。
使用 Tcases,用户可以使用属性和条件来实现这一点。下文将详细解释如何实现这一点,并提供一些避免约束可能引入的某些问题的技巧。
value Properties
一个值定义可以声明一个列表,其中包含一个或多个“属性”,用于描述该值。例如:
...
"patternMatches": {
"type": "integer",
"format": "int32",
"values": {
"0": {
},
"1": {
"properties": ["match"]
},
"many": {
"properties": ["match", "matchMany"]
}
}
}
...
一个属性只是你为自己发明的一个名称,用于标识这个值的重要特性。这个概念是,当将这个值包含在测试用例中时,它会将其所有属性都贡献出来——这些属性现在成为测试用例本身的属性。这使得我们能够为测试用例定义“条件”,即该测试用例必须具有哪些属性,才能包含特定的值。
例如,上面对变量 file.contents.patternMatches 的定义表示,当我们为一个测试用例选择值 1 时,该测试用例会获得一个名为 match 的属性。但如果我们选择值 many ,该测试用例会获得两个属性—— match 和matchMany 。如果我们选择值 0 ,不会为测试用例添加任何新属性。请注意,这些特定值的名称与属性之间的对应并非完全偶然——它有助于我们理解这些元素的含义——但它没有特殊的意义。如果我们愿意,可以给其中任何一个不同的名称。
但请注意,所有这些都只适用于有效的值定义,而不适用于指定 ` "failure": true ` 的失败值定义。为什么?因为失败值是不同的!
value conditions
我们可以通过定义一个名为 when 的属性来定义将某个值包含在测试用例所需的条件。该属性定义了一个“条件”对象。添加一个条件对象意味着“要将此值包含在测试用例中,测试用例的属性必须满足此条件”。
例如,考虑 file.contents.patternsInLine 变量的取值条件。
...
"patternsInLine": {
"type": "integer",
"format": "int32",
"values": {
"1": {
"when": {"allOf": [ {"hasAll": ["matchable", "match"]}, {"hasNone": ["patternEmpty"]}]}
},
"many": {
"when": {"allOf": [ {"hasAll": ["matchable", "matchMany"]}, {"hasNone": ["patternEmpty"]}]},
"minimum": 2,
"maximum": 4
}
}
}
...
这定义了 file.contents.patternsInLine 变量的取值约束。我们希望有一个测试用例,其中该变量的值为 1 ,即某行恰好包含一个符合模式的子字符串。如果是这样,那么这必须是一个文件中至少包含一条匹配行的测试用例。换句话说,该测试用例必须具有 match 属性,只有当 file.contents.patternMatches 的值不为零时才会出现该属性。
我们还需要一个测试用例,其中 file.contents.patternsInLine 的值为 many 。对于这个值,所需的条件基本上与前一个测试用例相同,但不能是文件中仅包含一个模式匹配实例的测试用例。因此,测试用例必须具有以下所示的 matchMany 属性。
...
"patternMatches": {
"type": "integer",
"format": "int32",
"values": {
"0": {
},
"1": {
"properties": ["match"]
},
"many": {
"minimum": 2,
"maximum": 16,
"properties": ["match", "matchMany"]
}
}
},
...
此外,这个测试用例必须包含至少有一行比模式长的文件。否则,不可能找到匹配项。换句话说,测试用例必须具有 matchable 属性,只有当 file.contents.linesLongerThanPattern 的值不为零时才会出现,如下所示。
...
"linesLongerThanPattern": {
"type": "integer",
"format": "int32",
"values": {
"1": {
"properties": ["matchable"]
},
"many": {
"minimum": 2,
"maximum": 32,
"properties": ["matchable"]
},
"0": {
"failure": true
}
}
},
...
此外,这个测试用例必须包含非空的模式。否则,在任何一行中匹配的次数就无关紧要了。换句话说,测试用例必须不具有 patternEmpty 属性,因为 pattern 变量的值是空字符串。但是这种输入组合真的不可能吗?严格来说,不是。但这显然没有多大用处。这展示了聪明的测试人员如何使用属性和条件:引导测试用例向更有效的组合方向发展,远离那些对缺陷排查贡献不大的组合。
值得注意的是,有些值是没有条件限制的。测试用例可以将这些值与其他任何值组合使用。这是件好事。我们希望在不消除任何可行测试用例的情况下,对函数的输入空间进行建模。否则,我们的测试将存在一个盲点,可能导致缺陷未被发现而溜过去。经验法则:谨慎使用条件,仅在必要时使用,以避免产生不可行或低效的测试用例。
不同?是的,因为“失败值定义”(即那些指定 "failure": true 的)无法定义属性。
如果你仔细思考,就会发现这是有根本原因的。假设你声明某个值=V定义了一个属性=P。你为什么要这样做呢?其实只有一个原因:这样其他值=X就可以要求与V(或更准确地说,与任何定义了P的值)组合。但如果V声明了失败值,那就没有意义了。如果其他值X是有效的,它就不能要求与失败值组合——否则,X就永远不可能出现在成功情况下。如果X本身就是一个失败值,它也不能要求与另一个失败值组合——在失败情况下至多只能出现一个失败值。
但请注意,失败值也可以定义条件。换句话说,它可以与其他变量的特定值结合使用。从这个方向出发,你可以控制失败情况下使用的其他值。例如, find 命令在文件中没有足够长的行来匹配模式时会识别错误。换句话说,对于变量 file.contents.linesLongerThanPattern ,值 0 是一个失败值。但在这种情况下,模式不应该仅仅只有1个字符。通过适当定义属性和条件,我们可以确保这种组合正确形成。
...
"pattern": {
"values": {
...
"unquotedMany": {
"pattern": "^[^\\s\"]+$",
"minLength": 2,
"maxLength": 16,
"properties": ["patternMany"]
},
"quoted": {
"pattern": "^\"[^\\s\"]+\"$",
"minLength": 2,
"maxLength": 16,
"properties": ["patternMany"]
},
...
}
},
...
"file": {
"members": {
...
"contents": {
"members": {
"linesLongerThanPattern": {
"values": {
...
"0": {
"when": { "hasAll": ["patternMany"]},
"failure": true
}
}
},
...
}
}
}
}
...
Condition expressions
` when ` 属性的值是一个条件表达式对象。条件表达式是一个具有单个属性的 JSON 对象,该属性必须是下列值之一。
-
"hasAll"
-
-
包含一个值属性名称的列表。
-
如果测试用例包含了列出的所有特性,则满足。
-
-
"hasAny"
-
-
包含一个值属性名称的列表。
-
如果测试用例至少具有列表中列出的其中一项属性,则认为测试用例是满足的。
-
-
"hasNone"
-
-
包含一个值属性名称的列表。
-
相当于 ` "not": { "hasAny": [...]}`
-
-
"allOf"
-
-
逻辑“与”表达式
-
包含一个条件表达式对象的列表。
-
-
"anyOf"
-
-
逻辑“或”表达式
-
包含一个条件表达式对象的列表。
-
-
"not"
-
-
逻辑否定表达式
-
包含一个单一的条件表达式对象。
-
-
"lessThan"
-
-
基数条件
-
当给定的条件 property 发生的次数少于给定的条件 max 时满足。
-
-
"notLessThan"
-
-
基数条件
-
当给定的条件 property 满足或超过给定的条件 min 的次数时满足。
-
-
"moreThan"
-
-
基数条件
-
当给出的条件 property 出现的次数多于给出的条件 min 的次数时满足。
-
-
"notMoreThan"
-
-
基数条件
-
当给定的条件 property 发生的次数小于或等于给定的 max 次时满足。
-
-
"between"
-
-
基数条件
-
当出现的 property 事件同时满足大于等于给定的 min 和小于等于给定的 max 条件时,用户会感到满意。如果用户想要指定严格大于或小于的关系,请使用 exclusiveMin 或 exclusiveMax 属性代替。
-
-
"equals"
-
-
基数条件
-
当给出的条件 property 恰好发生 count 次时满足。
-
Variable conditions
你可能会发现,在某些条件下,一个输入变量变得无关紧要。无论你选择哪个值—— 它们都不会影响函数的行为。很容易模拟这种情况——只需在变量定义本身上定义一个条件即可。
例如,当我们测试 find 命令时,我们希望尝试定义在 file.contents.patternMatches 变量的每个维度上的所有值。但是,如果文件中没有足够长的行来匹配模式,那么它包含多少匹配项就毫无意义。在这种情况下, file.contents.patternMatches 变量就无关紧要了。同样,当我们测试一个完全不含匹配项的文件时, file.contents.patternsInLine 变量就没有意义了。我们可以通过向这些变量添加 when 条件来捕捉输入空间的这些事实,如下所示。请注意,这些变量条件如何使我们能够更简单或不必要地定义单个值的条件。
...
"patternMatches": {
"when": { "hasAll": ["matchable"]},
"type": "integer",
"format": "int32",
"values": {
"0": {
},
"1": {
"properties": ["match"]
},
"many": {
"minimum": 2,
"maximum": 16,
"properties": ["match", "matchMany"]
}
}
},
"patternsInLine": {
"when": { "hasAll": ["match"]},
"type": "integer",
"format": "int32",
"values": {
"1": {
},
"many": {
"when": { "hasAll": ["matchMany"]},
"minimum": 2,
"maximum": 4
}
}
}
...
你可以在变量集合的任何层次上定义变量条件。例如,下面的例子显示了为整个 file.contents 变量集合定义了一个条件。这个条件模拟了这样的事实:当指定搜索的文件根本不存在或者要匹配的模式为空时,文件的内容就无关紧要。
...
"contents": {
"when": { "allOf": [ { "hasAll": ["fileExists"]}, { "hasNone": ["patternEmpty"]}]},
"members": {
...
}
}
...
变量条件如何影响由Tcases生成的测试用例?在测试用例中,如果某个变量无关紧要,则不会为其分配 value ,而是将其标记为 NA ,意思是“不适用”。例如,下面的测试用例0展示了如何测试空模式导致 file.contents 无关紧要。同样,测试用例8展示了如何测试不存在的文件使几乎所有其他变量变得无关紧要。
"testCases": [
{
"id": 0,
"name": "pattern='empty'",
"has": {"properties": "fileExists,fileName,patternEmpty"},
"arg": {
"pattern": {"value": "", "source": "empty"},
"fileName": {"value": "defined"}
},
"env": {
"file.exists": {"value": true},
"file.contents.linesLongerThanPattern": {"NA": true},
"file.contents.patternMatches": {"NA": true},
"file.contents.patternsInLine": {"NA": true}
}
},
...
{
"id": 8,
"name": "file.exists='false'",
"has": {"properties": "fileName"},
"arg": {
"pattern": {"NA": true},
"fileName": {"value": "defined"}
},
"env": {
"file.exists": {"failure": true, "value": false},
"file.contents.linesLongerThanPattern": {"NA": true},
"file.contents.patternMatches": {"NA": true},
"file.contents.patternsInLine": {"NA": true}
}
}
]
...
Cardinality conditions
Cardinality conditions只关注测试用例中是否存在某些属性。但是,如果测试用例中使用的两个或多个值都贡献了相同的属性,那么一个测试用例可以累积多个实例的属性。在某些情况下,属性的出现次数是对输入空间的显著限制。用户可以使用基数条件来建模这些情况,这些条件检查属性出现的次数是否大于、小于或等于特定值。
例如,考虑一家冰淇淋店,出售不同种类的冰淇淋甜筒。这些美味的产品只有特定的组合,价格取决于添加的冰淇淋球和配料的组合。那么如何进行测试呢?这些甜筒的输入模型可能看起来如下所示。(用户可以在这里找到完整的示例。)
{
"system": "Ice-Cream",
"Cones": {
"arg": {
"Cone": {
"values": {
"Empty": {...},
"Plain": {...},
"Plenty": {...},
"Grande": {...},
"Too-Much": {...}
}
},
"Flavors": {
"members": {
"Vanilla": {"values": {"Yes": {...}, "No": {}}},
"Chocolate": {"values": {"Yes": {...}, "No": {}}},
"Strawberry": {"values": {"Yes": {...}, "No": {}}},
...
}
},
"Toppings": {
"members": {
"Sprinkles": {"values": {"Yes": {...}, "No": {}}},
"Pecans": {"values": {"Yes": {...}, "No": {}}},
"Oreos": {"values": {"Yes": {...}, "No": {}}},
...
}
}
}
}
}
要制作一个圆锥形冰淇淋,你可以添加任何口味和提供的配料。为了方便追踪,这些选择中的每一个都会为我们的冰淇淋测试用例添加一个 scoop 或 topping 属性。
...
"Flavors": {
"members": {
"Vanilla": {"values": {"Yes": {"properties": ["scoop"]}, "No": {}}},
"Chocolate": {"values": {"Yes": {"properties": ["scoop"]}, "No": {}}},
"Strawberry": {"values": {"Yes": {"properties": ["scoop"]}, "No": {}}},
...
}
},
"Toppings": {
"members": {
"Sprinkles": {"values": {"Yes": {"properties": ["topping"]}, "No": {}}},
"Pecans": {"values": {"Yes": {"properties": ["topping"]}, "No": {}}},
"Oreos": {"values": {"Yes": {"properties": ["topping"]}, "No": {}}},
...
}
}
然后我们可以根据冰淇淋的份数和配料的种类来定义特定的锥形积,使用诸如 lessThan 、 equals 和 between 之类的基数条件。
...
"Cone": {
"values": {
"Empty": {
"failure": true,
"when": {"lessThan": {"property": "scoop", "max": 1}}
},
"Plain": {
"when": {
"allOf": [
{"equals": {"property": "scoop", "count": 1}},
{"notMoreThan": {"property": "topping", "max": 1}}
]
}
},
"Plenty": {
"when": {
"allOf": [
{"between": {"property": "scoop", "min": 1, "max": 2}},
{"notMoreThan": {"property": "topping", "max": 2}}
]
}
},
"Grande": {
"when": {
"allOf": [
{"between": {"property": "scoop", "exclusiveMin": 0, "exclusiveMax": 4}},
{"between": {"property": "topping", "min": 1, "max": 3}}
]
}
},
"Too-Much": {
"failure": true,
"when": {
"anyOf": [
{"moreThan": {"property": "scoop", "min": 3}},
{"notLessThan": {"property": "topping", "min": 4}}
]
}
}
}
}
...
但是要小心!
随着属性和条件的限制而来的是巨大的力量。请谨慎使用!有可能定义的限制使得Tcases很难甚至无法生成用户想要的测试用例。如果看起来Tcases似乎被冻结了,那很可能就是这种情况。Tcases并没有被冻结——它正在进行一项漫长而可能徒劳无功的搜索,以找到一组满足用户的限制的值。
接下来的几个部分将描述一些需要留意的情况。
不可行的组合
Tcases总是会根据用户指定的覆盖率级别生成包含特定值组合的测试用例。但如果用户定义了使某些预期的值组合不可能的约束条件呢?如果是这样,我们就说这种组合是“不可行的”。对于包含两个或更多变量(2元组、3元组等)的组合,这可能是预期的,因此Tcases只会记录一个警告并继续运行。对于包含单个变量(默认覆盖级别)的“组合”,这是错误的,用户必须在Tcases继续运行之前修复有问题的约束条件。
小贴士:
-
为了帮助找出给你带来麻烦的坏约束,可以尝试将日志级别更改为 DEBUG 或 TRACE 。
-
你是否使用了更高级别的覆盖率(2元组、3元组等)?如果是这样,请尝试仅使用默认覆盖率运行快速检查。一种简单的方法是像这样运行Tcases:tcases < ${myInputModelFile} 。如果存在不可行的值,此检查有时可以显示错误。
通常情况下,Tcases可以快速报告哪些组合是不可行的。但在某些情况下,Tcases只有在尝试并排除了所有可能性之后才能找到问题所在。如果你觉得Tcases好像被冻住了,那很可能就是这种情况。Tcases并没有被冻住——它正在进行一项漫长、耗费精力且最终徒劳无功的搜索。
为了避免出现此类问题,记住这个简单的规则很有帮助:每个“成功”测试用例必须为所有变量定义有效的值。对于任何一个变量 V ,无论在“成功”测试用例中为其他变量选择了哪些值,都必须至少有一个与这些值兼容的有效值 V 。
例如,以下变量定义是不可行的。因为不存在与 Color 兼容的有效值,所以无法完成包含 Shape=Square 的成功测试用例。
...
"Shape": {
"values": {
"Square": {"properties": ["quadrilateral"]},
"Circle": {}
}
},
"Color": {
"values": {
"Red": {"when": {"hasNone": ["quadrilateral"]}},
"Green": {"when": {"hasNone": ["quadrilateral"]}},
"Blue": {"when": {"hasNone": ["quadrilateral"]}},
"None": {"failure": true}
}
}
...
唯一的例外是当一个变量被明确定义为完全不相关的情况。例如,以下定义是可行的,因为它们明确声明 Color 与 Shape=Square 不兼容。
...
"Shape": {
"values": {
"Square": {"properties": ["quadrilateral"]},
"Circle": {}
}
},
"Color": {
"when": {"hasNone": ["quadrilateral"]},
"values": {
"Red": {},
"Green": {},
"Blue": {},
"None": {"failure": true}
}
}
...
大规模的环境条件
你可以使用 ` anyOf ` 条件来定义一个逻辑“OR”表达式。但是要小心包含大量子表达式的 ` anyOf `。当 Tcases 试图为满足此类条件的值组合寻找可能时,它必须评估大量可能性。随着子表达式的数量增加,可能的数量会以指数级增长!即使存在满足条件的组合,这种情况也可能很快变得难以控制。如果这种情况导致有意的测试用例不可行,情况会变得更糟。如果你觉得 Tcases 运行缓慢或被冻结,那可能就是这种情况。
如果你遇到这种情况,你应该尝试找到一种简化大型复杂条件的方法。例如,你可以通过为产生相同结果的特殊属性赋值来消除子表达式。
定义输入覆盖
Tcases通过为所有输入变量创建值的组合来生成测试用例定义。但是它是如何生成这些组合的呢?为什么是这些特定的组合而不是其他组合?这些测试用例的质量如何?用户可以信赖它们来彻底测试用户的系统吗?
问得好。基本答案是这样的:Tcases会生成满足用户指定的覆盖要求所需的最小数量的测试用例。但是要理解这意味着什么,用户需要了解Tcases是如何衡量覆盖的。
组合测试基础
Tcases关注的是输入空间覆盖率——即测试了多少可行的输入值组合。为了衡量输入空间覆盖率,Tcases 借鉴了组合测试领域的概念。作为测试人员,我们寻找的是能够触发故障的输入值组合,从而暴露出 SUT 中的缺陷。但是,通常我们无法承担测试所有组合所需的努力。我们必须选择某些子集。但是该如何选择呢?
我们可以尝试以下方法。对于第一个测试用例,为每个输入变量选择一个有效的值。然后,对于下一个测试用例,为每个变量选择不同的有效值。继续这样做,直到我们至少使用过每个变量的每个有效值一次。当然,在进行过程中,我们会跳过任何不满足我们约束条件的不可行组合。结果将是一个相对较小的测试用例集合。实际上,假设输入值没有约束条件,该过程产生的“成功”案例数量将是S,其中S是任何单一变量定义的有效值的最大数量。对于失败案例,我们可以通过为每个无效值创建一个新的测试用例,并将其替换为其他有效值的其他组合来实现类似的效果。这将为我们提供F个测试用例,其中F是所有变量的无效值总数。因此,这是S×F个测试用例,一个相当小的测试套件,应该可以轻松实现。那么覆盖率是多少呢?我们已经保证了每个变量的每个值至少被使用一次。这就是所谓的“完全测试”。这种覆盖方式被称为“单向覆盖”或“一元组覆盖”,即对一个变量的所有“组合”进行覆盖(也称为“每个选择覆盖”)。
但是这样就足够了吗?经验和研究告诉我们,许多失败是由两个或多个变量相互作用引起的。因此,也许我们应该追求更高的输入覆盖率。我们可以对每一对输入变量进行迭代,并考虑它们的所有组合的值。
例如, file.contents.linesLongerThanPattern 变量有两个有效的值, file.contents.patternMatches 变量也有两个有效的值。这对变量组合共有4种情况。对于每一对变量,创建一个测试用例,并为其他所有变量填充值。最后,我们将拥有一个测试套件,其中每个这样的对至少被使用一次——这就是“双向覆盖”或“二元组覆盖”(也称为“对齐覆盖”)。这是一个更强大的测试套件——更有可能发现许多缺陷——但它也是更多的测试用例数量。
我们可以将这种方法进一步推广到更高层次的组合覆盖——3-重覆盖、4-重覆盖等。随着层次的提高,我们的测试能力也随之增强。但是代价是,每增加一个层次,测试用例的数量就会迅速增加。在某个时候,收益不再值得付出这样的代价。实际上,研究表明,很少有失败是由4个或更多变量的交互引起的,而需要6个或更多变量交互的失败几乎不存在。大多数失败似乎是由一重或二重交互引起的。但这并不一定意味着你应该停止在二重覆盖上。每个系统都有其独特的风险。此外,并非所有变量的交互作用都相同——你可能有一些需要比其他变量更高层次覆盖的变量集。
需要注意的是,要达到特定的覆盖率水平所需的测试用例数量取决于许多因素。自然地,N-way覆盖所需的测试用例数量会随着N的增大而增加。此外,具有大量值的变量会创建更多的组合需要覆盖,这可能需要更多的测试用例。此外,当输入值之间存在约束时,所需的测试用例数量也会增加。例如,一个变量条件意味着某些测试用例必须使用 "NA": true 来设置该变量的值,这意味着需要额外的测试用例来覆盖实际值。
失败的案例各有不同!
注意,当我们讨论多维变量组合的各个级别时,我们总是小心地将这些组合仅应用于这些变量的有效值。为什么呢?因为异常情况是不同的!
显然,对于每个变量,每个无效值(即带有 ` "failure": true ` 的值)都应该有自己的测试用例。像这样的“失败”情况应该只有一个变量的值无效,而其他所有变量的值都为有效值。这是唯一能确保验证预期失败是由哪个无效值引起的方法。因此,应该理解,无论采用哪种组合覆盖级别,由 Tcases 生成的失败用例的数量始终等于无效值的数量。
当然,对于任何给定的N元有效组合,失败案例的集合几乎总是会包括其中的一些组合。但这并不算数!为了实现真正的N元组覆盖,测试集必须至少在一个“成功”案例中包含所有有效的组合。再次强调这一点的原因是显而易见的。这是唯一确保验证预期结果的方法。
定义更高覆盖率
对于更高级别的覆盖率,用户需要创建一个生成器定义,详细说明用户的覆盖要求。生成器定义是另一份文档,Tcases 将其应用于用户的系统输入定义,并定义了一组“生成器”。
生成器定义是可选的——如果省略,则默认使用每个函数对应的覆盖率生成器。按照惯例,生成器定义出现在一个名为 ${myProjectName}-Generators.json 的文件中。但是,如果用户更喜欢,可以使用 -g 选项在 tcases 命令中指定不同的文件名。
最简单的生成器定义等同于默认值:对所有函数,对所有变量实现1-元组覆盖。它看起来是这样的:
{
"*": {}
}
要为 find 函数中的所有变量实现2元组覆盖,用户可以创建如下的生成器定义:
{
"find": {
"tuples": 2
}
}
要仅对名为 F 的函数要求3-元组覆盖,而为其他所有函数生成2-元组覆盖,用户可以创建如下所示的生成器定义。请注意,用户可以使用特殊函数名 * 明确标识“所有函数”。
{
"*": {
"tuples": 2
},
"F": {
"tuples": 3
}
}
定义多层次的覆盖范围
当你仔细研究你的待测系统(SUT)的功能时,你可能会发现其中一些功能需要更严格的测试,而另一些则不需要。这就是生成器定义允许你做的事情。实际上,当你仔细研究一个单独的功能时,你可能更关心某些特定变量之间的交互。你甚至可能想要测试一小部分关键变量的所有可能组合。在其他地方实现基本覆盖的同时,是否可以在某些区域实现高覆盖?是的,可以做到。本节将解释如何实现这一点。
你已经看到如何为不同的函数指定不同的覆盖率级别。为了更精细的控制,你可以使用一个或多个组合器定义。组合器定义是一个对象,它定义了针对特定变量定义子集所生成的覆盖率级别。你使用一个“变量路径模式”来指定要组合的变量。
例如,这里是一个定义 find 函数的生成器,它为 file.contents 变量集内的所有变量指定了2元组覆盖,对其他变量则指定了1元组覆盖(默认值)。
{
"find": {
"combiners": [
{
"tuples": 2,
"include": [ "file.contents.**" ]
}
]
}
}
可变路径模式描述了到特定变量定义的路径,该路径可能嵌套在变量集层次结构中。通配符允许用户匹配变量集的全部直接子集( * )或全部后代( ** )。请注意,模式中最多只能包含一个通配符,该通配符只能出现在路径的末尾。
你可以使用 include 和 exclude 属性来简洁地描述要组合的变量。例如,这里是一个生成 find 函数的生成器,它为 file.contents 变量集的所有变量生成1-元覆盖(默认值),除了 file.contents.patternsInLine ,对其他所有变量生成2-元覆盖。
{
"find": {
"tuples": 2,
"combiners": [
{
"include": [ "file.contents.*" ],
"exclude": [ "file.contents.patternsInLine" ]
}
]
}
}
一个组合器定义,它指定了特殊值 "tuples": 0 生成的测试用例将包括所有包含变量的所有可能值组合。显然,这种设置有可能产生大量的测试用例,因此应该谨慎使用,仅用于少量变量的组合。
每个组合器定义都描述了如何组合一组特定的变量。那么,那些没有包含在任何组合器中的变量该怎么办呢?对于这些变量,Tcases会自动创建一个默认的组合器定义,使用函数的默认 tuples 进行组合。
管理Tcases项目
使用Tcases设计测试用例集意味着:
-
了解SUT(被测系统)的预期行为
-
创建初始系统输入定义
-
生成、评估和改进测试用例定义
-
评估和改进覆盖需求
-
更改输入定义以处理新情况
你可能很快就能完成所有这些任务。或者这项努力可能会持续相当长的一段时间。无论如何,这都是一个项目。本节提供了一些建议,帮助用户更有效地完成用户的Tcases项目。
管理项目文件
Tcases项目必须处理几个紧密相关的文件:系统输入定义、零个或多个生成器定义,以及从这些文件中生成的测试用例定义文档(可能有多种形式)。tcases 命令实现了一些约定,使这些文件的组织更加容易。
` tcases ` 命令允许用户按照以下约定引用名为 ` ${myProjectName} ` 项目的所有文件。
-
[b0] 系统输入定义文件
-
[b0] 生成器定义文件
-
[b0] 测试用例定义文件
例如,这里有一个简单的命令来运行Tcases。
tcases ${myProjectName}
该命令执行以下操作。
-
从 ${myProjectName}-Input.json读取系统输入定义。
-
如果存在,则从 ` ${myProjectName}-Generators.json ` 读取生成器定义。
-
将测试用例定义写入 ${myProjectName}-Test.json 中。
当然,用户可以使用多种选项来自定义这个默认模式的 tcases 命令。有关详细信息,请参阅 TcasesCommand.Options 类,或者运行 tcases -help 。
复制一个Tcases项目
用户可以使用 ` tcases-copy ` 命令(或者,如果使用 Maven,则使用 ` tcases:copy ` 目标)将一个 Tcases 项目中的所有文件复制到不同的目录、不同的项目名称甚至不同的内容类型中。有关详细信息,请运行 ` tcases-copy -help `.
例如:
# Copy all of the files for the "find" project to /home/tcases/projects
tcases-copy --toDir /home/tcases/projects find
# Copy all of the files for the "find" project to a project named "myProject" in /home/tcases/projects
tcases-copy --toDir /home/tcases/projects --toName myProject find
# Convert all of the files for "myProject" to JSON
tcases-copy --toType json myProject-Input.xml
重复使用测试用例
你知道那种感觉。你花了好几天的时间,精心设计了一套最小的测试用例集,以覆盖所有的测试需求。然后开发人员带着好消息来了:他们决定添加一个新的功能,并增加了一些新的参数。而且他们对一些事情改变了主意。你知道那个必需的参数吗?现在它变成了可选的——不填写它不再是错误。有时候,这似乎是他们故意在折磨你。但说实话,大多数时候这只是开发项目的正常进展。经过几次迭代后,你获得了更多需要应用于你正在构建的系统的知识。或者在发布一两次之后,是时候让系统具备新的功能了。
无论如何,还是回到老式的测试绘图板上吧。难道不是吗?你没必要改变一切。你为什么不能只修改一下已有的测试用例呢?你问得真巧。因为Tcases正是这样做的。实际上,这是默认的工作方式。还记得那个简单的 tcases 命令行吗?
tcases ${myProjectName}
这才是它真正的用途:
-
从 ${myProjectName}-Input.json读取系统输入定义。
-
如果存在,则从 ` ${myProjectName}-Generators.json ` 读取生成器定义。
-
如果存在,则读取 ${myProjectName}-Test.json 中的先前测试用例。
-
然后编写新的测试用例定义并将其保存到 ${myProjectName}-Test.json 中,尽可能重用之前的测试用例,并根据需要进行扩展或修改。
你可能更愿意忽略之前的测试用例,并从头开始创建新的测试用例。尤其是在项目的早期阶段,你仍在完善系统输入定义的细节时,更是如此。如果是这样,你可以使用 -n 选项,始终创建新的测试用例,忽略任何之前的测试用例。
tcases -n ${myProjectName}
混合搭配:Random Combinations
默认情况下,Tcases 会按照系统输入定义的顺序从上到下遍历输入变量,并按照发现它们的顺序拾取它们。用户可以尝试利用这种自然的顺序,尽管满足约束条件可能会使事情偏离可预测的顺序。这就是为什么用户不应该太关心 Tcases 生成的组合的原因。更好的方法是让 Tcases 随机化其组合过程。
你可以在生成器定义中使用 ` seed ` 属性来定义随机组合(见下面的例子)。这个整数值作为随机数生成器的种子,控制组合过程。或者,你可以在后面的节中描述的命令行选项中重新定义种子值。通过明确指定种子值,你可以确保每次使用此生成器定义运行 Tcases 时将始终使用完全相同的随机组合。
{
"find": {
"seed": 200712190644
}
}
随机组合的结果可能非常有趣。首先,你可能会得到一些你可能没有考虑过的测试用例,尽管它们完全有效并且具有相同的覆盖率。有时,这足以暴露一个可能被忽视的缺陷,仅仅是因为没有人想到要尝试这种情况。这是将“不必要的多样性”原则应用于测试的一种方式。这还会产生另一个好处——有时不寻常的组合可以揭示你的测试设计中的缺陷。如果一个组合根本就不合理,那么很可能是一个约束缺失或不正确。
最后,随机组合偶尔可以减少所需的测试用例数量,以满足用户的覆盖要求。这是因为某些组合可能比其他同样有效的组合更有效地“消耗”变量元组。Tcases不会尝试花费巨大的努力来保证一个优化的最小测试用例集。它只是从头开始,尽其所能尽快到达终点。但是,通过组合的随机遍历,Tcases可能会找到更高效的路径。如果用户担心测试套件的大小,请尝试使用Tcases Reducer。
减少测试用例:A Random Walk
随机浏览这些组合可能会将Tcases引导到一个较小的测试用例集合。因此,你可以尝试反复地修改你的生成器定义,使用不同的 seed 值,以寻找一个能最小化生成的测试定义文件大小的值。听起来很乏味,对吧?所以,不要这样做——使用Tcases Reducer吧。
接下来,我将向用户展示如何使用 tcases-reducer 命令来实现这一效果。
cd ${tcases-release-dir}
cd docs/examples/json
tcases-reducer find-Input.json
结果如何?现在有一个名为“ find-Generators.json ”的文件,其内容类似于:一个使用每个函数的随机种子的生成器定义。
{
"find": {
"seed": 1909310132352748544
}
}
但是为什么选择这个种子值呢?为了更详细地了解情况,请查看生成的 tcases-reducer.log 文件(下面是一个示例)。首先,Reducer不使用随机种子生成测试用例,生成了10个测试用例。然后,Reducer再次尝试,将结果减少到9个测试用例。然后,Reducer又尝试了几次,每次使用不同的随机种子。最后,Reducer无法找到更大的减少结果,因此终止了操作。
INFO org.cornutum.tcases.Reducer - Reading system input definition=find-Input.json
INFO o.c.t.generator.TupleGenerator - FunctionInputDef[find]: generating test cases
...
INFO o.c.t.generator.TupleGenerator - FunctionInputDef[find]: completed 10 test cases
INFO o.c.t.generator.TupleGenerator - FunctionInputDef[find]: generating test cases
...
INFO o.c.t.generator.TupleGenerator - FunctionInputDef[find]: completed 9 test cases
INFO org.cornutum.tcases.Reducer - Round 1: after 2 samples, reached 9 test cases
...
INFO org.cornutum.tcases.Reducer - Round 2: after 10 samples, terminating
INFO org.cornutum.tcases.Reducer - Updating generator definition=find-Generators.json
简化器负责完成寻找最佳随机种子的所有工作,而不会覆盖任何现有的测试定义文件。以下是其工作原理。简化过程按“轮”操作。每一轮由一系列称为“样本”的测试用例生成组成。每个样本使用一个新的随机种子为指定函数(或默认情况下为所有函数)生成测试用例,以尝试找到产生最少测试用例的种子。如果一轮中的所有样本在不减少当前最小测试用例数的情况下完成,则简化过程将终止。否则,一旦达到新的最小值,就会开始新的一轮。每个后续轮中的样本数量由“重采样因子”确定。简化过程结束后,将使用产生最小测试用例数的随机种子值更新给定系统输入定义的生成器定义文件。
尽管 Reducer 会生成一个最小化测试用例的随机种子,但用户仍然需要考虑这些测试用例是否令人满意。用户可能会想知道是否使用不同的种子可以生成一个同样小但更有趣的测试用例集。如果是这样,请尝试使用 ` -R ` 选项。这会告诉 Reducer 忽略生成器定义中的任何先前随机种子,并寻找新的种子值。
关于 tcases-reducer 命令(及其Windows对应命令 tcases-reducer.bat )的所有选项的详细信息,请参见 ReducerCommand.Options 类的Javadoc。要在命令行上获取帮助,请运行 tcases-reducer -help 。
避免不必要的组合
即使在Tcases为默认的1-元覆盖生成测试用例时,也经常会看到某些输入值被多次使用。这很可能是因为那些只包含少量值定义的变量定义。即使使用了这些值之后,Tcases仍将继续重复使用它们来填充完成测试套件所需的剩余测试用例。在某些情况下,这可能会带来一些不便。有时,用户需要至少测试一次某个值定义,但由于各种原因,包括多次重复使用该值可能会增加复杂性,而实际上并不会增加发现新错误的可能性。在这种情况下,用户可以使用 once 属性作为提示,以避免重复使用值的次数超过一次。
例如,` find `命令要求 ` pattern ` 的长度不能超过文件中每一行的最大长度。即使有一行比模式多出一行也足以避免出现这种错误情况。实际上,边界值测试的原则建议应该有一个正好比模式多出一行的测试用例。因此:
...
"linesLongerThanPattern": {
"type": "integer",
"format": "int32",
"values": {
"1": {
"properties": ["matchable"]
},
"many": {
"minimum": 2,
"maximum": 32,
"properties": ["matchable"]
},
"0": {
"failure": true
}
}
},
...
但是这种情况很少出现,而且创建满足这种特殊条件的测试文件也是一件很麻烦的事情,而且要将文件扩展到满足其他条件也是很复杂的。此外,这种特殊条件与其他变量组合的高阶交互的可能性也很小。因此,让我们添加 "once": true 来请求Tcases仅在单个测试用例中包含此值。
...
"linesLongerThanPattern": {
"type": "integer",
"format": "int32",
"values": {
"1": {
"properties": ["matchable"],
"once": true
},
"many": {
"minimum": 2,
"maximum": 32,
"properties": ["matchable"]
},
"0": {
"failure": true
}
}
},
...
太棒了!但是请记住, once 提示并不总是会被遵守。即使在 "once": true 的情况下,如果某个值需要满足剩余测试用例中的约束条件,那么它可能会被多次使用。
这个 once 提示实际上是一个仅适用于单个变量值的1-元元组的快捷方式。如果生成器定义中包含了该变量的更高阶元组,那么 once 就不会起作用。但是,同样情况也可能出现在更高阶组合中。例如,尽管用户可能希望对一组变量进行对对覆盖,但其中一个或多个2元组可能是特殊情况,最多只能使用一次。要定义此类例外情况,可以在生成器定义中的组合器中添加一个或多个 once 元组。例如,对于 find 函数的以下生成器定义,它指定了对所有变量的2元组覆盖,但告诉Tcases只创建一个使用特定2元组组合的测试用例:包含多个匹配模式的行,其中包含引号。
{
"find": {
"combiners": [
{
"tuples": 2,
"once": [
{
"pattern": "quotedQuotes",
"file.contents.patternsInLine": "many"
}
]
}
]
}
}
简单的生成器定义
Tcases提供了一些选项,以便更轻松地创建和更新一个简单的生成器定义文档。
定义随机种子
要定义随机组合种子,请使用 -r 选项。例如,以下命令使用指定的种子值生成默认生成器的测试用例。
tcases -r 299293214 ${myProjectName}
如果你已经有一个名为“ ${myProjectName}-Generators.json ”的文件,这个命令会通过添加或更改默认的“ seed ”值来更新该文件,如下所示。如果尚未存在名为“ ${myProjectName}-Generators.json ”的文件,它将创建一个。
{
"find": {
"seed": 299293214
}
}
如果你想随机组合数据,但对种子值没有特殊要求,可以使用 -R 选项,Tcases会为你选择一个随机的种子值。当用户想查看不同的种子值是否可能产生更有趣的测试用例组合时,此选项非常有用。
定义默认覆盖率
要为所有函数设置默认覆盖级别,可以使用 ` -c ` 选项。例如,以下命令将生成使用指定覆盖级别的默认生成器的测试用例:
tcases -c 2 ${myProjectName}
如果你已经有了一个名为“ ${myProjectName}-Generators.json ”的文件,这个命令将通过添加或更改默认的“ tuples ”值来更新该文件,如下所示。如果尚未存在名为“ ${myProjectName}-Generators.json ”的文件,它将创建一个。
{
"find": {
"tuples": 2
}
}
转换测试用例
Tcases生成的测试用例定义不是可以直接执行的。它们的目的是指定并指导实际测试的构建。但由于测试用例定义可以以明确的JSON文档形式出现,因此将其转换为更具体的形式并不困难。本节描述了Tcases提供的输出转换选项。
创建HTML报告
测试用例定义的JSON格式很简单。但说实话——阅读长长的JSON文档可能会让人感到乏味。这可能不是在手动测试过程中向他人提供指导时想要的东西。那么,我们不妨将相同的信息以网页的形式展示在浏览器中呢?要实现这一点,只需在 tcases 命令中添加 -H 选项,Tcases将自动将测试用例定义以HTML文件的形式编写出来。
下面是一个简单的例子。请尝试执行以下命令:
cd ${tcases-release-dir}
cd docs/examples/json
tcases -H find
这会在 ` find-Input.json ` 中输入的定义上运行 Tcases,并生成一个名为 ` find-Test.htm ` 的文件。用浏览器打开这个文件,用户会看到类似下面这个简单的 HTML 报告。这个报告允许用户浏览所有测试用例,并详细查看每个测试用例。用户会看到与选定测试用例相关的所有输入值(忽略与该测试用例无关的任何输入变量)。
不喜欢这种报告格式吗?用户可以使用 TestDefToHtmlFilter 类来定义并应用自己的展示格式。
编写JUnit/TestNG测试
将测试用例转换为JUnit或TestNG代码是 tcases 命令所具有的功能。它是如何工作的呢?只需添加 -J 选项,Tcases就会自动将测试用例定义以Java代码的形式编写为JUnit测试。同样,该代码也适用于TestNG。
下面是一个简单的例子。请尝试执行以下命令:
cd ${tcases-release-dir}
cd docs/examples/json
tcases -J < find-Input.json
你将在标准输出中看到以下内容:每个测试用例定义都已转换为一个名为 @Test 的方法。该方法的名称基于函数名称。Javadoc 注释描述了该测试用例的输入值。同样,所有输入值赋值都在方法体中显示出来。否则,方法体是空的,等待你填写实现代码。对于失败测试用例,Javadoc 突出显示了定义该案例的单个无效值。
/**
* Tests {@link Examples#find find()} using the following inputs.
* <P>
* <TABLE border="1" cellpadding="8">
* <TR align="left"><TH colspan=2> 0. find (Success) </TH></TR>
* <TR align="left"><TH> Input Choice </TH> <TH> Value </TH></TR>
* <TR><TD> pattern </TD> <TD> </TD> </TR>
* <TR><TD> fileName </TD> <TD> defined </TD> </TR>
* <TR><TD> file.exists </TD> <TD> true </TD> </TR>
* <TR><TD> file.contents.linesLongerThanPattern </TD> <TD> (not applicable) </TD> </TR>
* <TR><TD> file.contents.patternMatches </TD> <TD> (not applicable) </TD> </TR>
* <TR><TD> file.contents.patternsInLine </TD> <TD> (not applicable) </TD> </TR>
* </TABLE>
* </P>
*/
@Test
public void find_0()
{
// properties = fileExists,fileName,patternEmpty
// Given...
//
// pattern =
//
// fileName = defined
//
// file.exists = true
//
// file.contents.linesLongerThanPattern = (not applicable)
//
// file.contents.patternMatches = (not applicable)
//
// file.contents.patternsInLine = (not applicable)
// When...
// Then...
}
...
当 System 对应一个类并且每个 Function 对应一个要测试的方法时, -J 选项最有用。因此,Javadoc包含一个指向要测试的方法的 @link ,如上所示。可以通过定义 class 参数或 system 参数来自定义此 @link 的形式。例如,如果用户使用选项 -J -p class=MyClass ,则输出将如下所示:
/**
* Tests {@link MyClass#find find()} using the following inputs.
* <P>
* <TABLE border="1" cellpadding="8">
* <TR align="left"><TH colspan=2> 0. find (Success) </TH></TR>
* <TR align="left"><TH> Input Choice </TH> <TH> Value </TH></TR>
* <TR><TD> pattern </TD> <TD> </TD> </TR>
* <TR><TD> fileName </TD> <TD> defined </TD> </TR>
* <TR><TD> file.exists </TD> <TD> true </TD> </TR>
* <TR><TD> file.contents.linesLongerThanPattern </TD> <TD> (not applicable) </TD> </TR>
* <TR><TD> file.contents.patternMatches </TD> <TD> (not applicable) </TD> </TR>
* <TR><TD> file.contents.patternsInLine </TD> <TD> (not applicable) </TD> </TR>
* </TABLE>
* </P>
*/
@Test
public void find_0()
{
...
或者,如果用户使用选项 " -J -p system=MySystem ",则输出将如下所示:
/**
* Tests MySystem using the following inputs.
* <P>
* <TABLE border="1" cellpadding="8">
* <TR align="left"><TH colspan=2> 0. find (Success) </TH></TR>
* <TR align="left"><TH> Input Choice </TH> <TH> Value </TH></TR>
* <TR><TD> pattern </TD> <TD> </TD> </TR>
* <TR><TD> fileName </TD> <TD> defined </TD> </TR>
* <TR><TD> file.exists </TD> <TD> true </TD> </TR>
* <TR><TD> file.contents.linesLongerThanPattern </TD> <TD> (not applicable) </TD> </TR>
* <TR><TD> file.contents.patternMatches </TD> <TD> (not applicable) </TD> </TR>
* <TR><TD> file.contents.patternsInLine </TD> <TD> (not applicable) </TD> </TR>
* </TABLE>
* </P>
*/
@Test
public void find_0()
{
...
如果用户不想在测试方法体中显示输入值赋值,可以通过添加以下选项来排除它们:`[TestMethod(ExcludeData=true)]`。
使用 -J 选项还会更改 tcases 命令的默认输出文件。通常,当用户在处理Tcases项目时,生成的测试用例定义会默认写入一个名为 ${myProjectName}-Test.json 的文件中。但是,使用 -J 时,生成的 @Test 方法会默认写入一个名为 ${myProjectName}Test.java 的文件中。例外情况:如果用户的 ${myProjectName} 不是有效的Java类标识符,则会用稍作修改的项目名称代替。
例如,以下命令将生成的测试定义以 @Test 方法的形式写入一个名为 findTest.java的文件中。
tcases -J find
使用Output Annotations
有时候,仅凭输入模型中的基本信息(函数、变量和值)还不足以生成具体的测试用例。你需要添加一些额外的信息,这些信息对于生成测试用例并不重要,但对于生成最终的输出结果却是必不可少的。这就是输出注释的作用所在。
输出注释是一种特殊的属性设置——一个名称-值对——用户可以将其添加到系统输入定义的各种元素中。它不会影响Tcases生成的测试用例。但是,Tcases会累积输出注释并将其附加到最终的系统测试定义文档中。此外,Tcases还将自动附加列出每个测试用例属性的输出注释。从那里,任何输出转换器都可以使用这些输出注释将测试用例转换为最终形式。
它是如何工作的
我们可以添加以下几种输出注释。
System annotations
系统注释是在顶层系统定义对象上添加一个 has 对象创建的。每个系统注释都是通过将其应用于顶层测试定义对象来传输到输出文档中的。此外,每个系统注释还添加到系统测试定义中的所有函数和测试用例对象中。
例如,给出以下系统注释…
{
"system": "Things",
"has": {
"systemType": "Graphics"
},
...
}
最终的测试定义如下所示:
{
"system": "Things",
"has": {
"systemType": "Graphics"
},
"Make": {
"has": {
"systemType": "Graphics"
},
"testCases": [
{
"id": 0,
"name": "Color.Hue='Red'",
"has": {
"systemType": "Graphics"
},
...
},
...
]
}
}
Function annotations
函数注释是通过在函数输入定义中添加一个 has 对象创建的。每个函数注释通过将其应用于相应的输出函数定义来转移到输出文档中。此外,每个函数注释还添加到其作用范围内的所有测试用例中。对于函数的注释会覆盖系统中相同名称的任何注释。
例如,给出以下函数注释…
{
"system": "Things",
"has": {
"systemType": "Graphics"
},
"Make": {
"has": {
"measurement": "None"
}
...
}
}
最终的测试定义如下所示:
{
"system": "Things",
"has": {
"systemType": "Graphics"
},
"Make": {
"has": {
"systemType": "Graphics",
"measurement": "None"
},
"testCases": [
{
"id": 0,
"name": "Color.Hue='Red'",
"has": {
"systemType": "Graphics",
"measurement": "None"
},
...
},
...
]
}
}
Variable binding annotations
变量绑定注释是通过在变量或变量集定义中添加一个 has 对象创建的。用户还可以通过在输入类型组中添加一个 has 对象来为一组变量创建变量绑定注释。或者,用户可以通过在值定义中添加一个 has 对象来为特定的值创建变量绑定注释。每个变量绑定注释都会将自身应用于其作用域内的所有变量绑定对象,并将其传递到输出文档中。
对于一个值的注释会覆盖相同名称定义在变量上的注释,而变量上的注释又会覆盖变量集合上的注释,而变量集合上的注释又会覆盖输入类型组上的注释。
例如,给出以下变量绑定注释…
{
"system": "Things",
"has": {
"systemType": "Graphics"
},
"Make": {
"has": {
"measurement": "None"
},
"arg": {
"has": {
"valueType": "Valid"
},
"Color": {
"has": {
"measurement": "Ordinal"
},
"members": {
"Hue": {
"has": {
"measurement": "Nominal"
},
"values": {
"Red": {},
"Green": {},
"Blue": {}
}
},
...
"Saturation": {
"values": {
"Pale": {},
"Even": {},
"Intense": {},
"Undefined": {
"has": {
"valueType": "Invalid"
},
"failure": true
}
}
}
}
},
...
}
}
}
最终的测试定义如下所示:
{
"system": "Things",
"has": {
"systemType": "Graphics"
},
"Make": {
"has": {
"systemType": "Graphics",
"measurement": "None"
},
"testCases": [
...
{
"id": 6,
"name": "Color.Saturation='Undefined'",
"has": {
"systemType": "Graphics",
"measurement": "None"
},
"arg": {
"Color.Hue": {
"has": {
"valueType": "Valid",
"measurement": "Nominal"
},
"value": "Red"
},
...
"Color.Saturation": {
"has": {
"valueType": "Invalid",
"measurement": "Ordinal"
},
"failure": true,
"value": "Undefined"
},
...
}
}
]
}
}
Multi-level output annotations
为什么系统和功能注释也会复制到所有相关的测试用例定义中?因为这些注释可能具有多种用途。在某些情况下,这些注释可以用来形成转换后的输出文档的相应级别。在其他情况下,这些注释可以用来定义系统或功能级别的默认值,用于形成单独的特定测试用例。
Property annotations
测试用例的特性值可以作为进一步转换测试用例数据的有用元数据。因此,Tcases会自动将它们附加到每个生成的测试用例上,使用一个名为“properties”的特殊输出注释。
往期系列文章
阿里微服务质量保障系列:微服务知多少
阿里微服务质量保障系列:研发流程知多少
阿里微服务质量保障系列:研发环境知多少
阿里微服务质量保障系列:阿里变更三板斧
阿里微服务质量保障系列:故障演练
阿里微服务质量保障系列:研发模式&发布策略
阿里微服务质量保障系列:性能监控
阿里微服务质量保障系列:性能监控最佳实践
阿里微服务质量保障系列:基于全链路的测试分析实践
- END -
下方扫码关注 软件质量保障,与质量君一起学习成长、共同进步,做一个职场最贵Tester!
-
关注公众号, 后台回复【测开】获取测试开发xmind脑图
-
扫码加作者, 获取加入测试社群!
往期推荐
聊聊工作中的自我管理和向上管理
经验分享|测试工程师转型测试开发历程
聊聊UI自动化的PageObject设计模式
细读《阿里测试之道》
我在阿里做测开