Closed mrnguyen4386 closed 4 years ago
What code did you write and run to cause that?
Sorry, I am super new at this. Thank you for your patience.
I updated my pom.xml to have the following:
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>4.2</version>
<relativePath />
</parent>
and I ran this test that ran into the issue:
class CiBuildTriggersHelperSpec extends JenkinsPipelineSpecification {
def ciBuildTriggersHelper = null
def setup(){
ciBuildTriggersHelper = loadPipelineScriptForTest('vars/ciBuildTriggersApp.groovy')
}
Hm; I'd hope to be able to see the implementation of ciBuildTriggersApp.groovy
, but I understand if that's not possible.
Jenkins is already Mocked!
It looks like somewhere there's probably a line like this:
import jenkins.model.Jenkins
...
Jenkins.get()
This is a static method on the jenkins.model.Jenkins
class, which normally returns the current instance of Jenkins. But, during unit tests, there is no real instance of Jenkins.
Per the documentation:
Mock Jenkins There is a Spock Mock of type Jenkins available at getPipelineMock("Jenkins"). It is set up as follows:
- Jenkins.HOLDER is implemented to return the mock Jenkins.
- Jenkins.getInstance is stubbed to return the mock Jenkins
- Jenkins.getInstanceOrNull is stubbed to return the mock Jenkins
- Jenkins.getExtensionList is stubbed to return instances of the @Extensions of the provided type (actually, only the ExtensionList.iterator method is stubbed, so use a for/each loop). -- https://javadoc.io/static/com.homeaway.devtools.jenkins/jenkins-spock/2.1.2/com/homeaway/devtools/jenkins/testing/JenkinsPipelineSpecification.html#mock-jenkins
It looks like Jenkins.get()
should've been automatically stubbed to return the existing mock, as well.
@awittha Thank you for the quick response. I don't know if this make a difference, but the unit test ran without issue before I modified the pom.xml to have org.jenkins-ci.plugins
as parent and added some dependencies to comply upper bound dependency issued by enforce plugin. Additionally, ciBuildTriggersApp.groovy
doesn't import jenkins.model.Jenkins
or doesn't used that object explicitly.
Before updating your pom.xml parent...
Was your parent pom an older version of the Jenkins plugin parent pom? That is to say, is your project a Jenkins plugin?
Or, was your parent pom something else?
I didn't have a parent in the older pom.xml. Below is the pom.xml content that run unit test without issue without parent. I fear the way pom.xml is configured may be the cause for the issue?
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.intusurg.spr.jenkins</groupId>
<artifactId>jenkins-shared-lib</artifactId>
<packaging>jar</packaging>
<version>0.4.0</version>
<description>Jenkins Shared Library</description>
<repositories>
<repository>
<id>jenkins-releases</id>
<name>Jenkins Releases</name>
<url>http://repo.jenkins-ci.org/releases</url>
</repository>
</repositories>
<properties>
<groovy.core.version>2.4.11</groovy.core.version>
<groovy.gmaven.pluginVersion>1.6.1</groovy.gmaven.pluginVersion>
<google.guava.version>20.0</google.guava.version>
<jenkins-spock.version>2.0.0</jenkins-spock.version>
<jenkins.version>2.102</jenkins.version>
<jenkins.servlet.version>3.1.0</jenkins.servlet.version>
<jenkins.workflow.cps.version>2.36</jenkins.workflow.cps.version>
<jenkins.workflow.step.version>2.10</jenkins.workflow.step.version>
<jenkins.workflow.basic.steps.version>2.6</jenkins.workflow.basic.steps.version>
<jenkins.workflow.durable.task.steps.version>2.21</jenkins.workflow.durable.task.steps.version>
<jenkins.pipeline.stage.step.version>2.3</jenkins.pipeline.stage.step.version>
<jenkins.git.version>3.9.1</jenkins.git.version>
<junit.version>4.12</junit.version>
<junit.plugin.version>1.24</junit.plugin.version>
<pipeline-utility-steps>2.3.1</pipeline-utility-steps>
<surefire.pluginVersion>2.22.0</surefire.pluginVersion>
<logback.configration>logback-test.xml</logback.configration>
<logdir>${project.build.directory}/log</logdir>
<test.loglevel>ERROR</test.loglevel>
<log.logback.version>1.2.3</log.logback.version>
<log.slf4j.version>1.7.25</log.slf4j.version>
<build-user-vars-plugin.version>1.5</build-user-vars-plugin.version>
<sonar.host.url>https://sonarqube-dev.corp.intusurg.com</sonar.host.url>
<sonar.projectName>Jenkins Shared Library</sonar.projectName>
<sonar.projectKey>jenkins-shared-lib</sonar.projectKey>
<sonar.sources>src</sonar.sources>
<sonar.tests>test</sonar.tests>
<sonar.groovy.jacoco.reportPath>target/jacoco.exec</sonar.groovy.jacoco.reportPath>
</properties>
<dependencyManagement>
<dependencies>
<!-- Transitive Dependencies that need Managing -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${google.guava.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.ivy</groupId>
<artifactId>ivy</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>com.launchdarkly</groupId>
<artifactId>launchdarkly-java-server-sdk</artifactId>
<version>4.11.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.homeaway.devtools.jenkins</groupId>
<artifactId>jenkins-spock</artifactId>
<version>${jenkins-spock.version}</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jenkins-ci.plugins/build-user-vars-plugin -->
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>build-user-vars-plugin</artifactId>
<version>${build-user-vars-plugin.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jenkins-ci.plugins/ssh-credentials -->
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>ssh-credentials</artifactId>
<version>1.18</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jenkins-ci.plugins/credentials-binding -->
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials-binding</artifactId>
<version>1.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.cloudbees/groovy-cps -->
<dependency>
<groupId>com.cloudbees</groupId>
<artifactId>groovy-cps</artifactId>
<version>1.31</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${log.logback.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${log.logback.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>jenkins-core</artifactId>
<version>${jenkins.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-basic-steps</artifactId>
<version>${jenkins.workflow.basic.steps.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>${jenkins.workflow.cps.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<!-- provides sh() step -->
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-durable-task-step</artifactId>
<version>${jenkins.workflow.durable.task.steps.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<!-- provides stage() step -->
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>pipeline-stage-step</artifactId>
<version>${jenkins.pipeline.stage.step.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<!-- provides git() step -->
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>git</artifactId>
<version>${jenkins.git.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${jenkins.servlet.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>junit</artifactId>
<version>${junit.plugin.version}</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>pipeline-utility-steps</artifactId>
<version>${pipeline-utility-steps}</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy.core.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${log.slf4j.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>${log.slf4j.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.pluginVersion}</version>
<executions>
<execution>
<id>default-test</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<forkCount>1</forkCount>
<includes>
<include>${test.pattern}</include>
</includes>
<useManifestOnlyJar>false</useManifestOnlyJar>
<systemPropertyVariables>
<root.loglevel>${test.loglevel}</root.loglevel>
<root.appender>Stdout</root.appender>
<test.loglevel>${test.loglevel}</test.loglevel>
<logdir>${logdir}</logdir>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>${groovy.gmaven.pluginVersion}</version>
<executions>
<execution>
<id>groovy</id>
<goals>
<goal>addSources</goal>
<goal>addTestSources</goal>
<goal>generateStubs</goal>
<goal>generateTestStubs</goal>
<goal>compile</goal>
<goal>compileTests</goal>
<goal>removeStubs</goal>
<goal>removeTestStubs</goal>
</goals>
<configuration>
<configScript>config.groovy</configScript>
<sources>
<source>
<directory>src</directory>
<includes>
<include>**/*.groovy</include>
</includes>
</source>
</sources>
<testSources>
<testSource>
<directory>test</directory>
<includes>
<include>**/*.groovy</include>
</includes>
</testSource>
</testSources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.3</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
<testResources>
<testResource>
<includes>
<include>vars/**/*.groovy</include>
</includes>
<directory>${project.basedir}</directory>
</testResource>
<testResource>
<directory>test/resources</directory>
</testResource>
<testResource>
<directory>resources</directory>
</testResource>
</testResources>
</build>
</project>
Is your project a Jenkins plugin? Generally, I'd say you shouldn't use the Jenkins plugin parent pom, if your project is not a Jenkins plugin.
ciBuildTriggersApp.groovy
doesn'timport jenkins.model.Jenkins
or doesn't used that object explicitly.
Yeah, it's coming from here: https://github.com/jenkinsci/pipeline-milestone-step-plugin/blob/master/src/main/java/org/jenkinsci/plugins/pipeline/milestone/MilestoneStep.java#L105
when MilestoneStep
's Descriptor is instantiated (as must be done by jenkins-spock
in order to get the name of the step and set up the correct mock), MilestoneStep
tries to load configuration from Jenkins... but there is no Jenkins. The specific path it uses hits Jenkins.get()
, which isn't stubbed like all of the other Jenkins.get....()
methods.
I don't know why, exactly, the parent pom.xml change caused this to surface, but if I had to guess I'd guess that adding the parent pom (or some of the other pom changes you mentioned) brought the Milestone plugin onto the classpath, causing jenkins-spock
to try to work with it.
You'd have gotten a different error - one that you could maybe work around by adding stubbing to the Jenkins
mock - if Jenkins.get()
had been properly stubbed by jenkins-spock.
That makes sense. Thank you very much for the explanation. Just want to clarify, as of now there is no work around beside not having the parent pom.xml because Jenkins
wasn't stubbed properly by jenkins-spock?
as of now there is no work around beside not having the parent pom.xml because Jenkins wasn't stubbed properly by jenkins-spock?
Actually...
I misspoke earlier. This is happening before any Specification-specific stuff happens... this is happening during static setup of the test-suite, before any mocks exist.
There will never be a mock Jenkins
at that time. This makes sense, because looking closer at the code, this actually would have worked if it had happened after the mock Jenkins was created by jenkins-spock
.
The stack trace starts here: https://github.com/ExpediaGroup/jenkins-spock/blob/ebdaf89887cce6552d52349af7b703e365b76430/src/main/groovy/com/homeaway/devtools/jenkins/testing/JenkinsPipelineSpecification.groovy#L1005
in static setup before the JenkinsPipelineSpecification
runs any tests or creates any mocks - it's still identifying what it will need to mock.
That leads to it instantiating MilestoneStep.DescriptorImpl
(so that it can call getFunctionNamed()
and know what to name the mock) and running into the load()
step from here: https://github.com/jenkinsci/pipeline-milestone-step-plugin/blob/master/src/main/java/org/jenkinsci/plugins/pipeline/milestone/MilestoneStep.java#L105
the implementation of which unfortunately calls Jenkins.get()
.
There is, indeed, no real Jenkins and no mock Jenkins at this time.
There cannot be, either... because this is static setup, not specific to any test case. A mock created and used here will either be inaccessible during any test or shared across all tests.
I am not sure how to fix this, anymore. I'll think about it.
In this case, it's extra-tough because MilestoneStep.DescriptorImpl
is a static final
class. We probably cannot mock or stub it or otherwise modify its behavior, so it's probably always going to try to load()
.
This kind of descriptor/extension-instantiation- or classload-time (truly static) Jenkins-dependent setup is very frustrating for offline unit-tests; Milestone is far from the only bit of code to to this.
Maybe a special mock Jenkins
created and used only at static setup-time just to placate these kinds of setup?
But even then, that won't work properly because there is no way for you, the test author, to access that mock and stub it to do what the step expects. How can you stub the implementation of load()
so that Milestone
is happy? Where would you write that test code? In setupSpec
in your own Specification? Well... the parent setupSpec
is run first, per Spock's design, so your own method is "too late." But of course, if your code could run before the parent's setupSpec
, the mock Jenkins
wouldn't be around for you to stub!
FOR NOW, just exclude the Milestone
jenkins plugin from your project if you can.
If your code-under-test actually uses it... then I don't know what to tell you.
@awittha I will exclude Milestone
jenkins plugin from my project since none of my code under test is not using it. I think the parent org.jenkins-ci.plugins
pom.xml has the milestone plugin. Do you know a way to exclude it from the parent pom.xml or it's something I have to not have the parent and add dependency as needed? Again thank you very much for your support.
If the parent pom has it as a direct <dependency>
, there's nothing you can do from a child pom.
If the parent pom has some dependency that has the Milestone plugin as a transitive dependency, you need a Maven exclusion for the Milestone plugin, placed in the declaration of whichever dependency(ies) are depending on the Milestone plugin.
You can run mvn dependency:tree
and search for "milestone" to locate the plugin(s) that are bringing it in.
The core problem should be resolved in jenkins-spock:2.1.4
.
Depending on the situation, there are still several ways to resolve the original exception. In this case, I believe excluding the Milestone
plugin (if your code actually doesn't use it) is still the right choice.
Other options are now detailed in the documentation. See https://github.com/ExpediaGroup/jenkins-spock/blob/master/src/test/groovy/com/homeaway/devtools/jenkins/testing/DescriptorTimeJenkinsInteractingSpec.groovy for an example of how you might handle the situation where you actually needed the milestone
step.
I ran into the following issue: