Use jenkins-test-harness + spock to unit test jenkins job dsl script

Hello all,

currently i try to create a project to centralize the generation of job on my jenkins : i use jenkins job dsl 1.77 and try to follow this explanation : Testing DSL Scripts · jenkinsci/job-dsl-plugin Wiki · GitHub

my env is : gradle 8 and jdk 11 on unix env (ubuntu)

here i follow the code :

// build.gradle
plugins {
    id 'groovy'
}

sourceSets {
    jobs {
        groovy {
            srcDirs 'src/jobs'
            compileClasspath += main.compileClasspath
        }
        compileClasspath += sourceSets.main.output
        runtimeClasspath += sourceSets.main.output
    }
    src {
        groovy {
            srcDir 'src/main/groovy'
        }
    }
    test {
        groovy {
            srcDir 'src/test/groovy'
        }
    }
}

repositories {
    mavenCentral()
    maven { url 'https://repo.jenkins-ci.org/releases/' }
    jcenter{url "https://jcenter.bintray.com/"}
}

configurations {
    testPlugins {}

    // see JENKINS-45512
    testCompile {
        exclude group: 'xalan'
        exclude group: 'xerces'
    }
}

configurations.all*.exclude group: 'xalan'

dependencies {
    implementation 'org.codehaus.groovy:groovy:2.5.14'
    implementation ("org.jenkins-ci.plugins:job-dsl-core:${jobDslVersion}") {
        exclude module: 'xstream'
    }


    testCompile 'org.spockframework:spock-core:1.3-groovy-2.5'
    testImplementation 'org.spockframework:spock-core:2.0-M5-groovy-3.0'
    testImplementation 'cglib:cglib-nodep:2.2.2' // used by Spock

    // Jenkins test harness dependencies
    testImplementation('org.jenkins-ci.main:jenkins-test-harness:2146.v6b_f8b_1cb_d12d') { //2.34 2146.v6b_f8b_1cb_d12d
        exclude group: 'org.netbeans.modules', module: 'org-netbeans-insane'
    }
    testImplementation("org.jenkins-ci.main:jenkins-war:${jenkinsVersion}") {
        exclude group: 'org.jenkins-ci.ui', module: 'bootstrap'
    }
    testCompile("org.jenkins-ci.main:jenkins-war:${jenkinsVersion}") {
        exclude group: 'org.jenkins-ci.ui', module: 'bootstrap'
    }

    // Job DSL plugin including plugin dependencies
    testImplementation "org.jenkins-ci.plugins:job-dsl:${jobDslVersion}"
    testImplementation "org.jenkins-ci.plugins:job-dsl:${jobDslVersion}@jar"
    testImplementation 'org.jenkins-ci.plugins:structs:1.20@jar'
    testImplementation 'org.jenkins-ci.plugins:script-security:1.54@jar'
    testImplementation "org.jenkins-ci.main:jenkins-core:${jenkinsVersion}"
    testCompile "org.jenkins-ci.plugins:job-dsl:${jobDslVersion}"
    testCompile "org.jenkins-ci.plugins:job-dsl:${jobDslVersion}@jar"
    testCompile 'org.jenkins-ci.plugins:structs:1.20@jar'
    testCompile 'org.jenkins-ci.plugins:script-security:1.54@jar'

    // Plugins to install in test instance
    testPlugins 'org.jenkins-ci.plugins:cloudbees-folder:5.14'
    testPlugins 'org.jenkins-ci.plugins:credentials:2.1.10'
    testPlugins 'org.jenkins-ci.plugins:cvs:2.13'
    testPlugins 'org.jenkins-ci.plugins:ghprb:1.40.0'
    testPlugins 'org.jenkins-ci.plugins:token-macro:2.5'
    testPlugins 'org.jenkins-ci.plugins.workflow:workflow-cps-global-lib:2.7'


}

task resolveTestPlugins(type: Copy) {
    from configurations.testPlugins
    into new File(sourceSets.test.output.resourcesDir, 'test-dependencies')
    include '*.hpi'
    include '*.jpi'
    def mapping = [:]

    doFirst {
        configurations.testPlugins.resolvedConfiguration.resolvedArtifacts.each {
            mapping[it.file.name] = "${it.name}.${it.extension}"
        }
    }
    rename { mapping[it] }

    doLast {
        List<String> baseNames = source*.name.collect { mapping[it] }.collect { it[0..it.lastIndexOf('.') - 1] }
        new File(destinationDir, 'index').setText(baseNames.join('\n'), 'UTF-8')
    }
}

test {
    dependsOn tasks.resolveTestPlugins
    inputs.files sourceSets.jobs.groovy.srcDirs
    useJUnitPlatform()
}
```

```
# gradle.properties
jobDslVersion=1.77
jenkinsVersion= 2.445
// JobRulesSpec.groovy
package com.example

import groovy.util.FileNameFinder
import javaposse.jobdsl.dsl.DslScriptLoader
import javaposse.jobdsl.plugin.JenkinsJobManagement
import org.junit.ClassRule
import org.jvnet.hudson.test.JenkinsRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll

class JobRulesSpec extends Specification {
    @Shared
    @ClassRule
    private JenkinsRule jenkinsRule = new JenkinsRule()

    @Unroll
    def 'test script #file.name'() { //File file
        given:
        println "==============> ${jenkinsRule.instance}"
        def jobManagement = new JenkinsJobManagement(System.out, [:], new File('.'))

        when:
        new DslScriptLoader(jobManagement).runScript(file.text)

        then:
        noExceptionThrown()

        where:
        file << new FileNameFinder().getFileNames('src/jobs', '**/*.groovy').collect { new File(it) }
    }
}
result 

<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="net.regnology.JobRulesSpec" tests="1" skipped="0" failures="1" errors="0" timestamp="2024-02-19T16:49:41" hostname="INV-PC-432" time="0.683">
  <properties/>
  <testcase name="test script job.groovy" classname="net.regnology.JobRulesSpec" time="0.683">
    <failure message="Expected no exception to be thrown, but got 'java.lang.IllegalStateException'" type="org.spockframework.runtime.UnallowedExceptionThrownError">Expected no exception to be thrown, but got 'java.lang.IllegalStateException'
	at app//spock.lang.Specification.noExceptionThrown(Specification.java:118)
	at net.regnology.JobRulesSpec.test script #file.name(jobsRulesSpec.groovy:27)
Caused by: java.lang.IllegalStateException: Jenkins.instance is missing. Read the documentation of Jenkins.getInstanceOrNull to see what you are doing wrong.
	at jenkins.model.Jenkins.get(Jenkins.java:819)
	at javaposse.jobdsl.plugin.LookupStrategy.lambda$static$0(LookupStrategy.java:19)
	at javaposse.jobdsl.plugin.LookupStrategy.getContext(LookupStrategy.java:57)
	at javaposse.jobdsl.plugin.LookupStrategy.getItem(LookupStrategy.java:42)
	at javaposse.jobdsl.plugin.JenkinsJobManagement.createOrUpdateConfig(JenkinsJobManagement.java:136)
	at javaposse.jobdsl.dsl.AbstractDslScriptLoader.extractGeneratedJobs_closure4(AbstractDslScriptLoader.groovy:204)
	at groovy.lang.Closure.call(Closure.java:412)
	at groovy.lang.Closure.call(Closure.java:428)
	at javaposse.jobdsl.dsl.AbstractDslScriptLoader.extractGeneratedJobs(AbstractDslScriptLoader.groovy:197)
	at javaposse.jobdsl.dsl.AbstractDslScriptLoader.extractGeneratedItems(AbstractDslScriptLoader.groovy:184)
	at javaposse.jobdsl.dsl.AbstractDslScriptLoader.runScripts_closure1(AbstractDslScriptLoader.groovy:63)
	at groovy.lang.Closure.call(Closure.java:412)
	at groovy.lang.Closure.call(Closure.java:428)
	at javaposse.jobdsl.dsl.AbstractDslScriptLoader.runScripts(AbstractDslScriptLoader.groovy:46)
	at javaposse.jobdsl.dsl.AbstractDslScriptLoader.runScript(AbstractDslScriptLoader.groovy:87)
	at net.regnology.JobRulesSpec.test script #file.name(jobsRulesSpec.groovy:24)
</failure>
  </testcase>
  <system-out><![CDATA[==============> null
Processing provided DSL script
]]></system-out>
  <system-err><![CDATA[Feb 19, 2024 5:49:41 PM javaposse.jobdsl.plugin.JenkinsJobManagement createOrUpdateConfig
INFO: createOrUpdateConfig for test
]]></system-err>
</testsuite>

i take same question on jenkins test harness repository but from authors of jenkins test harness seems not possible : i can initialize spock test · Issue #732 · jenkinsci/jenkins-test-harness · GitHub

if you have any idea that’s great thx

regards

I belive that you need add spock-junit4 or add junit5 see this Release Notes
At least this fix my issue which I hit few days ago when updating my script.

thx decide to use directly junit instead of spock . seems pretty fine now

Hi @josselinchevalay ,

I’ve the same issue.

Can you please provides your new build.gradle and JobRulesSpec.groovy files that solved this issue?

Best regards

The version from the Wiki is ok, it does still work with Spock, you just need to adapt to the current versions which are compatible with your Jenkins version, so of cause you can’t do a 1:1 copy - have that setup running with Jenkins 2.492.2.