Skip to content
🎨 作者:海针 📔 阅读量:

一种针对Linux系统异常命令的测试方法技术交底书

0、缩略语和关键术语定义

术语全称解释
LinuxGNU/Linux一种免费使用和自由传播的类UNIX操作系统。
ShellShell传统意义上的Shell指的是命令行式的Shell,是操作系统最外面的一层,提供了你与操作系统之间通讯的方式,同时它又是一种程序设计语言
自动化测试框架/一个或多个自动化测试基础模块、自动化测试管理模块、自动化测试统计模块等组成的工具集合。
Shell状态码/执行命令的退出状态码,可体现Shell命令执行情况。

1、相关技术背景(背景技术),与本发明最相近似的现有实现方案(现有技术)

在基于Linux操作系统的自动化测试中,需要使用各种Shell命令对系统进行操作并测试系统响应是否符合预期。这些命令往往都可以通过自动化测试完成,通过自动化测试框架执行转换好的测试用例,以此完成测试。

例如目前较为流行的测试框架: shUnit2 、bats 均可胜任该工作。针对 Linux 系统执行 Shell 命令后,都会有对应的返回状态码,执行成功为0,执行失败为非0。所以针对 Shell 命令的测试中,状态码在自动化测试框架中运用比较频繁,主要用于自动化测试用例的断言,例如判断执行的 Shell 命令,是否符合预期。

状态码详情可参考:

表1 Shell 状态码对应详情

状态码对应情况
0命令成功结束
1通用未知错误
2误用Shell命令
126命令不可执行
127没找到命令
128无效退出参数
128+xLinux信号x的严重错误
130Linux信号2的严重错误,即命令通过SIGINT(Ctrl+C)终止
255退出状态码越界

1.1、与本发明相关的现有技术

1.1.1、现有技术的技术方案

为了更好的说明,此时使用 Shell脚本模拟一个简单的测试场景:

l 步骤:向文件testfile1插入内容“True”

l 预期1:文件testfile1中包含关键字“True”

l 预期2:文件testfile1中不包含关键字“False”

l 测试脚本内容如下:

#! /bin/bashecho True> testfile1 echo "【场景1】grep筛选到关键字True,输出结果:"cat testfile1 | grep True; echo -e "\n命令执行状态:$?"echo "___________________" echo "【场景2】grep未筛选到关键字False,输出结果:"cat testfile1 | grep False; echo -e "\n命令执行状态:$?"echo "___________________" echo "【场景3】grep筛选关键字Fasle时文件名称输入错误,输出结果:"cat testfile2 | grep False; echo -e "\n命令执行状态:$?"echo "___________________"

执行以上脚本结果如下:

【场景1】grep筛选到关键字,输出结果:True 命令执行状态码:0___________________【场景2】grep未筛选到关键字,输出结果: 命令执行状态码:1___________________【场景3】模拟场景2日志名称错误报错,输出结果:cat: testfile2: 没有那个文件或目录 命令执行状态码:1

通过以上脚本【场景1】和【场景2】命令执行结果,可以看出当工具“grep”筛选到关键字返回0,未筛选到关键字会返回1,哪怕命令是成功结束的,说明“grep”会改变系统状态码。通过【场景3】可看到命令执行失败(日志文件缺失),但最终返回状态码1,与【场景2】结果一致。

如果后续针对该场景做测试,预期结果断言为非0通过,那么【场景3】执行了错误的命令,从返回结果来看也是符合预期的,测试结果为通过。其实【场景3】最终状态码返回值与命令中包含管道“|”也存在关系,管道会对最终状态码产生影响,与“grep”都存在时,会产生双重干扰,下面用纯管道命令举例。

例如在一条测试用例中,包含以下操作步骤:

l 步骤:打印文件testfile1内容,同时把内容“True”替换为“False”展示

l 测试脚本内容如下:

#! /bin/bashecho True> testfile1 echo "【场景4】打印文件内容,并进行了数据处理,输出结果:"cat testfile1 | sed ‘s/True/False/g’; echo -e "\n命令执行状态:$?"echo "___________________" echo "【场景5】打印文件内容,并进行了数据处理,但文件名输入错误,输出结果:"cat testfile2 | sed ‘s/True/False/g’; echo -e "\n命令执行状态:$?"echo "___________________" rm testfile1

执行以上脚本结果如下:

【场景4】打印文件内容,并进行了数据处理,输出结果:False命令执行状态码:0** **___________________【场景5】模拟场景4文件名称错误,输出结果:cat: testfile2: 没有那个文件或目录 命令执行状态码:0

通过【场景4】和【场景5】可以看出,当命令中包含管道“|”,状态码的返回值是以最后一个命令的返回值为准,即使管道前的命令状态码非0。所以从状态码来看,是无法识别出命令异常的,之前提到的【场景3】同理。

上述举例的情况,在实际工作中出现概率是很大的,除了人为原因代码编写错误或经验不足以外,例如依赖的工具更新/需求变更,导致命令/参数发生变化,同样会导致正常命令变为异常命令。问题看似不大,却存在巨大的质量风险,因为你不知道执行通过的用例中,有一条已经无效了,用例体量大的话,日积月累下甚至会出现很多此类情况。针对此类问题,在人工测试时很容易发现,但是在自动化测试时,市面上现有的测试框架并不能很好的发现此类问题。

1.1.2、现有技术的缺点

1.1.2.1、shUnit2测试框架

使用目前较为流行的Shell开源测试框架 shUnit2 对1.1.1章节脚本中包含的命令进行测试,测试用例如下:

#! /bin/sh# file: examples/equality_test.sh oneTimeSetUp() { echo True > testfile1} # 用例1test1() { assertTrue "cat testfile1 | grep True"} # 用例2test2() { assertFalse "cat testfile1 | grep False"} # 用例3test3() { assertFalse "cat testfile2 | grep False"} # 用例4test4() { assertTrue "cat testfile1 | sed ‘s/True/False/g’"} # 用例5test5() { assertTrue "cat testfile2 | sed ‘s/True/False/g’"} oneTimeTearDown() { rm testfile1} # Load and run shUnit2.. ../shunit2

执行以上测试结果如下:

test1test2test3test4test5 Ran 5 tests. OK

通过以上执行结果可以看到5条用例全部执行通过。但是【用例3】和【用例5】的执行结果是错误的,因为testfile2这个目录并不存在,这是一条异常命令,但用例的测试结果却是通过,这显然是一个无效测试。

**1.1.2.2、**bats 测试框架

使用另外一个热门 Shell 测试框架 bats 进行测试:

#!/usr/bin/env bats setup(){ echo True > testfile1} # 用例1@test "test1" { run cat testfile1 | grep True [ $status -eq 0 ]} # 用例2@test "test2" { run cat testfile1 | grep False [ $status -ne 0 ]} # 用例3@test "test3" { run cat testfile2 | grep False [ $status -ne 0 ]} # 用例4@test "test4" { run cat testfile1 | sed ‘s/True/False/g’ [ $status -ne 0 ]} # 用例5@test "test5" { run cat testfile2 | sed ‘s/True/False/g’ [ $status -ne 0 ]} teardown()

执行测试结果如下:

✗ test1 (in test file test1.bats, line 11) `run cat testfile1 | grep True' failed ✗ test2 (in test file test1.bats, line 18) `run cat testfile1 | grep False' failed ✗ test3 (in test file test1.bats, line 25) `run cat testfile2 | grep False' failed ✗ test4 (in test file test1.bats, line 32) `[ $status -eq 0 ]' failed with status 2 ✗ test5 (in test file test1.bats, line 38) `[ $status -eq 0 ]' failed with status 2 5tests, 5 failures

可以看到所有测试命令的结果均为失败,很明显测试命令中包含管道时,会影响框架对测试结果的判断,这里就限制了很多测试场景了,部分用例将命令状换后去掉管道再进行测试(用例4、用例5主要描述管道问题,由于无法使用,这里直接去掉):

#!/usr/bin/env bats setup(){ echo True > testfile1} # 用例1@test "test1" { run grep True testfile1 [ $status -eq 0 ]} # 用例2@test "test2" { run grep False testfile1 [ $status -ne 0 ]} # 用例3@test "test3" { run grep False testfile2 [ $status -ne 0 ]} teardown()

运行结果如下:

✓ test1 ✓ test2 ✓ test3 3 tests, 0 failures

现在可以测试成功了,但是和 shUnit2 测试框架一样,用例3的测试结果同样是错误的,未识别出异常命令。而且该框架测试场景也存在一定限制,在实际测试中使用管道的场景还是很多,比如数据的多重处理、需要人机交互的命令等,所以无法满足目前测试场景中复杂的变化。

2、本发明技术方案的详细阐述

2.1、本发明所要解决的技术问题

针对Linux系统Shell命令的测试中,为了解决上述技术问题,本发明提供了一种在自动化测试框架中执行测试命令的算法。经过该设计方法来执行测试命令,命令会在方法内部进行大量逻辑处理,重新制定状态码,快速识别异常命令并对异常命令的测试提供支撑,保留最大测试场景支持的同时,最终还能提供准确、可靠的测试结果。

2.2、本发明提供的完整技术方案

方案流程:

图1 运行逻辑主流程

图2 运行逻辑子流程1

图3 运行逻辑子流程2

图4 运行逻辑子流程3

图5 运行逻辑子流程4

准备工作:

针对需要测试的Linux命令的测试,需要给其命令类型定位,并给予标记,例如定义变量`case_type=True/False`。实现方式可以在调用执行命令的方法时传入参数,也可以在测试用例编写时直接定义等。类型设计定义为两大类型:

(1) 正确命令:系统执行成功,状态码返回0的命令。

(2) 异常命令:系统执行失败,状态码返回非0的命令。

注:定义这个两个类型的原因,是为了满足我们日常测试中更丰富的测试场景,例如我们会针对异常命令进行测试,虽然命令报错了,但是这正是我们想要达到的预期,同时会需要对错误输出的文案内容做进一步测试。所以当该命令被定义为`异常命令`后,执行命令最终报错,在测试眼里反而是“执行成功”,反之如果执行成功,那么其实是“执行失败”。以上只是在测试场景较为简单时适用,除此之外还有一些更复杂的场景,有不同的处理逻辑,后续会有说明。



方案详情:

往往一条测试用例中会存在多个执行命令,这里仅用执行一条命令的流程举例,后续说明当出现多条命令时如何处理,单命令处理流程如下:

  1. 在执行一条Linux命令前,先对命令进行解析,判断命令的组成内容,这里定义为4大场景:

(1) 【场景1】命令中不包含`管道`与`grep`命令。

(2) 【场景2】命令中不包含`管道`,但包含`grep`命令。

(3) 【场景3】命令中包含`管道`,但不包含`grep`命令。

(4) 【场景4】命令中包含`管道`与`grep`命令。

  1. 根据解析后的命令分类进行不同处理:

(1) 【场景1】如“图2 运行逻辑子流程1”中所示,处理流程为:

① 执行测试命令。

② 判断其状态码是否为0。

1) 状态码为0,继续判断命令类型:

a. 正确命令:返回状态码0,表示这是一条正常执行完成的命令。

b. 异常命令:返回状态码1,表示这是一条报错的异常命令。

2) 状态码非0,继续判断命令类型:

a. 正确命令:返回状态码1,表示这是一条报错的异常命令。

b. 异常命令:返回状态码0,表示这是一条正常执行完成的命令。

③ 返回该条命令执行后重新定制的状态码与标准/错误输出内容。

④ 流程结束。

注:这个场景是最常见与普通的场景,针对市面上现有的处理方法来说,新增的是搭配`命令类型`使用的部分,对异常命令测试的执行结果状态码做了新的定义,使测试场景更为丰富和灵活。

(2) 【场景2】如“图3 运行逻辑子流程2”中所示,处理流程为:

① 执行测试命令。

② 判断其状态码是否为0。

1) 状态码为0,继续判断命令类型:

a. 正确命令:返回状态码0,表示这是一条正常执行完成的命令。

b. 异常命令:返回状态码1,表示这是一条报错的异常命令。

2) 状态码非0,继续判断命令类型:

a. 类型为正确命令,继续判断输出内容是否为空:

a) 空:返回状态码0,表示这是一条正常执行完成的命令。

b) 非空:返回状态码1,表示这是一条报错的异常命令。

b. 类型为异常命令,继续判断输出内容是否为空:

a) 空:返回状态码1,表示这是一条报错的异常命令。

b) 非空:返回状态码0,表示这是一条正常执行完成的命令。

③ 返回该条命令执行后重新定制的状态码与标准/错误输出内容。

④ 流程结束。

注:这里利用了`grep`命令的特性,当未筛选到关键字返回状态码非0,且标准输出不会存在内容,如果有输出内容则说明是错误输出,是命令执行的报错信息。

(3) 【场景3】如“图4 运行逻辑子流程3”中所示,处理流程为:

① 在执行测试命令前,引入命令`set`,并配置参数`-o pipefail`。

② 执行测试命令。

③ 判断其状态码是否为0。

1) 状态码为0,继续判断命令类型:

a. 正确命令:返回状态码0,表示这是一条正常执行完成的命令。

b. 异常命令:返回状态码1,表示这是一条报错的异常命令。

2) 状态码非0,继续判断命令类型:

a. 正确命令:返回状态码1,表示这是一条报错的异常命令。

b. 异常命令:返回状态码0,表示这是一条正常执行完成的命令。

④ 返回该条命令执行后重新定制的状态码与标准/错误输出内容。

⑤ 再次调用命令`set`,并配置参数`+o pipefail`,恢复默认状态,避免对后续用例造成影响。

⑥ 流程结束。

注:这里运用`set`命令达到的效果是重组状态码,无论命令中有多少管道,只要有一条命令返回状态码非0,则最终状态码也返回非0;反之均为0时,最终状态码为0。这里避免了管道中存在命令报错,但最终状态码为0的情况,解决了管道中命令出现异常后带来的风险。

(4) 【场景3】如“图5 运行逻辑子流程4”中所示,处理流程为:

① 在执行测试命令前,引入命令`set`,并配置参数`-o pipefail`。

② 执行测试命令。

③ 判断存在的`grep`命令是否在管道最后:

1) `grep`命令不在管道最后:

a. 判断其状态码是否为0:

a) 状态码为0,继续判断命令类型:

i. 正确命令:返回状态码0,表示这是一条正常执行完成的命令。

ii. 异常命令:返回状态码1,表示这是一条报错的异常命令。

b) 状态码非0,继续判断命令类型:

i. 正确命令:返回状态码1,表示这是一条报错的异常命令。

ii. 异常命令:返回状态码1,表示这是一条报错的异常命令。

注:命令中包含管道和`grep`命令,情况变的复杂,之前的处理都不再适用,这个场景下对管道的定义是各命令的处理具备连续性,若`grep`命令未筛选到结果,后续的命令处理均无意义,所以这类情况无论命令类型为正确/异常,最终状态定义为错误,状态码直接判断为1。

2) `grep` 命令在管道最后,继续判断管道除最后一个命令以外的状态码是否都为0:

a. 状态码存在非0,继续判断命令类型:

a) 正确命令:返回状态码1,表示这是一条报错的异常命令。

b) 异常命令:返回状态码1,表示这是一条报错的异常命令。

注:原理同上个注释,命令中包含管道,代表各命令的处理具备连续性,若中间部分命令报错,那后续的命令处理均无意义,所以这类情况无论命令类型为正确/异常,最终状态定义为错误,状态码直接判断为1。

b. 状态码均为0,继续判断命令最终状态码:

a) 状态码为0,继续判断命令类型:

i. 正确命令:返回状态码0,表示这是一条正常执行完成的命令。

ii. 异常命令:返回状态码1,表示这是一条报错的异常命令。

b) 状态码非0,继续判断命令类型:

i. 正确命令:返回状态码0,表示这是一条正常执行完成的命令。

ii. 异常命令:返回状态码1,表示这是一条报错的异常命令。

注:流程走到这一步说明,`grep`命令之前的所有命令均执行成功,所以此时执行`grep`命令不管是否筛选出关键字,命令都算是执行成功,这里必然是正确命令,不会出现异常命令的情况,如果命令类型为异常命令则为类型设置错误,需要修改。

④ 返回该条命令执行后重新定制的状态码与标准/错误输出内容。

⑤ 再次调用命令`set`,并配置参数`+o pipefail`,恢复默认状态,避免对后续用例造成影响。

⑥ 流程结束。

注:相对于之前的场景,该场景最为复杂,融合了所有变量,并对其做了新的定义。其中判断存在的`grep`命令是否在管道最后的意义在于,若使用了管道说明每一个命令都是有意义的,如果还没进行到最后一个命令就出现了报错,或者是`grep`命令未筛选到关键字的情况,那后续的所有命令都毫无意义,这里将这种情况定义为异常,在上面的注释中也有说明。

  1. 根据以上子流程中返回的全新状态码与输出内容,配合断言做进一步测试。

  2. 根据断言结果返回测试结果,若存在一条测试用例包含多条测试命令的情况,结合重新定义的状态码进行判断:

(1) 所有命令返回的状态码均为0,根据断言得出测试结果:

① 每一条用例中,所有断言均通过则用例通过,状态标记为`pass`。

② 每一条用例中,存在一个断言不通过,则用例不通过,状态标记为`fail`。

(2) 若有其中一条命令返回的状态码非0,则该条用例状态为`error`。

  1. 单条用例测试流程完毕。__

_ _

实验验证:

最后依然采用1.1.2章节的测试点进行实际验证,代码如下:

#! /bin/bash echo True > testfile1test1 () { ...... # 省略部分为定义的用例标题、编号等 quiet "cat testfile1 | grep True" "true" # 测试命令后传入参数"true/false"代表命令类型 Com1="${quiet_s}" # 重新定义的状态码 Com2="${quiet_r}" # 输出内容 Exp1="0" Exp2="True" assertEqual "${Com1}" "${Exp1}" assertIn "${Exp2}" "${Com2}"} test2() { ...... quiet "cat testfile1 | grep False" "false" Com1="${quiet_s}" Com2="${quiet_r}" Exp1="0" Exp2="" assertEqual "${Com1}" "${Exp1}" assertEqual "${Exp2}" "${Com2}"} test3 () { ...... quiet "cat testfile2 | grep False" "true" Com="${quiet_s}" Exp1="0" Exp2="" assertEqual "${Com1}" "${Exp1}" assertEqual "${Exp2}" "${Com2}" } test4 () { ...... quiet "cat testfile1 | sed 's/True/False/g'" "true" Com="${quiet_r}" Exp="False" assertEqual "${Com}" "${Exp}"} test5 () { ...... quiet "cat testfile2 | sed 's/True/False/g'" "true" Com="${quiet_r}" Exp="False" assertEqual "${Com}" "${Exp}"} rm testfile1

测试结果如下:

图6 实际验证结果

可以看到在1.1.2章节中测试无效的测试点,根据这套处理逻辑执行之后,能明显的识别出异常,规避了异常用例的测试结果为\`pass\`的问题。这里新增用例状态为\`error\`,快速的识别出了用例代码质量风险,代表用例中存在测试命令异常,该条用例需要重点检查,是测试环境变化导致,还是需求变更导致命令变化等。

2.3、本发明技术方案带来的有益效果

上述设计的执行Shell算法,运用在自动化测试框架中后:

(1) 测试结果更准确,降低质量风险。

(2) 能更好的识别测试命令的有效性,快速发现异常的命令,提升分析效率。

(3) 支持并扩展异常命令测试场景,满足更多的测试场景变化。

2.4、针对上述技术方案,是否还有替代方案同样能完成发明目的

3、本发明的技术关键点和欲保护点是什么

本发明的技术关键点和欲保护点是2.2章节中所阐述,设计的执行Shell测试命令的一整算法,可复用在任何Shell自动化测试框架中,达到2.3章节提到的效果:

(1) 主流程:结合事先定义的命令类型,配合解析命令后得出的四大场景,给予不同子流程算法。

(2) 子流程1:针对场景1设计的算法。

(3) 子流程2:针对场景2设计的算法。

(4) 子流程3:针对场景3设计的算法。

(5) 子流程4:针对场景4设计的算法。

(6) 结合主流程和子流程算法,得出全新的状态码用于对测试结果的支撑,达到2.3章节提到的目的。

4、附件:

参考文献(如专利/论文/标准等)

l Linux操作系统

l Shell壳层

l 自动化测试框架