JenkinsPipelineUnit, 单元测试Jenkins管道框架

分享于 

14分钟阅读

GitHub

  繁體 雙語
Framework for unit testing Jenkins pipelines
  • 源代码名称:JenkinsPipelineUnit
  • 源代码网址:http://www.github.com/jenkinsci/JenkinsPipelineUnit
  • JenkinsPipelineUnit源代码文档
  • JenkinsPipelineUnit源代码下载
  • Git URL:
    git://www.github.com/jenkinsci/JenkinsPipelineUnit.git
    Git Clone代码到本地:
    git clone http://www.github.com/jenkinsci/JenkinsPipelineUnit
    Subversion代码到本地:
    $ svn co --depth empty http://www.github.com/jenkinsci/JenkinsPipelineUnit
    Checked out revision 1.
    $ cd repo
    $ svn up trunk
    
    Jenkins管道单元测试框架

    Jenkins流水线单元是单元测试Jenkins管道的测试框架,在 Groovy流水线DSL中编写。

    Build StatusBuild status

    如果你使用Jenkins作为你的CI权威,你喜欢写 pipeline-as-code,你已经知道流水线代码是非常强大的,但却能很复杂。

    通过提供管道代码的模拟执行,这个测试框架允许你对管道代码的配置和条件逻辑编写单元测试。 你可以模拟Jenkins命令,作业配置,查看整个执行的stacktrace,甚至跟踪回归。

    用法

    作为测试依赖项添加到你的项目中

    Maven:

     <dependency>
     <groupId>com.lesfurets</groupId>
     <artifactId>jenkins-pipeline-unit</artifactId>
     <version>1.0</version>
     <scope>test</scope>
     </dependency>

    Gradle:

    testCompile group:'com.lesfurets', name:'jenkins-pipeline-unit', version:'1.0'

    开始编写测试

    你可以使用你喜欢的测试框架在Groovy或者 Java 8中编写测试。 最简单的入口点是扩展抽象类 BasePipelineTest,它使用JUnit初始化框架。

    假设你编写了这个出色的管道脚本,它生成并测试你的项目:

    defexecute() {
     node() {
     def utils = load "src/test/jenkins/lib/utils.jenkins"String revision = stage('Checkout') {
     checkout scm
     return utils.currentRevision()
     }
     gitlabBuilds(builds: ["build", "test"]) {
     stage("build") {
     gitlabCommitStatus("build") {
     sh "mvn clean package -DskipTests -DgitRevision=$revision" }
     }
     stage("test") {
     gitlabCommitStatus("test") {
     sh "mvn verify -DgitRevision=$revision" }
     }
     }
     }
    }returnthis

    现在使用Jenkins管线单元,如果它做了这项工作,你可以 单元测试:

    importcom.lesfurets.jenkins.unit.BasePipelineTestclassTestExampleJobextendsBasePipelineTest {
     //...@Testvoidshould_execute_without_errors() throwsException {
     def script = loadScript("job/exampleJob.jenkins")
     script.execute()
     printCallStack()
     }
    }

    这里测试将打印执行的调用堆栈:

    
     exampleJob.run()
    
    
     exampleJob.execute()
    
    
     exampleJob.node(groovy.lang.Closure)
    
    
     exampleJob.load(src/test/jenkins/lib/utils.jenkins)
    
    
     utils.run()
    
    
     exampleJob.stage(Checkout, groovy.lang.Closure)
    
    
     exampleJob.checkout({$class=GitSCM, branches=[{name=feature_test}], doGenerateSubmoduleConfigurations=false, extensions=[], submoduleCfg=[], userRemoteConfigs=[{credentialsId=gitlab_git_ssh, url=github.com/lesfurets/JenkinsPipelineUnit.git}]})
    
    
     utils.currentRevision()
    
    
     utils.sh({returnStdout=true, script=git rev-parse HEAD})
    
    
     exampleJob.gitlabBuilds({builds=[build, test]}, groovy.lang.Closure)
    
    
     exampleJob.stage(build, groovy.lang.Closure)
    
    
     exampleJob.gitlabCommitStatus(build, groovy.lang.Closure)
    
    
     exampleJob.sh(mvn clean package -DskipTests -DgitRevision=bcc19744)
    
    
     exampleJob.stage(test, groovy.lang.Closure)
    
    
     exampleJob.gitlabCommitStatus(test, groovy.lang.Closure)
    
    
     exampleJob.sh(mvn verify -DgitRevision=bcc19744)
    
    
    
    

    模拟Jenkins变量

    你可以同时定义 环境变量 和作业执行参数。

    @Override@BeforevoidsetUp() throwsException {
     super.setUp()
     // Assigns false to a job parameter ENABLE_TEST_STAGE  binding.setVariable('ENABLE_TEST_STAGE', 'false')
     // Defines the previous execution status binding.getVariable('currentBuild').previousBuild = [result: 'UNSTABLE']
     }

    测试 helper 已经提供了基本变量,比如一个非常简单的currentBuild定义。 你可以根据需要重新定义它们。

    模拟Jenkins命令

    你可以对模拟的,命令进行 register 捕获,这可以能或者不返回结果。

    @Override@BeforevoidsetUp() throwsException {
     super.setUp()
     helper.registerAllowedMethod("sh", [Map.class], {c->"bcc19744"})
     helper.registerAllowedMethod("timeout", [Map.class, Closure.class], null)
     helper.registerAllowedMethod("timestamps", [], { println'Printing timestamp' })
     helper.registerAllowedMethod(method("readFile", String.class), { file->returnFiles.contentOf(newFile(file), Charset.forName("UTF-8"))
     })
     }

    测试 helper 已经包含一些模拟,但是列表远远不够完整。 如果你想重写这些模拟并添加其他的方法,你需要使用 register 允许的方法。 注意,你需要提供方法签名和回调( 闭合或者 lambda ),以便允许方法。 无法识别的任何方法调用都将引发异常。

    你可以查看 BasePipelineTest 类以获得允许的方法的简短列表。

    一些棘手的方法,如 loadparallel,在 helper 中直接实现。 如果要重写这些属性,请确保扩展了 PipelineTestHelper 类。

    分析模拟执行

    helper 注册每个方法调用,以提供模拟执行的stacktrace。

    @Testvoidshould_execute_without_errors() throwsException {
     loadScript("Jenkinsfile")
     assertThat(helper.callStack.findAll { call-> call.methodName =="sh" }.any { call-> callArgsToString(call).contains("mvn verify")
     }).isTrue()
     assertJobStatusSuccess()
    }

    这将检查 mvn verify 在执行作业期间是否已经被调用。

    将callstacks与以前的实现进行比较

    callstacks的另一个用途是检查管道的执行,以便可能的回归。 你有一个专用方法,可以在扩展 BaseRegressionTest 时调用该方法:

    @TestvoidtestNonReg() throwsException {
     def script = loadScript("job/exampleJob.jenkins")
     script.execute()
     super.testNonRegression('example')
     }

    这将将作业的当前堆栈与文本调用程序引用文件中的当前堆栈进行比较。 要使用新的调用堆栈更新这里文件,只需在运行测试时设置这里JVM参数: -Dpipeline.stack.write=true

    然后,你可以在SCM中提交这里更改,以检查更改。

    配置

    抽象类 BasePipelineTest 使用有用的约定配置 helper:

    • 它在 root ( ./. ) 和 src/main/jenkins 路径中查找项目中的管道脚本。
    • Jenkins管道允许你使用 load 命令从父脚本加载其他脚本。 但是 load 将完整路径 relative 接受到项目 root。 测试 helper 模拟成功地加载了 load 命令来加载脚本。 要使 relative 路径有效,需要配置管道脚本所在的项目路径,该路径默认为 .
    • 管道脚本扩展,默认为 jenkins ( 匹配任何 *.jenkins 文件)

    覆盖这些默认值很简单:

    classTestExampleJobextendsBasePipelineTest {
     @Override@BeforevoidsetUp() throwsException {
     helper.baseScriptRoot ='jenkinsJobs' helper.roots +='src/main/groovy' helper.extension ='pipeline'super.setUp()
     }
    }

    对于这样的项目结构,这将工作得很好:

    
     jenkinsJobs
    
    
     └── src
    
    
     ├── main
    
    
     │ └── groovy
    
    
     │ └── ExampleJob.pipeline
    
    
     └── test
    
    
     └── groovy
    
    
     └── TestExampleJob.groovy
    
    
    
    

    测试共享库

    借助共享库,Jenkins允许你在组织的不同存储库上共享管道上的公共代码。 通过Jenkins的设置界面配置共享库,并在脚本中使用 @Library 注释导入。

    使用外部库测试管道脚本并不简单,因为共享库代码是在另一个存储库中检查的。 JenkinsPipelineUnit允许你根据这些库测试共享库和管道。

    下面是一个使用共享库的示例管道:

    @Library('commons')importnet.courtanet.jenkins.UtilssayHello 'World'node() {
     stage ('Checkout') {
     def utils =newUtils()
     checkout "${utils.gitTools()}" }
     stage ('Build') {
     sh './gradlew build' }
     stage ('Post Build') {
     String json = libraryResource 'net/courtanet/jenkins/request.json' sh "curl -H 'Content-Type: application/json' -X POST -d '$json' ${acme.url}" }
    }

    这里管道使用名为 commons的共享库。 现在让我们来测试一下:

    String clonePath ='path/to/clone'def library = library()
    . name('commons')
    . retriever(gitSource('git@gitlab.admin.courtanet.net:devteam/lesfurets-jenkins-shared.git'))
    . targetPath(clonePath)
    . defaultVersion("master")
    . allowOverride(true)
    . implicit(false)
    . build()
     helper.registerSharedLibrary(library)
     loadScript("job/library/exampleJob.jenkins")
     printCallStack()

    注意我们如何定义共享库并将它的注册到 helper。 库定义通过一个流畅的API完成,它允许你设置与全局管道库相同的配置。

    retrievertargetPath 字段告诉框架如何获取库的源代码,其中本地路径是。 这个框架有两个天真但有用的猎犬,gitSourcelocalSource。 你可以通过实现 SourceRetriever 接口编写自己的检索器。

    请注意,属性 defaultVersionallowOverrideimplicit 是可选的,默认值为 mastertruefalse

    现在执行这个测试,框架将从Git库获取源代码,加载类。脚本。全局变量和库中找到的资源。 执行的调用堆栈将如下所示:

    
    Loading shared library commons with version master
    
    
    libraryJob.run()
    
    
     libraryJob.sayHello(World)
    
    
     sayHello.echo(Hello, World.)
    
    
     libraryJob.node(groovy.lang.Closure)
    
    
     libraryJob.stage(Checkout, groovy.lang.Closure)
    
    
     Utils.gitTools()
    
    
     libraryJob.checkout({branch=master})
    
    
     libraryJob.stage(Build, groovy.lang.Closure)
    
    
     libraryJob.sh(./gradlew build)
    
    
     libraryJob.stage(Post Build, groovy.lang.Closure)
    
    
     libraryJob.libraryResource(net/courtanet/jenkins/request.json)
    
    
     libraryJob.sh(curl -H 'Content-Type: application/json' -X POST -d '{"name" :"Ben"}' http://acme.com)
    
    
    
    

    上的

    如果你已经摆弄过Jenkins流水线的DSL,你在Jenkins执行过程中会遇到奇怪的错误。 this在Groovy中并没有直接执行流水线,而是将管道代码转换为中间格式,以便在延续样式中运行Groovy代码( 在延续样式中)。

    由于沙沙的安全原因,一般是由于沙沙沙的应用而造成的,部分原因是由于可以串行化的。

    Jenkins要求每个执行步骤都可以序列化整个脚本上下文,以便停止和恢复作业执行。 为了模拟这个方面,帮助器的das版本将脚本转换为cer格式,并检查脚本上下文是否可以序列化。

    要使用这个基于的实验特性,可以使用抽象类 BasePipelineTestCPS 而不是 BasePipelineTest。 你可能会看到 helper 所注册的调用堆栈中的一些更改。 注意,用于测试的序列化与Jenkins所使用的不同。 你可能会发现这个水平有些不一致。

    JenkinsPipelineUnit旨在帮助devops代码和测试Jenkins管道缩短开发周期。 它解决了 JENKINS-33925中的一些需求跟踪。 如果你愿意的话,请不要犹豫讨论问题并打开请求请求。


      framework  test  测试  PIP  管道  
    相关文章