Jenkins Shared Libraries是一种扩展Jenkins Pipeline的技术,通过编写Shared Libraries可以实现自定义的Steps,将流水线逻辑中重复或共通的部分进行抽象和封装。 实践中每个DevOps团队都应该通过维护一个或多个Shared Libraries项目再结合第三方的Jenkins插件定制团队自己的Jenkins流水线。
1.Shared Libraries项目初始化
Shared Libraries项目的工程化首先要选型构建工具,这里选择gradle:
使用gradle初始化一个空项目:
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的目录结构:
(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的单元测试用例):
.
├── 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项目:
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脚本:
def info(message) {
echo "INFO: ${message}"
}
def warn(message) {
echo "WARNING: ${message}"
}上面的脚本定义了两个全局方法,正常我们在Jenkinsfile中是这样使用:
@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:
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即可。
import com.lesfurets.jenkins.unit.BasePipelineTest
import spock.lang.Specification
class PipelineSpecification extends Specification {
@Delegate BasePipelineTest basePipelineTest
def setup() {
basePipelineTest = new BasePipelineTest() {}
basePipelineTest.setUp()
}
}注意上面使用@Delegate将com.lesfurets.jenkins.unit.BasePipelineTest的属性和方法外推到PipelineSpecification,所以在PipelineSpecification的子类中可以直接使用这些属性和方法。
下面是使用Spock编写的logTest.groovy测试:
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'
}
}