Jenkins Shared Libraries是一种扩展Jenkins Pipeline的技术,通过编写Shared Libraries可以实现自定义的Steps,将流水线逻辑中重复或共通的部分进行抽象和封装。 实践中每个DevOps团队都应该通过维护一个或多个Shared Libraries项目再结合第三方的Jenkins插件定制团队自己的Jenkins流水线。

1.Shared Libraries项目初始化

Shared Libraries项目的工程化首先要选型构建工具,这里选择gradle:

使用gradle初始化一个空项目:

 1mkdir shared-lib
 2
 3gradle init
 4
 5Select type of project to generate:
 6  1: basic
 7  2: cpp-application
 8  3: cpp-library
 9  4: groovy-application
10  5: groovy-library
11  6: java-application
12  7: java-library
13  8: kotlin-application
14  9: kotlin-library
15  10: scala-library
16Enter selection (default: basic) [1..10] 1
17
18Select build script DSL:
19  1: groovy
20  2: kotlin
21Enter selection (default: groovy) [1..2] 1
22
23Project name (default: shared-lib): 

注意这里选择的是1: basic的项目模板,这是因为jenkins shared lib项目的目录结构与常见maven工程结构不同,所以我们初始化一个空白的gradle工程,后边手动配置出项目的具体结构。

根据官方文档Extending with Shared Libraries 中描述Shared Libraries的目录结构:

 1(root)
 2+- src                     # Groovy source files
 3|   +- org
 4|       +- foo
 5|           +- Bar.groovy  # for org.foo.Bar class
 6+- vars
 7|   +- foo.groovy          # for global 'foo' variable
 8|   +- foo.txt             # help for 'foo' variable
 9+- resources               # resource files (external libraries only)
10|   +- org
11|       +- foo
12|           +- bar.json    # static helper data for org.foo.Bar

一个shared libraries项目的标准代码结构由三部分组成:

  • src目录中是标准的Groovy代码
  • vars目录中是依赖于Jenkins运行环境的Groovy脚本
  • resources目录中是静态资源文件

下面按照shared libraries项目的标准结构,在前面使用gradle创建的空白项目shared-lib创建对应目录src, var, test, resources(这里比标准目录结构多创建了一个test目录将用于存放对shared libraries的单元测试用例):

 1.
 2├── build.gradle
 3├── gradle
 4│   └── wrapper
 5│       ├── gradle-wrapper.jar
 6│       └── gradle-wrapper.properties
 7├── gradlew
 8├── gradlew.bat
 9├── resources
10├── settings.gradle
11├── shared-lib.iml
12├── src
13├── test
14└── vars

因为shared-lib项目并不遵循标准的maven目录结构,所以需要在build.gradle中针对源码目录做出配置,下面修改build.gradle项目:

 1plugins {
 2    id 'groovy'
 3}
 4
 5sourceSets {
 6    main {
 7        groovy {
 8            srcDir 'src'
 9            srcDir 'vars'
10        }
11        resources {
12            srcDir 'resources'
13        }
14    }
15    test {
16        groovy {
17            srcDir 'test'
18        }
19    }
20}
21
22repositories {
23    mavenCentral()
24}
25
26targetCompatibility = 1.8
27sourceCompatibility = 1.8
28
29configurations {
30    ivy
31}
32
33tasks.withType(GroovyCompile) {
34    groovyClasspath += configurations.ivy
35}
36
37dependencies {
38    implementation 'org.codehaus.groovy:groovy-all:2.5.7'
39    implementation 'com.cloudbees:groovy-cps:1.29'
40    def ivyDep = 'org.apache.ivy:ivy:2.4.0'
41    ivy ivyDep
42    implementation ivyDep
43
44    testImplementation 'com.lesfurets:jenkins-pipeline-unit:1.1'
45    testImplementation 'junit:junit:4.12'
46    // spock
47    testImplementation 'org.spockframework:spock-core:1.3-groovy-2.5'
48    testImplementation 'org.objenesis:objenesis:3.0.1'
49    testImplementation 'cglib:cglib-nodep:3.2.12'
50}

上面的build.gradle定义了项目具体的源码结构,同时引入了groovy依赖以及用于单元测试的jenkins-pipeline-unit。 同时还引入了JUnit4和Spock的依赖,可以编写Java JUnit或Groovy Spock两种风格的单元测试。

2.使用JenkinsPipelineUnit编写单元测试

下面通过一个简单例子介绍一下JenkinsPipelineUnit单元测试框架的使用。

在vars中创建一个简单log.groovy脚本:

1def info(message) {
2    echo "INFO: ${message}"
3}
4
5def warn(message) {
6    echo "WARNING: ${message}"
7}

上面的脚本定义了两个全局方法,正常我们在Jenkinsfile中是这样使用:

1@Library('shared-lib') _
2
3log.info 'Starting'
4log.warn 'Nothing to do!'

如果需要对Shared Libraries进行集成测试,可以编写Jenkinsfile放到Jenkins中创建Job去运行。 但实际开发Shared Libraries的过程中我们需要通过大量的单元测试用例去完成单元测试,并达成一定的测试覆盖率。

如果你对Groovy和Spock还不太熟悉,推荐编写JUnit风格的单元测试,因为Groovy是兼容Java的,可以渐进式的学习,写单元测试只是为了达到测试Jenkins Shared Library的目的,选择适合自己的工具即可。 不过还是强烈推荐使用Spock编写更简洁的测试代码。

2.1 使用JUnit编写单元测试

下面编写log.groovy的单元测试,创建logTest.groovy:

 1import com.lesfurets.jenkins.unit.BasePipelineTest
 2import org.junit.Before
 3import org.junit.Test
 4import static org.junit.Assert.*;
 5import static org.hamcrest.CoreMatchers.*;
 6
 7class logTest extends  BasePipelineTest {
 8
 9    def log
10
11    @Before
12    void setUp() {
13        super.setUp()
14        log = loadScript("vars/log.groovy")
15    }
16
17    @Test
18    void logInfo() {
19        log.info("info message")
20        assertThat(helper.methodCallCount("info"), is(1L))
21    }
22
23    @Test
24    void logWarn() {
25        log.warn("warn message")
26        assertThat(helper.methodCallCount("warn"), is(1L))
27        printCallStack()
28    }
29
30}

通过执行gradle test命令运行单元测试。

2.2 使用Spock编写单元测试

由于JenkinsPipelineUnit并不支持Spock Specification,所以我们编写一个PipelineSpecification集成自spock.lang.Specification,这样后边编写Spock测试类时都继承自PipelineSpecification即可。

 1import com.lesfurets.jenkins.unit.BasePipelineTest
 2import spock.lang.Specification
 3
 4class PipelineSpecification extends  Specification {
 5
 6    @Delegate BasePipelineTest basePipelineTest
 7
 8    def setup() {
 9        basePipelineTest = new BasePipelineTest() {}
10        basePipelineTest.setUp()
11    }
12
13}

注意上面使用@Delegatecom.lesfurets.jenkins.unit.BasePipelineTest的属性和方法外推到PipelineSpecification,所以在PipelineSpecification的子类中可以直接使用这些属性和方法。

下面是使用Spock编写的logTest.groovy测试:

 1class logGroovyTest extends PipelineSpecification {
 2
 3    def log
 4
 5    def setup() {
 6        log = loadScript("vars/log.groovy")
 7    }
 8
 9    def 'log info'() {
10        when:
11        log.info("info message")
12        then:
13        helper.methodCallCount("info") == 1
14        helper.callStack.find {call -> call.methodName == 'info'}.args[0] == 'info message'
15    }
16
17    def 'log warn'() {
18        when:
19        log.warn('warn message')
20        then:
21        helper.methodCallCount('warn') == 1
22        helper.callStack.find {call -> call.methodName == 'warn'}.args[0] == 'warn message'
23    }
24}

参考