如何:设置可单元测试的 Jenkins 共享管道库

2025-06-07

如何:设置可单元测试的 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 上的复选框。

项目创建-1

接下来,输入 GroupId 和 ArtifactId。

项目创建-2

忽略下一个窗口(默认即可),单击“下一步”,输入项目名称,然后单击“完成”。

项目创建-3

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


Enter fullscreen mode Exit fullscreen mode

所以:

  1. 将文件夹添加vars到项目根文件夹
  2. 将文件夹添加resource到项目根文件夹
  3. 删除里面的所有文件/文件夹src并添加一个新包,例如org.somecompany
  4. 编辑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'
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

保存后,将您的更改导入到 Gradle 项目:

导入更改

此时,我们的项目结构已正确,可以被 Jenkins 用作共享库。但是,正如您可能在上面的代码片段中看到的,我们还添加了一个名为 的单元测试源目录test。现在需要在项目的根目录下创建此文件夹,并org.somecompany像之前一样添加一个包src。最终结构应如下所示。

最终项目结构

太棒了,现在是时候实现我们的共享库了!

一般方法

首先简单介绍一下我们如何构建我们的库以及为什么我们这样做:

  • var我们将尽可能简化“自定义”步骤,不包含任何实际逻辑。相反,我们会创建一些类(在 内部src)来完成所有工作。
  • 我们创建一个接口,它声明了所有必需的 Jenkins 步骤(shbaterror等)的方法。类仅通过此接口调用步骤。
  • 我们为您的类编写单元测试,就像您通常使用 JUnit 和 Mockito 一样。

这样我们就可以:

  • 无需 Jenkins 即可编译并执行我们的库/单元测试
  • 测试我们的课程是否按预期工作
  • 测试是否使用正确的参数调用 Jenkins 步骤
  • 当 Jenkins 步骤失败时测试我们的代码的行为
  • 通过 Jenkins 本身构建、测试、运行指标并部署 Jenkins 管道库

现在让我们开始吧。

步进访问接口

首先,我们将在内部创建接口,所有org.somecompany类都将使用该接口来访问常规 Jenkins 步骤,例如sherror



package org.somecompany

interface IStepExecutor {
    int sh(String command)
    void error(String message)
    // add more methods for respective steps if needed
}


Enter fullscreen mode Exit fullscreen mode

这个接口很简洁,因为它可以在我们的单元测试中进行模拟。这样,我们的类就独立于 Jenkins 本身了。现在,让我们添加一个将在varsGroovy 脚本中使用的实现:



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)
    }
}


Enter fullscreen mode Exit fullscreen mode

添加基本​​依赖注入

因为我们不想在单元测试中使用上述实现,所以我们将设置一些基本的依赖注入,以便在单元测试期间将上述实现与模拟替换。如果您不熟悉依赖注入,您可能需要阅读相关内容,因为在这里解释它超出了范围,但您可能可以直接复制粘贴本章中的代码并继续操作。

因此,首先我们创建org.somecompany.ioc包并添加一个IContext接口:



package org.somecompany.ioc

import org.somecompany.IStepExecutor

interface IContext {
    IStepExecutor getStepExecutor()
}


Enter fullscreen mode Exit fullscreen mode

同样,这个接口将在单元测试中被模拟。但为了库的正常执行,我们仍然需要一个默认实现:



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)
    }
}


Enter fullscreen mode Exit fullscreen mode

为了完成我们的基本依赖注入设置,让我们添加一个“上下文注册表”,用于存储当前上下文(在正常执行期间和单元测试期间的DefaultContextMockito 模拟):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
    }
}


Enter fullscreen mode Exit fullscreen mode

就这样!现在我们可以自由地在里面编写可测试的 Jenkins 步骤了vars

编写自定义 Jenkins 步骤

假设我们这里举个例子,想要在库中添加一个步骤,调用 .NET 构建工具“MSBuild”来构建 .NET 项目。为此,我们首先ex_msbuild.groovyvars文件夹中添加一个 Groovy 脚本,该脚本的名称与我们要实现的自定义步骤的名称相同。由于脚本的名称已更改,因此ex_msbuild.groovy稍后可以在 Jenkinsfile 中调用该步骤ex_mbsbuild。现在,请在脚本中添加以下内容:



def call(String solutionPath) {
    // TODO
}


Enter fullscreen mode Exit fullscreen mode

按照我们的总体思路,我们希望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")
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

如你所见,我们在类中同时使用了sherror步骤,但不是直接使用它们,而是使用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()
}


Enter fullscreen mode Exit fullscreen mode

首先,我们使用上下文注册表设置上下文。由于我们不在单元测试中,因此我们使用默认上下文。this我们传入的registerDefaultContext()将被保存在DefaultContext其内部的私有_steps变量中,并用于访问 Jenkins 步骤。注册上下文后,我们就可以实例化我们的MsBuild类并调用build()执行所有工作的方法了。

很好,我们的vars脚本完成了。现在我们只需要为我们的MsBuild类编写一些单元测试。

添加单元测试

此时,编写单元测试应该一切如常。我们MsBuildTest在 test 文件夹中创建一个新的测试类,并包含 package org.somecompany.build。每次测试之前,我们都使用 Mockito 模拟IContextIStepExecutor接口,并注册模拟的上下文。然后,我们可以在测试中简单地创建一个新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());
    }
}


Enter fullscreen mode Exit fullscreen mode

您可以使用 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'
            }
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

显然,我还有很多内容可以讲(例如单元测试、依赖注入、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
PREV
useAxios:用于任何 Axios 调用的 React hook
NEXT
成为 Golang 英雄的 50 个项目创意