如何对 Jenkins 共享库进行单元测试
2019獨角獸企業(yè)重金招聘Python工程師標準>>>
本文首發(fā)于:Jenkins 中文社區(qū)
Jenkins 共享庫是除了 Jenkins 插件外,另一種擴展 Jenkins 流水線的技術。通過它,可以輕松地自定義步驟,還可以對現(xiàn)有的流水線邏輯進行一定程度的抽象與封裝。至于如何寫及如何使用它,讀者朋友可以移步附錄中的官方文檔。
對共享庫進行單元測試的原因
但是如何對它進行單元測試呢?共享庫越來越大時,你不得不考慮這個問題。因為如果你不在早期就開始單元測試,共享庫后期可能就會發(fā)展成如下圖所示的“藝術品”——能工作,但是脆弱到?jīng)]有人敢動。
[圖片來自網(wǎng)絡,侵權必刪]
這就是代碼越寫越慢的原因之一。后人要不斷地填前人有意無意挖的坑。
共享庫單元測試搭建
共享庫官方文檔介紹的代碼倉庫結構
(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 類代碼本質上沒有太大的區(qū)別。只不過換成了 Groovy 類。
但是 vars 目錄中代碼本身是嚴重依賴于 Jenkins 運行時環(huán)境的腳本。
接下來,分別介紹如何搭建它們的測試腳手架。
測試 src 目錄中的 Groovy 代碼
在對 src 目錄中的 Groovy 代碼進行單元測試前,我們需要回答一個問題:使用何種構建工具進行構建?
我們有兩種常規(guī)選擇: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 目錄中 Groovy 代碼
對 vars 目錄中的腳本的測試難點在于它強依賴于 Jenkins 的運行時環(huán)境。換句話說,你必須啟動一個 Jenkins 才能正常運行它。但是這樣就變成集成測試了。那么怎么實現(xiàn)單元測試呢?
經(jīng) Google 發(fā)現(xiàn),前人已經(jīng)寫了一個 Jenkins 共享庫單元測試的框架。我們拿來用就好。所謂,前人載樹,后人乘涼。
這個框架叫:Jenkins Pipeline Unit testing framework。后文簡稱“框架”。它的使用方法如下:
創(chuàng)建單元測試時,注意選擇 Groovy 語言,同時類名要以 Test 結尾。
此時,我們最簡單的共享庫的單元測試腳手架就搭建好了。
但是,實際工作中遇到場景并不會這么簡單。面對更復雜的場景,必須了解 Jenkins Pipeline Unit testing framework 的原理。由此可見,寫單元測試也是需要成本的。至于收益,仁者見仁,智者見智了。
Jenkins Pipeline Unit testing framework 原理
上文中的單元測試實際上做了三件事情:
從第三步的 helper.callStack 中,我們可以猜到第二步中的script.call() 并不是真正的執(zhí)行,而是將腳本中方法調用被寫到 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 腳本執(zhí)行引擎 GroovyScriptEngine。它在這里的作用是拿到腳本的 Class 類型,然后使用 Groovy 語言的 InvokerHelper 靜態(tài)幫助類創(chuàng)建一個腳本對象。
接下來做的就是核心了:
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 stackint depth = Thread.currentThread().stackTrace.findAll { it.className == delegate.class.name }.size()this.registerMethodCall(delegate, depth, name, args)// check if it is to be intercepteddef intercepted = this.getAllowedMethodEntry(name, args)if (intercepted != null && intercepted.value) {intercepted.value.delegate = delegatereturn callClosure(intercepted.value, args)}// if not search for the method declarationMetaMethod m = delegate.metaClass.getMetaMethod(name, args)// ...and call it. If we cannot find it, delegate call to methodMissingdef result = (m ? this.callMethod(m, delegate, args) : delegate.metaClass.invokeMissingMethod(delegate, name, args))return result}它做了三件事情:
需要解釋一個第二點。并不是所有的共享庫中的方法都是需要攔截的。我們只需要對我們感興趣的方法進行攔截,并實現(xiàn) mock 的效果。
寫到這里,有些讀者朋友可能頭暈了。筆者在這里進行小結一下。
因為我們不希望共享庫腳本中的依賴于 Jenkins 運行時的方法(比如拉代碼的步驟)真正運行。所以,我們需要對這些方法進行 mock。在 Groovy 中,我們可以通過方法級別的攔截來實現(xiàn) mock 的效果。 但是我們又不應該對共享庫中所有的方法進行攔截,所以就需要我們在執(zhí)行單元測試前將自己需要 mock 的方法進行注冊到 helper 的 allowedMethodCallbacks 字段中。methodInterceptor攔截器會根據(jù)它來進行攔截。
在 BasePipelineTest 的 setUp 方法中,框架注冊了一些默認方法,不至于我們要手工注冊太多方法。以下是部分源碼:
helper.registerAllowedMethod("sh", [Map.class], null) helper.registerAllowedMethod("checkout", [Map.class], null) helper.registerAllowedMethod("echo", [String.class], null)registerAllowedMethod 各參數(shù)的作用:
- 第一個參數(shù):要注冊的方法。
- 第二參數(shù):該方法的參數(shù)列表。
- 第三參數(shù):一個閉包。當該訪問被調用時會執(zhí)行此閉包。
以上就是框架的基本原理了。接下來,再介紹幾種場景。
幾種應用場景
環(huán)境變量
當你的共享庫腳本使用了 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 執(zhí)行前注冊 setEnvStep 方法。
helper.registerAllowedMethod("setEnvStep", [LinkedHashMap.class], null)希望被 mock 的方法能有返回值
helper.registerAllowedMethod("getDevOpsMetadata", [String.class, String.class], {return "data from cloud" })后記
不得不說 Jenkins Pipeline Unit testing framework 框架的作者非常聰明。另外,此類技術不僅可以用于單元測試。理論上還可以用于 Jenkins pipeline 的零侵入攔截,以實現(xiàn)一些平臺級特殊的需求。
附錄
- 共享庫官方文檔:https://jenkins.io/zh/doc/book/pipeline/shared-libraries/
- 本文示例代碼:https://github.com/zacker330/jenkins-pipeline-shared-lib-unittest-demo
- JenkinsPipelineUnit:https://github.com/jenkinsci/JenkinsPipelineUnit
作者:翟志軍
轉載于:https://my.oschina.net/jenkinszh/blog/3055265
總結
以上是生活随笔為你收集整理的如何对 Jenkins 共享库进行单元测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 初识 TensorFlow 旅程之一
- 下一篇: 需求分析——调研需求时如何调查系统相关者