本文首發於: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 目錄部分。它們的測試腳手架的搭建方式是不同的。網絡
src 目錄中的代碼與普通的 Java 類代碼本質上沒有太大的區別。只不過換成了 Groovy 類。閉包
可是 vars 目錄中代碼自己是嚴重依賴於 Jenkins 運行時環境的腳本。框架
接下來,分別介紹如何搭建它們的測試腳手架。ide
在對 src 目錄中的 Groovy 代碼進行單元測試前,咱們須要回答一個問題:使用何種構建工具進行構建?工具
咱們有兩種常規選擇: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 的零侵入攔截,以實現一些平臺級特殊的需求。
做者:翟志軍