当前位置: 首页 > 科技观察

利用BATS测试Bash脚本和库

时间:2023-03-18 22:04:45 科技观察

Bash自动化测试系统使Bash代码也可以通过Java、Ruby和Python开发人员使用的相同测试程序。使用Java、Ruby和Python等语言编写应用程序的软件开发人员拥有复杂的库,可以帮助他们随着时间的推移保持软件的完整性。他们可以创建测试,通过结构化环境中的一系列操作来运行应用程序,以确保其软件的所有方面都按预期工作。当这些测试在持续集成(CI)系统中自动化时,它们会更加强大,每次推送到源代码存储库时都会触发测试,并且在测试失败时会立即通知开发人员。这种快速反馈增加了开发人员对其应用程序功能完整性的信心。Bash自动测试系统(BATS)使编写Bash脚本和库的开发人员能够将Java、Ruby、Python和其他开发人员使用的相同约定应用于他们的Bash代码。安装BATSBATSGitHub页面包含安装说明。有两个BATS辅助库可提供更强大的断言或允许覆盖BATS使用的任何测试协议(TAP)输出格式。这些库可以安装在标准位置并被所有脚本引用。对于每组要测试的脚本或库,将完整版的BATS及其辅助库包含在Git存储库中会更方便。这可以通过git子模块系统来完成。以下命令会将BATS及其辅助库安装到Git存储库的测试目录中。gitsubmoduleinitgitsubmoduleaddhttps://github.com/sstephenson/batstest/libs/bats.com/ztombol/bats-supporttest/libs/bats-supportgitadd.gitcommit-m'installedbats'来克隆一个Git存储库并同时安装其子模块,在git克隆时使用--recurse-submodules标志。每个BATS测试脚本都必须由bats可执行文件执行。如果您将BATS安装到源代码存储库的test/libs目录中,则可以使用以下命令调用测试:./test/libs/bats/bin/bats或者,将以下内容添加到每个的开头BATS测试脚本:#!/usr/bin/env./test/libs/bats/bin/batsload'libs/bats-support/load'load'libs/bats-assert/load'然后执行命令chmod+x<测试脚本的路径>。这将a)使用安装在./test/libs/bats中的BATS使它们可执行,并且b)包含这些辅助库。BATS测试脚本通常存放在测试目录下,以待测脚本命名,扩展名为.bats。例如,测试bin/build的BATS脚本应称为test/build.bats。您还可以通过将正则表达式传递给BATS来运行全套BATS测试文件,例如./test/lib/bats/bin/batstest/*.bats。为BATS覆盖率组织库和脚本Bash脚本和库必须以一种有效的方式组织,以便将它们的内部工作暴露给BATS。一般来说,调用或执行时会运行许多命令的库函数和shell脚本不适合进行有效的BATS测试。比如build.sh就是一个典型的脚本,很多人都会写。本质上是一大堆代码。有些人甚至可能将这一堆代码放入库中的函数中。但是,不可能在BATS测试中运行一大堆代码并涵盖它在单个测试用例中可能遇到的所有类型的故障。测试这堆代码并具有足够覆盖率的唯一方法是将其分解为许多小的、可重用的、最重要的是,独立可测试的函数。向库中添加更多函数很简单。作为奖励,其中一些功能本身就非常有用。将库函数分解为许多较小的函数后,您可以在BATS测试中获取这些库的源代码,并像运行任何其他命令一样运行这些函数。Bash脚本还必须分解成脚本执行时应由脚本的主要部分调用的函数。此外,还有一个非常有用的技巧可以让您更轻松地使用BATS测试Bash脚本:将脚本主要部分中执行的所有代码移至名为run_main的函数中。然后,将以下内容添加到脚本的末尾:if[["${BASH_SOURCE[0]}"=="${0}"]]thenrun_mainfi这个额外的代码做了一些特别的事情。它使脚本在作为脚本执行时与使用引用源进入环境时表现不同。该技术通过引用和测试单个函数,使脚本能够以与库相同的方式进行测试。例如,这里是为了更好的BATS可测试性而重构的build.sh。编写和运行测试如上所述,BATS是一个符合TAP的测试框架,其语法和输出对于使用过其他符合TAP的测试套件(例如JUnit、RSpec或Jest)的用户来说将很熟悉。它的测试被组织成单独的测试脚本。测试脚本被组织成一个或多个描述性@test块,这些块描述了被测应用程序的单元。每个@test块将运行一系列准备测试环境的命令,运行要测试的命令,并对被测试命令的退出和输出进行断言。许多断言函数是通过bats、bats-assert和bats-support库导入的,这些库在BATS测试脚本开始时加载到环境中。下面是一个典型的BATS测试块:@test"requiresCI_COMMIT_REF_SLUGenvironmentvariable"{unsetCI_COMMIT_REF_SLUGassert_empty"${CI_COMMIT_REF_SLUG}"runsome_commandassert_failureassert_output--partial"CI_COMMIT_REF_SLUG"}(teardown)函数,BATS会在前后自动执行它们每个测试块运行。这使得创建环境变量、测试文件和执行一个或所有测试所需的其他操作成为可能,然后在每次测试运行后将它们拆除。Build.bats是我们新格式化的build.sh脚本的完整BATS测试。(此测试中的mock_docker命令在下面的模拟/标记部分进行了解释。)当测试脚本运行时,BATS使用exec(执行)将每个@test块作为单独的子进程运行。这使得在一个@test中导出环境变量甚至函数成为可能,而不会影响其他@tests或污染您当前的shell会话。测试运行的输出采用人类可以理解的标准格式,并且可以由TAP消费者以编程方式进行解析或操作。以下是CI_COMMIT_REF_SLUG测试块失败时的输出示例:?需要CI_COMMIT_REF_SLUG环境变量(来自文件test/libs/bats-assert/src/assert.bash中的函数“assert_output”,第231行,在测试文件test/ci_deploy中.bats,line26)`assert_output--partial"CI_COMMIT_REF_SLUG"'failed--outputdoesnotcontainsubstring--substring(1lines):CI_COMMIT_REF_SLUGoutput(3lines):./bin/deploy.sh:join_string_by:commandnotfoundocerrorCouldnotlogin--**Didnotdelete,astestfailed**1test,1failure以下是成功测试的输出:?需要CI_COMMIT_REF_SLUG环境变量AuxiliaryLibraries像任何shell脚本或库一样,BATS测试脚本可以包含辅助库,以在测试之间共享公共代码或增强其性能。这些辅助库,例如bats-assert和bats-support,甚至可以用BATS进行测试。该库可以放在与BATS脚本相同的测试目录中。如果test目录下的文件太多,也可以放在test/libs目录下。BATS提供加载函数,该函数采用相对于要测试的脚本(例如我们示例中的测试)的Bash文件路径,并调用该文件。该文件必须以后缀.bash结尾,但传递给加载函数的文件路径不能包含该后缀。build.bats加载bats-assert和bats-support库、一个小的helpers.bash库和docker_mock.bash库(如下所述),以下代码位于测试脚本的开头,解释器魔术线下方:load'libs/bats-support/load'load'libs/bats-assert/load'load'helpers'load'docker_mock'标记测试输入和模拟外部调用大多数Bash脚本和库在运行时执行函数和/或可执行文件。通常,它们被编程为根据这些函数或可执行文件的输出状态或输出(stdout、stderr)以特定方式运行。为了正确测试这些脚本,通常需要制作这些命令的伪造版本,这些命令旨在在特定测试期间以特定方式运行,称为“存根”。可能还需要监视被测试的程序,以确保它调用特定的命令,或者使用特定的参数调用特定的命令,这一过程称为“模拟”。有关更多信息,请查看适用于任何测试系统的RubyRSpec中模拟和标记的讨论。Bashshell提供了一些技巧,可在您的BATS测试脚本中用于模拟和标记。所有这些都需要使用带有-f标志的Bashexport命令来导出覆盖原始函数或可执行文件的函数。这必须在测试程序执行之前完成。下面是一个覆盖可执行命令cat的简单示例:functioncat(){echo"THISWOULDCAT${*}"}export-fcat此方法以相同的方式覆盖函数。如果测试需要覆盖被测脚本或库中的函数,那么在标记或模拟函数之前声明被测脚本或库很重要。否则,在声明脚本时,标记/模拟将被替换为原始功能。此外,在运行即将到来的测试命令之前确认标记/模拟。下面是一个build.bats的例子,它模拟了build.sh中描述的raise函数,以确保登录函数引发特定的错误消息:@test".loginraisesonocerror"{source${profile_script}functionraise(){echo"${1}提高了";}export-fraiserunloginassert_failureassert_output-p"Couldnotloginraise"}一般情况下,测试后不需要恢复标记/模拟功能,因为export(output)只在exec(当前@test块的执行)。但是,可以模拟/标记BATSassert函数内部使用的命令(例如cat、sed等)。在运行这些断言命令之前,必须取消(恢复)这些模拟/标记功能,否则它们将无法正常工作。下面是build.bats中的一个演示,该演示模拟sed,运行build_deployable函数并在运行任何断言之前恢复:@test".build_deployable打印信息,运行dockerbuildonamodifiedDockerfile.productionandpublish_imagewhenitsnotadry_run"{localexpected_dockerfile='Dockerfile.production'localapplication='application'localenvironment='environment'localexpected_original_base_image="${application}"localexpected_candidate_image="${application}-candidate:${environment}"localexpected_deployable_image="${application}:${environment}"source${profile_script}mock_dockerbuild--build-argOAUTH_CLIENT_ID--build-argOAUTH_REDIRECT--build-argDDS_API_BASE_URL-t"${expected_deployable_image}"-函数publish_image(){echo"publish_image${*}";}export-fpublish_imagefunctionsed(){echo"sed${*}">&2;echo"FROMapplication-candidate:environment";}导出-fsed运行build_deploy能够"${application}""${environment}"assert_successunsetsedassert_output--regexp"sed.*${expected_dockerfile}"assert_output-p"Building${expected_original_base_image}deployable${expected_deployable_image}FROM${expected_candidate_image}"assert_output-p"FROM${expected_candidate_image}管道"assert_output-p"build--build-argOAUTH_CLIENT_ID--build-argOAUTH_REDIRECT--build-argDDS_API_BASE_URL-t${expected_deployable_image}-"assert_output-p"publish_image${expectedim_ageployable"}有时同一个命令,比如foo,会在同一个被测函数中用不同的参数被多次调用。这些情况需要创建一组函数:mock_foo:将预期参数作为输入,并将它们持久化到TMP文件中foo:命令的模拟版本,该文件使用预期参数的持久化列表处理每次调用。它必须使用export-f导出。cleanup_foo:删除TMP文件,用于teardown功能。这可以被测试以确保@test块在被删除之前成功完成。由于此功能经常在不同的测试中重复使用,因此创建一个可以像任何其他库一样加载的帮助程序库是有意义的。docker_mock.bash就是一个很好的例子。它被加载到build.bats中,并在任何测试调用Docker可执行文件的函数的测试块中使用。使用docker_mock的典型测试块如下所示:@test“.publish_imagefailsifdockerpushfails”“${expected_image}”“${expected_publishable_image}”mock_dockerpush“${expected_publishable_image}”and_failrunpublish_image“${expected_image}”assert_failureassert_output-p“将${expected_image}标记为${expected_publishable_image}”assertp_output${expected_image}${expected_publishable_image}"assert_output-p"pushingimagetogitlabregistry"assert_output-p"push${expected_publishable_image}"}此测试建立了使用不同参数调用Docker两次的预期。当第二次调用Docker失败时,它运行测试命令,然后测试退出状态和调用Docker的预期结果。一方面,BATS使用mock_docker.bash引入${BATS_TMPDIR}环境变量,由BATS在测试开始时设置,以允许测试和辅助程序在标准位置创建和销毁TMP文件。如果测试失败,mock_docker.bash库不会删除其持久性模拟文件,但会打印出其位置以便查看和删除。您可能需要定期从该目录中清除旧的模拟文件。关于模拟/标记的一个注意事项:build.bats测试有意识地违反了关于测试声明的规则:不要模拟你不拥有的东西!该规定要求,非开发者编写的测试命令,如docker、cat、sed等,应封装在自己的库中,并在使用其脚本的测试中进行模拟。然后应该在不模拟外部命令的情况下测试包装器库。这是一个很好的建议,忽视它是要付出代价的。如果DockerCLIAPI更改,测试脚本不会检测到此更改,导致错误内容不会显示,直到使用新版本的Docker在生产中运行测试的build.sh脚本。测试开发人员必须确定他们希望遵守该标准的严格程度,但他们应该了解所涉及的权衡取舍。小结在任何软件开发项目中引入测试机制都会在以下方面做出权衡:a.增加开发和维护代码以及测试所需的时间和组织,以及b.提高开发人员对应用程序在其整个生命周期中的完整性的信心。信心。测试制度可能不适用于所有脚本和库。一般来说,满足以下一项或多项标准的脚本和库有资格进行BATS测试:修改它们其功能可供其他人使用一旦决定将测试规则应用于一个或多个Bash脚本或库,BATS将提供在其他软件开发环境中可用的综合测试功能。致谢:感谢DarrinMann向我介绍BATS测试。