本文首發於:Jenkins 中文社區java
Jenkins 共享庫是除了 Jenkins 插件外,另外一種擴展 Jenkins 流水線的技術。經過它,能夠輕鬆地自定義步驟,還能夠對現有的流水線邏輯進行必定程度的抽象與封裝。至於如何寫及如何使用它,讀者朋友能夠移步附錄中的官方文檔。git
可是如何對它進行單元測試呢?共享庫愈來愈大時,你不得不考慮這個問題。由於若是你不在早期就開始單元測試,共享庫後期可能就會發展成以下圖所示的「藝術品」——能工做,可是脆弱到沒有人敢動。github
[圖片來自網絡,侵權必刪]編程
這就是代碼越寫越慢的緣由之一。後人要不斷地填前人有意無心挖的坑。json
(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
複製代碼
以上是共享庫官方文檔介紹的代碼倉庫結構。整個代碼庫能夠分紅兩部分:src 目錄部分和 vars 目錄部分。它們的測試腳手架的搭建方式是不同的。bash
src 目錄中的代碼與普通的 Java 類代碼本質上沒有太大的區別。只不過換成了 Groovy 類。網絡
可是 vars 目錄中代碼自己是嚴重依賴於 Jenkins 運行時環境的腳本。閉包
接下來,分別介紹如何搭建它們的測試腳手架。框架
在對 src 目錄中的 Groovy 代碼進行單元測試前,咱們須要回答一個問題:使用何種構建工具進行構建?ide
咱們有兩種常規選擇:Maven 和 Gradle。本文選擇的是前者。
接下來的第二個問題是,共享庫源代碼結構並非 Maven 官方標準結構。下例爲標準結構:
├── pom.xml
└── src
├── main
│ ├── java
│ └── resources
└── test
├── java
└── resources
複製代碼
由於共享庫使用的 Groovy 寫的,因此,還必須使 Maven 能對 Groovy 代碼進行編譯。
能夠經過 Maven 插件:GMavenPlus 解決以上問題,插件的關鍵配置以下:
<configuration>
<sources>
<source>
<!-- 指定Groovy類源碼所在的目錄 -->
<directory>${project.basedir}/src</directory>
<includes>
<include>**/*.groovy</include>
</includes>
</source>
</sources>
<testSources>
<testSource>
<!-- 指定單元測試所在的目錄 -->
<directory>${project.basedir}/test/groovy</directory>
<includes>
<include>**/*.groovy</include>
</includes>
</testSource>
</testSources>
</configuration>
複製代碼
同時,咱們還必須加入 Groovy 語言的依賴:
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy-all.version}</version>
</dependency>
複製代碼
最終目錄結構以下圖所示:
而後咱們就能夠愉快地對 src 目錄中的代碼進行單元測試了。
對 vars 目錄中的腳本的測試難點在於它強依賴於 Jenkins 的運行時環境。換句話說,你必須啓動一個 Jenkins 才能正常運行它。可是這樣就變成集成測試了。那麼怎麼實現單元測試呢?
經 Google 發現,前人已經寫了一個 Jenkins 共享庫單元測試的框架。咱們拿來用就好。所謂,前人載樹,後人乘涼。
這個框架叫:Jenkins Pipeline Unit testing framework。後文簡稱「框架」。它的使用方法以下:
<dependency>
<groupId>com.lesfurets</groupId>
<artifactId>jenkins-pipeline-unit</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
複製代碼
// test/groovy/codes/showme/pipeline/lib/SayHelloTest.groovy
// 必須繼承 BasePipelineTest 類
class SayHelloTest extends BasePipelineTest {
@Override
@Before
public void setUp() throws Exception {
// 告訴框架,共享庫腳本所在的目錄
scriptRoots = ["vars"]
// 初始化框架
super.setUp()
}
@Test
void call() {
// 加載腳本
def script = loadScript("sayHello.groovy")
// 運行腳本
script.call()
// 斷言腳本中運行了 echo 方法
// 同時參數爲"hello pipeline"
assertThat(
helper.callStack
.findAll { c -> c.methodName == 'echo' }
.any { c -> c.argsToString().contains('hello pipeline') }
).isTrue()
// 框架提供的方法,後面會介紹。
printCallStack()
}
}
複製代碼
建立單元測試時,注意選擇 Groovy 語言,同時類名要以
Test
結尾。
extends BasePipelineTest
和 setUp
方法抽到一個父類中,全部其它測試類繼承於它。此時,咱們最簡單的共享庫的單元測試腳手架就搭建好了。
可是,實際工做中遇到場景並不會這麼簡單。面對更復雜的場景,必須瞭解 Jenkins Pipeline Unit testing framework 的原理。因而可知,寫單元測試也是須要成本的。至於收益,仁者見仁,智者見智了。
上文中的單元測試實際上作了三件事情:
loadScript
方法由框架提供。loadScript
方法返回加載好的腳本。helper
是 BasePipelineTest
的一個字段。從第三步的 helper.callStack
中,咱們能夠猜到第二步中的script.call()
並非真正的執行,而是將腳本中方法調用被寫到 helper 的 callStack 字段中。從 helper 的源碼能夠確認這一點:
/** * Stack of method calls of scripts loaded by this helper */
List<MethodCall> callStack = []
複製代碼
那麼,script.call() 內部是如何作到將方法調用寫入到 callStack 中的呢?
必定是在 loadScript
運行過程作了什麼事情,不然,script 怎麼會多出這些行爲。咱們來看看它的底層源碼:
/** * Load the script with given binding context without running, returning the Script * @param scriptName * @param binding * @return Script object */
Script loadScript(String scriptName, Binding binding) {
Objects.requireNonNull(binding, "Binding cannot be null.")
Objects.requireNonNull(gse, "GroovyScriptEngine is not initialized: Initialize the helper by calling init().")
Class scriptClass = gse.loadScriptByName(scriptName)
setGlobalVars(binding)
Script script = InvokerHelper.createScript(scriptClass, binding)
script.metaClass.invokeMethod = getMethodInterceptor()
script.metaClass.static.invokeMethod = getMethodInterceptor()
script.metaClass.methodMissing = getMethodMissingInterceptor()
return script
}
複製代碼
gse
是 Groovy 腳本執行引擎 GroovyScriptEngine
。它在這裏的做用是拿到腳本的 Class 類型,而後使用 Groovy 語言的 InvokerHelper
靜態幫助類建立一個腳本對象。
接下來作的就是核心了:
script.metaClass.invokeMethod = getMethodInterceptor()
script.metaClass.static.invokeMethod = getMethodInterceptor()
script.metaClass.methodMissing = getMethodMissingInterceptor()
複製代碼
它將腳本對象實例的方法調用都委託給了攔截器 methodInterceptor
。Groovy 對元編程很是友好。能夠直接對方法進行攔截。攔截器源碼以下:
/** * Method interceptor for any method called in executing script. * Calls are logged on the call stack. */
public methodInterceptor = { String name, Object[] args ->
// register method call to stack
int depth = Thread.currentThread().stackTrace.findAll { it.className == delegate.class.name }.size()
this.registerMethodCall(delegate, depth, name, args)
// check if it is to be intercepted
def intercepted = this.getAllowedMethodEntry(name, args)
if (intercepted != null && intercepted.value) {
intercepted.value.delegate = delegate
return callClosure(intercepted.value, args)
}
// if not search for the method declaration
MetaMethod m = delegate.metaClass.getMetaMethod(name, args)
// ...and call it. If we cannot find it, delegate call to methodMissing
def result = (m ? this.callMethod(m, delegate, args) : delegate.metaClass.invokeMissingMethod(delegate, name, args))
return result
}
複製代碼
它作了三件事情:
須要解釋一個第二點。並非全部的共享庫中的方法都是須要攔截的。咱們只須要對咱們感興趣的方法進行攔截,並實現 mock 的效果。
寫到這裏,有些讀者朋友可能頭暈了。筆者在這裏進行小結一下。
由於咱們不但願共享庫腳本中的依賴於 Jenkins 運行時的方法(好比拉代碼的步驟)真正運行。因此,咱們須要對這些方法進行 mock。在 Groovy 中,咱們能夠經過方法級別的攔截來實現 mock 的效果。 可是咱們又不該該對共享庫中全部的方法進行攔截,因此就須要咱們在執行單元測試前將本身須要 mock 的方法進行註冊到 helper 的
allowedMethodCallbacks
字段中。methodInterceptor
攔截器會根據它來進行攔截。
在 BasePipelineTest
的 setUp
方法中,框架註冊了一些默認方法,不至於咱們要手工註冊太多方法。如下是部分源碼:
helper.registerAllowedMethod("sh", [Map.class], null)
helper.registerAllowedMethod("checkout", [Map.class], null)
helper.registerAllowedMethod("echo", [String.class], null)
複製代碼
registerAllowedMethod
各參數的做用:
以上就是框架的基本原理了。接下來,再介紹幾種場景。
當你的共享庫腳本使用了 env
變量,能夠這樣測試:
binding.setVariable('env', new HashMap())
def script = loadScript('setEnvStep.groovy')
script.invokeMethod("call", [k: '123', v: "456"])
assertEquals("123", ((HashMap) binding.getVariable("env")).get("k"))
複製代碼
binding
由 BasePipelineTest
的一個字段,用於綁定變量。binding
會被設置到 gse
中。
好比腳本 a 中調用到了 setEnvStep
。這時能夠在 a 執行前註冊 setEnvStep
方法。
helper.registerAllowedMethod("setEnvStep", [LinkedHashMap.class], null)
複製代碼
helper.registerAllowedMethod("getDevOpsMetadata", [String.class, String.class], {
return "data from cloud"
})
複製代碼
不得不說 Jenkins Pipeline Unit testing framework 框架的做者很是聰明。另外,此類技術不只能夠用於單元測試。理論上還能夠用於 Jenkins pipeline 的零侵入攔截,以實現一些平臺級特殊的需求。
做者:翟志軍