如何:设置可单元测试的 Jenkins 共享管道库
(最初发布于loglevel-blog.com)
在这篇博文中,我将尝试解释如何为 Jenkins 设置和开发一个共享管道库,该库易于操作,并且可以使用 JUnit 和 Mockito 进行单元测试。
注意:这篇博文有点长,涉及很多主题,但并未进行详尽的解释。如果您不想跟着冗长的教程学习,可以查看GitHub上的完整示例库。此外,如果您有任何疑问,或者有任何关于我可以改进的地方的反馈,请留言,我会尽快回复您 ;) 另外,如果您对 Jenkins 共享库完全不熟悉,建议您先阅读官方文档。
我们走吧!
基本开发设置
首先,让我们创建一个新的 IntelliJ IDEA 项目。我建议使用 IntelliJ IDEA 进行 Jenkins 共享流水线开发,因为它是我所知道的唯一一款完美支持 Java 和 Groovy 并支持 Gradle 的 IDE。如果您还没有安装,可以在这里下载适用于 Windows、Linux 或 MacOS 的版本。另外,请确保已安装 Java 开发工具包 (Java Development Kit),可在此处获取。
当一切准备就绪后,启动 IntelliJ,创建一个新项目,选择 Gradle 并确保选中 Groovy 上的复选框。
接下来,输入 GroupId 和 ArtifactId。
忽略下一个窗口(默认即可),单击“下一步”,输入项目名称,然后单击“完成”。
IntelliJ 应该会启动你的新项目。你的项目中的文件夹结构应该类似于以下内容。
对于通常的 Java/Groovy 项目来说这很酷,但为了我们的目的,我们必须稍微改变一下,因为 Jenkins 要求这样的项目结构:
(root)
+- src # Groovy source files
| +- org
| +- somecompany
| +- 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
| +- somecompany
| +- bar.json # static helper data for org.foo.Bar
所以:
- 将文件夹添加
vars
到项目根文件夹 - 将文件夹添加
resource
到项目根文件夹 - 删除里面的所有文件/文件夹
src
并添加一个新包,例如org.somecompany
- 编辑
build.gradle
文件:
group 'somecompany'
version '1.0-SNAPSHOT'
apply plugin: 'groovy'
apply plugin: 'java'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.3.11'
testCompile group: 'junit', name: 'junit', version: '4.12'
testCompile "org.mockito:mockito-core:2.+"
}
sourceSets {
main {
java {
srcDirs = []
}
groovy {
// all code files will be in either of the folders
srcDirs = ['src', 'vars']
}
}
test {
java {
srcDir 'test'
}
}
}
保存后,将您的更改导入到 Gradle 项目:
此时,我们的项目结构已正确,可以被 Jenkins 用作共享库。但是,正如您可能在上面的代码片段中看到的,我们还添加了一个名为 的单元测试源目录test
。现在需要在项目的根目录下创建此文件夹,并org.somecompany
像之前一样添加一个包src
。最终结构应如下所示。
太棒了,现在是时候实现我们的共享库了!
一般方法
首先简单介绍一下我们如何构建我们的库以及为什么我们这样做:
var
我们将尽可能简化“自定义”步骤,不包含任何实际逻辑。相反,我们会创建一些类(在 内部src
)来完成所有工作。- 我们创建一个接口,它声明了所有必需的 Jenkins 步骤(
sh
、bat
、error
等)的方法。类仅通过此接口调用步骤。 - 我们为您的类编写单元测试,就像您通常使用 JUnit 和 Mockito 一样。
这样我们就可以:
- 无需 Jenkins 即可编译并执行我们的库/单元测试
- 测试我们的课程是否按预期工作
- 测试是否使用正确的参数调用 Jenkins 步骤
- 当 Jenkins 步骤失败时测试我们的代码的行为
- 通过 Jenkins 本身构建、测试、运行指标并部署 Jenkins 管道库
现在让我们开始吧。
步进访问接口
首先,我们将在内部创建接口,所有org.somecompany
类都将使用该接口来访问常规 Jenkins 步骤,例如或。sh
error
package org.somecompany
interface IStepExecutor {
int sh(String command)
void error(String message)
// add more methods for respective steps if needed
}
这个接口很简洁,因为它可以在我们的单元测试中进行模拟。这样,我们的类就独立于 Jenkins 本身了。现在,让我们添加一个将在vars
Groovy 脚本中使用的实现:
package org.somecompany
class StepExecutor implements IStepExecutor {
// this will be provided by the vars script and
// let's us access Jenkins steps
private _steps
StepExecutor(steps) {
this._steps = steps
}
@Override
int sh(String command) {
this._steps.sh returnStatus: true, script: "${command}"
}
@Override
void error(String message) {
this._steps.error(message)
}
}
添加基本依赖注入
因为我们不想在单元测试中使用上述实现,所以我们将设置一些基本的依赖注入,以便在单元测试期间将上述实现与模拟替换。如果您不熟悉依赖注入,您可能需要阅读相关内容,因为在这里解释它超出了范围,但您可能可以直接复制粘贴本章中的代码并继续操作。
因此,首先我们创建org.somecompany.ioc
包并添加一个IContext
接口:
package org.somecompany.ioc
import org.somecompany.IStepExecutor
interface IContext {
IStepExecutor getStepExecutor()
}
同样,这个接口将在单元测试中被模拟。但为了库的正常执行,我们仍然需要一个默认实现:
package org.somecompany.ioc
import org.somecompany.IStepExecutor
import org.somecompany.StepExecutor
class DefaultContext implements IContext, Serializable {
// the same as in the StepExecutor class
private _steps
DefaultContext(steps) {
this._steps = steps
}
@Override
IStepExecutor getStepExecutor() {
return new StepExecutor(this._steps)
}
}
为了完成我们的基本依赖注入设置,让我们添加一个“上下文注册表”,用于存储当前上下文(在正常执行期间和单元测试期间的DefaultContext
Mockito 模拟):IContext
package org.somecompany.ioc
class ContextRegistry implements Serializable {
private static IContext _context
static void registerContext(IContext context) {
_context = context
}
static void registerDefaultContext(Object steps) {
_context = new DefaultContext(steps)
}
static IContext getContext() {
return _context
}
}
就这样!现在我们可以自由地在里面编写可测试的 Jenkins 步骤了vars
。
编写自定义 Jenkins 步骤
假设我们这里举个例子,想要在库中添加一个步骤,调用 .NET 构建工具“MSBuild”来构建 .NET 项目。为此,我们首先ex_msbuild.groovy
在vars
文件夹中添加一个 Groovy 脚本,该脚本的名称与我们要实现的自定义步骤的名称相同。由于脚本的名称已更改,因此ex_msbuild.groovy
稍后可以在 Jenkinsfile 中调用该步骤ex_mbsbuild
。现在,请在脚本中添加以下内容:
def call(String solutionPath) {
// TODO
}
按照我们的总体思路,我们希望ex_msbuild
脚本尽可能简洁,并将所有工作都放在一个可单元测试的类中完成。因此,让我们MsBuild
在新包中创建一个新类org.somecompany.build
:
package org.somecompany.build
import org.somecompany.IStepExecutor
import org.somecompany.ioc.ContextRegistry
class MsBuild implements Serializable {
private String _solutionPath
MsBuild(String solutionPath) {
_solutionPath = solutionPath
}
void build() {
IStepExecutor steps = ContextRegistry.getContext().getStepExecutor()
int returnStatus = steps.sh("echo \"building ${this._solutionPath}...\"")
if (returnStatus != 0) {
steps.error("Some error")
}
}
}
如你所见,我们在类中同时使用了sh
和error
步骤,但不是直接使用它们,而是使用ContextRegistry
来获取 的实例,IStepExecutor
并用它来调用 Jenkins 步骤。这样,当我们稍后需要对该方法进行单元测试时,就可以交换上下文build()
。
现在我们可以完成我们的ex_msbuild
脚本了:
import org.somecompany.build.MsBuild
import org.somecompany.ioc.ContextRegistry
def call(String solutionPath) {
ContextRegistry.registerDefaultContext(this)
def msbuild = new MsBuild(solutionPath)
msbuild.build()
}
首先,我们使用上下文注册表设置上下文。由于我们不在单元测试中,因此我们使用默认上下文。this
我们传入的registerDefaultContext()
将被保存在DefaultContext
其内部的私有_steps
变量中,并用于访问 Jenkins 步骤。注册上下文后,我们就可以实例化我们的MsBuild
类并调用build()
执行所有工作的方法了。
很好,我们的vars
脚本完成了。现在我们只需要为我们的MsBuild
类编写一些单元测试。
添加单元测试
此时,编写单元测试应该一切如常。我们MsBuildTest
在 test 文件夹中创建一个新的测试类,并包含 package org.somecompany.build
。每次测试之前,我们都使用 Mockito 模拟IContext
和IStepExecutor
接口,并注册模拟的上下文。然后,我们可以在测试中简单地创建一个新MsBuild
实例,并验证方法的行为build()
。完整的测试类包含两个示例测试:
package org.somecompany.build;
import org.somecompany.IStepExecutor;
import org.somecompany.ioc.ContextRegistry;
import org.somecompany.ioc.IContext;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.Mockito.*;
/**
* Example test class
*/
public class MsBuildTest {
private IContext _context;
private IStepExecutor _steps;
@Before
public void setup() {
_context = mock(IContext.class);
_steps = mock(IStepExecutor.class);
when(_context.getStepExecutor()).thenReturn(_steps);
ContextRegistry.registerContext(_context);
}
@Test
public void build_callsShStep() {
// prepare
String solutionPath = "some/path/to.sln";
MsBuild build = new MsBuild(solutionPath);
// execute
build.build();
// verify
verify(_steps).sh(anyString());
}
@Test
public void build_shStepReturnsStatusNotEqualsZero_callsErrorStep() {
// prepare
String solutionPath = "some/path/to.sln";
MsBuild build = new MsBuild(solutionPath);
when(_steps.sh(anyString())).thenReturn(-1);
// execute
build.build();
// verify
verify(_steps).error(anyString());
}
}
您可以使用 IntelliJ 代码编辑器左侧的绿色播放按钮来运行测试,希望它能变成绿色。
总结
基本上就是这样。现在是时候用 Jenkins 设置你的库了,创建一个新的作业,并运行 Jenkinsfile 来测试你的新自定义ex_msbuild
步骤。一个简单的测试 Jenkinsfile 可能如下所示:
// add the following line and replace necessary values if you are not loading the library implicitly
// @Library('my-library@master') _
pipeline {
agent any
stages {
stage('build') {
steps {
ex_msbuild 'some/path/to.sln'
}
}
}
}
显然,我还有很多内容可以讲(例如单元测试、依赖注入、Gradle、Jenkins 配置、使用 Jenkins 本身构建和测试库等等),但我希望这篇已经很长的博文能够更加简洁。我希望本文的总体思路和方法能够清晰地阐述,并帮助您创建一个可单元测试的共享库,使其比通常情况下更加健壮且更易于操作。
最后一条建议:单元测试和 Gradle 设置非常出色,在简化健壮共享流水线的开发方面大有裨益,但不幸的是,即使库测试通过,流水线内部仍然可能出现很多问题。以下情况大多是由于 Jenkins 的 Groovy 和沙盒机制的怪异性造成的:
- 未实现的类
Serializable
是必需的,因为“管道必须在 Jenkins 重启后继续存在” java.io.File
使用库中的类,这是被禁止的- Jenkinsfile 中的语法和拼写错误
因此,将 Jenkins 实例专门用于集成测试可能是一个好主意,vars
可以在“上线”之前测试新的和修改过的脚本。
再次,请随意在评论中写下任何问题或反馈,并查看GitHub上已完成的、可运行的示例库。
文章来源:https://dev.to/kuperadrian/how-to-setup-a-unit-testable-jenkins-shared-pipeline-library-2e62