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

1.Shared Libraries项目初始化

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mkdir shared-lib

gradle init

Select type of project to generate:
  1: basic
  2: cpp-application
  3: cpp-library
  4: groovy-application
  5: groovy-library
  6: java-application
  7: java-library
  8: kotlin-application
  9: kotlin-library
  10: scala-library
Enter selection (default: basic) [1..10] 1

Select build script DSL:
  1: groovy
  2: kotlin
Enter selection (default: groovy) [1..2] 1

Project name (default: shared-lib): 

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(root)
+- src                     # Groovy source files
|   +- org
|       +- foo
|           +- Bar.groovy  # for org.foo.Bar class
+- vars
|   +- foo.groovy          # for global 'foo' variable
|   +- foo.txt             # help for 'foo' variable
+- resources               # resource files (external libraries only)
|   +- org
|       +- foo
|           +- 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
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── resources
├── settings.gradle
├── shared-lib.iml
├── src
├── test
└── vars

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
plugins {
    id 'groovy'
}

sourceSets {
    main {
        groovy {
            srcDir 'src'
            srcDir 'vars'
        }
        resources {
            srcDir 'resources'
        }
    }
    test {
        groovy {
            srcDir 'test'
        }
    }
}

repositories {
    mavenCentral()
}

targetCompatibility = 1.8
sourceCompatibility = 1.8

configurations {
    ivy
}

tasks.withType(GroovyCompile) {
    groovyClasspath += configurations.ivy
}

dependencies {
    implementation 'org.codehaus.groovy:groovy-all:2.5.7'
    implementation 'com.cloudbees:groovy-cps:1.29'
    def ivyDep = 'org.apache.ivy:ivy:2.4.0'
    ivy ivyDep
    implementation ivyDep

    testImplementation 'com.lesfurets:jenkins-pipeline-unit:1.1'
    testImplementation 'junit:junit:4.12'
    // spock
    testImplementation 'org.spockframework:spock-core:1.3-groovy-2.5'
    testImplementation 'org.objenesis:objenesis:3.0.1'
    testImplementation 'cglib:cglib-nodep:3.2.12'
}

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

2.使用JenkinsPipelineUnit编写单元测试

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

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

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

def warn(message) {
    echo "WARNING: ${message}"
}

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

1
2
3
4
@Library('shared-lib') _

log.info 'Starting'
log.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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import com.lesfurets.jenkins.unit.BasePipelineTest
import org.junit.Before
import org.junit.Test
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

class logTest extends  BasePipelineTest {

    def log

    @Before
    void setUp() {
        super.setUp()
        log = loadScript("vars/log.groovy")
    }

    @Test
    void logInfo() {
        log.info("info message")
        assertThat(helper.methodCallCount("info"), is(1L))
    }

    @Test
    void logWarn() {
        log.warn("warn message")
        assertThat(helper.methodCallCount("warn"), is(1L))
        printCallStack()
    }

}

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

2.2 使用Spock编写单元测试

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import com.lesfurets.jenkins.unit.BasePipelineTest
import spock.lang.Specification

class PipelineSpecification extends  Specification {

    @Delegate BasePipelineTest basePipelineTest

    def setup() {
        basePipelineTest = new BasePipelineTest() {}
        basePipelineTest.setUp()
    }

}

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class logGroovyTest extends PipelineSpecification {

    def log

    def setup() {
        log = loadScript("vars/log.groovy")
    }

    def 'log info'() {
        when:
        log.info("info message")
        then:
        helper.methodCallCount("info") == 1
        helper.callStack.find {call -> call.methodName == 'info'}.args[0] == 'info message'
    }

    def 'log warn'() {
        when:
        log.warn('warn message')
        then:
        helper.methodCallCount('warn') == 1
        helper.callStack.find {call -> call.methodName == 'warn'}.args[0] == 'warn message'
    }
}

参考