spock icon indicating copy to clipboard operation
spock copied to clipboard

PowerMockito can mock static JRE methods in JUnit but not in Spock

Open kriegaex opened this issue 5 years ago • 8 comments

I sometimes help people on StackOverflow or development teams I am coaching as an Agile Coach with test automation. Usually I introduce them to Spock + Geb and tell them to take their fingers off of PowerMock and similar frameworks. Instead I am telling them to refactor for better testability.

Anyway, I do know how to use PowerMockito from Spock and for the most part it works nicely, but in the past and then again lately I ran into problems when trying to do something like the following JUnit test in Spock:

package de.scrum_master.test;

import java.sql.Date;
import java.util.UUID;

public class ClassCallingStaticJREMethods {
  public String genUUID() {
    return UUID.randomUUID().toString();
  }

  public Date genDate(String date) {
    return Date.valueOf(date);
  }
}
package de.scrum_master.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.sql.Date;
import java.util.UUID;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.anyString;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({ ClassCallingStaticJREMethods.class })
public class JUnitTest {
  @Test
  public void testUUID() {
    final String id = "493410b3-dd0b-4b78-97bf-289f50f6e74f";
    UUID uuid = UUID.fromString(id);
    System.out.println(uuid);

    // Static mocking works for system (JDK, JRE) classes if the class *using* the
    // system class is present in @PrepareForTest instead of the system class itself
    mockStatic(UUID.class);
    when(UUID.randomUUID()).thenReturn(uuid);

    ClassCallingStaticJREMethods underTest = new ClassCallingStaticJREMethods();
    assertEquals(id, underTest.genUUID());
  }

  @Test
  public void testSQLDate() {
    Date date = new Date(1971 - 1900, 5 - 1, 8);
    System.out.println(date);

    // Static mocking works for system (JDK, JRE) classes if the class *using* the
    // system class is present in @PrepareForTest instead of the system class itself
    mockStatic(Date.class);
    when(Date.valueOf(anyString())).thenReturn(date);

    ClassCallingStaticJREMethods underTest = new ClassCallingStaticJREMethods();
    assertEquals(date, underTest.genDate("1998-12-20"));
  }
}

So far, so good. Now let's convert that test into a Spock test:

package de.scrum_master.test

import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import org.powermock.modules.junit4.PowerMockRunnerDelegate
import org.spockframework.runtime.Sputnik
import spock.lang.Specification

import java.sql.Date

import static org.mockito.ArgumentMatchers.anyString
import static org.powermock.api.mockito.PowerMockito.mockStatic
import static org.powermock.api.mockito.PowerMockito.when

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([ClassCallingStaticJREMethods.class])
class SpockTest extends Specification {
  def testUUID() {
    given:
    final String id = "493410b3-dd0b-4b78-97bf-289f50f6e74f";
    UUID uuid = UUID.fromString(id)
    println uuid
    mockStatic(UUID)
    // MissingMethodInvocationException:
    // when() requires an argument which has to be 'a method call on a mock'
    when(UUID.randomUUID()).thenReturn(uuid)
    ClassCallingStaticJREMethods underTest = new ClassCallingStaticJREMethods()

    when:
    String ret = underTest.genUUID()

    then:
    id == ret
  }

  def testSQLDate() {
    given:
    def date = new Date(1971 - 1900, 5 - 1, 8)
    println date
    mockStatic(Date)
    // IllegalArgumentException at java.sql.Date.valueOf(Date.java:143)
    // because the Date constructor is actually being called
    when(Date.valueOf(anyString())).thenReturn(date)
    ClassCallingStaticJREMethods underTest = new ClassCallingStaticJREMethods()

    when:
    def ret = underTest.genDate("1998-12-20" as String)

    then:
    ret == date
  }
}

If you want a repeatable build, put the class under test into src/main/java and the tests under src/test/java and src/test/groovy, respectively. This is the standard Maven project layout. Then build using this POM (I condensed a POM from bigger project into this one):

<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/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>de.scrum-master</groupId>
  <artifactId>test</artifactId>
  <version>1.0-SNAPSHOT</version>

  <dependencies>
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>2.23.0</version>
    </dependency>
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-core</artifactId>
      <version>2.0.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-api-mockito2</artifactId>
      <version>2.0.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-module-junit4</artifactId>
      <version>2.0.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>2.5.7</version>
      <type>pom</type>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <artifactId>groovy-test-junit5</artifactId>
          <groupId>org.codehaus.groovy</groupId>
        </exclusion>
        <exclusion>
          <artifactId>groovy-testng</artifactId>
          <groupId>org.codehaus.groovy</groupId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy</artifactId>
      <version>2.5.7</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.spockframework</groupId>
      <artifactId>spock-core</artifactId>
      <version>1.3-groovy-2.5</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib-nodep</artifactId>
      <version>3.2.5</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.objenesis</groupId>
      <artifactId>objenesis</artifactId>
      <version>2.5.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
        <executions>
          <execution>
            <id>default-compile</id>
            <phase>compile</phase>
            <goals>
              <goal>compile</goal>
            </goals>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
              <useIncrementalCompilation>false</useIncrementalCompilation>
              <encoding>UTF-8</encoding>
              <compilerId>groovy-eclipse-compiler</compilerId>
              <fork>true</fork>
            </configuration>
          </execution>
          <execution>
            <id>default-testCompile</id>
            <phase>test-compile</phase>
            <goals>
              <goal>testCompile</goal>
            </goals>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
              <useIncrementalCompilation>false</useIncrementalCompilation>
              <encoding>UTF-8</encoding>
              <compilerId>groovy-eclipse-compiler</compilerId>
              <fork>true</fork>
            </configuration>
          </execution>
        </executions>
        <dependencies>
          <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-compiler</artifactId>
            <version>3.4.0-01</version>
            <scope>compile</scope>
          </dependency>
          <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-batch</artifactId>
            <version>2.5.7-01</version>
            <scope>compile</scope>
          </dependency>
        </dependencies>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <useIncrementalCompilation>false</useIncrementalCompilation>
          <encoding>UTF-8</encoding>
          <compilerId>groovy-eclipse-compiler</compilerId>
          <fork>true</fork>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.codehaus.groovy</groupId>
        <artifactId>groovy-eclipse-compiler</artifactId>
        <version>3.4.0-01</version>
        <extensions>true</extensions>
        <executions>
          <execution>
            <id>default-add-groovy-build-paths</id>
            <phase>initialize</phase>
            <goals>
              <goal>add-groovy-build-paths</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.18.1</version>
        <executions>
          <execution>
            <id>default-test</id>
            <phase>test</phase>
            <goals>
              <goal>test</goal>
            </goals>
            <configuration>
              <excludes>
                <exclude>**/IT*.java</exclude>
              </excludes>
              <argLine>-Dfile.encoding=UTF8</argLine>
            </configuration>
          </execution>
        </executions>
        <configuration>
          <excludes>
            <exclude>**/IT*.java</exclude>
          </excludes>
          <argLine>-Dfile.encoding=UTF8</argLine>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

The Maven build mvn clean test > build.log 2>&1 says:

[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------< de.scrum-master:test >------------------------
[INFO] Building test 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ test ---
[INFO] Deleting ~\Documents\java-src\Spock_ML_PowerMockStaticJREMethods\target
[INFO] 
[INFO] --- groovy-eclipse-compiler:3.4.0-01:add-groovy-build-paths (default-add-groovy-build-paths) @ test ---
[INFO] Adding /src/main/groovy to the list of source folders
[INFO] Adding /src/test/groovy to the list of test source folders
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ test ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory ~\Documents\java-src\Spock_ML_PowerMockStaticJREMethods\src\main\resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.7.0:compile (default-compile) @ test ---
[INFO] Using Groovy-Eclipse compiler to compile both Java and Groovy files
[INFO] Compiling in a forked process using ~\.m2\repository\org\codehaus\groovy\groovy-eclipse-batch\2.5.7-01\groovy-eclipse-batch-2.5.7-01.jar
[INFO] Aktive Codepage: 65001.
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ test ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory ~\Documents\java-src\Spock_ML_PowerMockStaticJREMethods\src\test\resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.7.0:testCompile (default-testCompile) @ test ---
[INFO] Using Groovy-Eclipse compiler to compile both Java and Groovy files
[INFO] Compiling in a forked process using ~\.m2\repository\org\codehaus\groovy\groovy-eclipse-batch\2.5.7-01\groovy-eclipse-batch-2.5.7-01.jar
[INFO] Aktive Codepage: 65001.
[INFO] 
[INFO] --- maven-surefire-plugin:2.18.1:test (default-test) @ test ---
[INFO] Surefire report directory: ~\Documents\java-src\Spock_ML_PowerMockStaticJREMethods\target\surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Aktive Codepage: 65001.
Running de.scrum_master.test.JUnitTest
493410b3-dd0b-4b78-97bf-289f50f6e74f
1971-05-08
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.247 sec - in de.scrum_master.test.JUnitTest
Running de.scrum_master.test.SpockTest
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
Notifications are not supported when all test-instances are created first!
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
493410b3-dd0b-4b78-97bf-289f50f6e74f
Notifications are not supported when all test-instances are created first!
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
1971-05-08
Tests run: 3, Failures: 0, Errors: 3, Skipped: 0, Time elapsed: 2.018 sec <<< FAILURE! - in de.scrum_master.test.SpockTest
testUUID(de.scrum_master.test.SpockTest)  Time elapsed: 1.023 sec  <<< ERROR!
org.mockito.exceptions.misusing.MissingMethodInvocationException: 
when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
   Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.

	at de.scrum_master.test.SpockTest.testUUID(SpockTest.groovy:28)

testSQLDate(de.scrum_master.test.SpockTest)  Time elapsed: 0.119 sec  <<< ERROR!
java.lang.IllegalArgumentException: null
	at java.sql.Date.valueOf(Date.java:143)
	at de.scrum_master.test.SpockTest.testSQLDate(SpockTest.groovy:45)

Test mechanism  Time elapsed: 0.121 sec  <<< ERROR!
org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Misplaced or misused argument matcher detected here:

-> at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:55)

You cannot use argument matchers outside of verification or stubbing.
Examples of correct usage of argument matchers:
    when(mock.get(anyInt())).thenReturn(null);
    doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject());
    verify(mock).someMethod(contains("foo"))

This message may appear after an NullPointerException if the last matcher is returning an object 
like any() but the stubbed method signature expect a primitive argument, in this case,
use primitive alternatives.
    when(mock.get(any())); // bad use, will raise NPE
    when(mock.get(anyInt())); // correct usage use

Also, this error might show up because you use argument matchers with methods that cannot be mocked.
Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().
Mocking methods declared on non-public parent classes is not supported.

	at org.junit.runner.notification.SynchronizedRunListener.testFinished(SynchronizedRunListener.java:56)
	at org.junit.runner.notification.RunNotifier$7.notifyListener(RunNotifier.java:190)
	at org.junit.runner.notification.RunNotifier$SafeNotifier.run(RunNotifier.java:72)
	at org.junit.runner.notification.RunNotifier.fireTestFinished(RunNotifier.java:187)
	at org.spockframework.runtime.JUnitSupervisor.afterFeature(JUnitSupervisor.java:197)
	at org.spockframework.runtime.BaseSpecRunner.runFeature(BaseSpecRunner.java:236)
	at org.spockframework.runtime.BaseSpecRunner.runFeatures(BaseSpecRunner.java:185)
	at org.spockframework.runtime.BaseSpecRunner.doRunSpec(BaseSpecRunner.java:95)
	at org.spockframework.runtime.BaseSpecRunner$1.invoke(BaseSpecRunner.java:81)
	at org.spockframework.runtime.BaseSpecRunner.invokeRaw(BaseSpecRunner.java:484)
	at org.spockframework.runtime.BaseSpecRunner.invoke(BaseSpecRunner.java:467)
	at org.spockframework.runtime.BaseSpecRunner.runSpec(BaseSpecRunner.java:73)
	at org.spockframework.runtime.BaseSpecRunner.run(BaseSpecRunner.java:64)
	at org.spockframework.runtime.Sputnik.run(Sputnik.java:63)
	at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:283)
	at org.apache.maven.surefire.junit4.JUnit4Provider.executeWithRerun(JUnit4Provider.java:173)
	at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:153)
	at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:128)
	at org.apache.maven.surefire.booter.ForkedBooter.invokeProviderInSameClassLoader(ForkedBooter.java:203)
	at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:155)
	at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:103)


Results :

Tests in error: 
  
Misplaced or misused argument matcher detected here:

-> at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:55)

You cannot use argument matchers outside of verification or stubbing.
Examples of correct usage of argument matchers:
    when(mock.get(anyInt())).thenReturn(null);
    doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject());
    verify(mock).someMethod(contains("foo"))

This message may appear after an NullPointerException if the last matcher is returning an object 
like any() but the stubbed method signature expect a primitive argument, in this case,
use primitive alternatives.
    when(mock.get(any())); // bad use, will raise NPE
    when(mock.get(anyInt())); // correct usage use

Also, this error might show up because you use argument matchers with methods that cannot be mocked.
Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().
Mocking methods declared on non-public parent classes is not supported.

  SpockTest.testSQLDate:45 » IllegalArgument
  SpockTest.testUUID:28 MissingMethodInvocation 
when() requires an argument whi...

Tests run: 5, Failures: 0, Errors: 3, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  11.944 s
[INFO] Finished at: 2019-08-24T12:31:34+07:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.18.1:test (default-test) on project test: Execution default-test of goal org.apache.maven.plugins:maven-surefire-plugin:2.18.1:test failed: There was an error in the forked process
[ERROR] org.apache.maven.surefire.testset.TestSetFailedException: org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
[ERROR] Misplaced or misused argument matcher detected here:
[ERROR] 
[ERROR] -> at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:55)
[ERROR] 
[ERROR] You cannot use argument matchers outside of verification or stubbing.
[ERROR] Examples of correct usage of argument matchers:
[ERROR]     when(mock.get(anyInt())).thenReturn(null);
[ERROR]     doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject());
[ERROR]     verify(mock).someMethod(contains("foo"))
[ERROR] 
[ERROR] This message may appear after an NullPointerException if the last matcher is returning an object 
[ERROR] like any() but the stubbed method signature expect a primitive argument, in this case,
[ERROR] use primitive alternatives.
[ERROR]     when(mock.get(any())); // bad use, will raise NPE
[ERROR]     when(mock.get(anyInt())); // correct usage use
[ERROR] 
[ERROR] Also, this error might show up because you use argument matchers with methods that cannot be mocked.
[ERROR] Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().
[ERROR] Mocking methods declared on non-public parent classes is not supported.
[ERROR] 
[ERROR] 	at org.apache.maven.surefire.common.junit4.JUnit4RunListener.rethrowAnyTestMechanismFailures(JUnit4RunListener.java:213)
[ERROR] 	at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:133)
[ERROR] 	at org.apache.maven.surefire.booter.ForkedBooter.invokeProviderInSameClassLoader(ForkedBooter.java:203)
[ERROR] 	at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:155)
[ERROR] 	at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:103)
[ERROR] Caused by: org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
[ERROR] Misplaced or misused argument matcher detected here:
[ERROR] 
[ERROR] -> at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:55)
[ERROR] 
[ERROR] You cannot use argument matchers outside of verification or stubbing.
[ERROR] Examples of correct usage of argument matchers:
[ERROR]     when(mock.get(anyInt())).thenReturn(null);
[ERROR]     doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject());
[ERROR]     verify(mock).someMethod(contains("foo"))
[ERROR] 
[ERROR] This message may appear after an NullPointerException if the last matcher is returning an object 
[ERROR] like any() but the stubbed method signature expect a primitive argument, in this case,
[ERROR] use primitive alternatives.
[ERROR]     when(mock.get(any())); // bad use, will raise NPE
[ERROR]     when(mock.get(anyInt())); // correct usage use
[ERROR] 
[ERROR] Also, this error might show up because you use argument matchers with methods that cannot be mocked.
[ERROR] Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().
[ERROR] Mocking methods declared on non-public parent classes is not supported.
[ERROR] 
[ERROR] 	at org.junit.runner.notification.SynchronizedRunListener.testFinished(SynchronizedRunListener.java:56)
[ERROR] 	at org.junit.runner.notification.RunNotifier$7.notifyListener(RunNotifier.java:190)
[ERROR] 	at org.junit.runner.notification.RunNotifier$SafeNotifier.run(RunNotifier.java:72)
[ERROR] 	at org.junit.runner.notification.RunNotifier.fireTestFinished(RunNotifier.java:187)
[ERROR] 	at org.spockframework.runtime.JUnitSupervisor.afterFeature(JUnitSupervisor.java:197)
[ERROR] 	at org.spockframework.runtime.BaseSpecRunner.runFeature(BaseSpecRunner.java:236)
[ERROR] 	at org.spockframework.runtime.BaseSpecRunner.runFeatures(BaseSpecRunner.java:185)
[ERROR] 	at org.spockframework.runtime.BaseSpecRunner.doRunSpec(BaseSpecRunner.java:95)
[ERROR] 	at org.spockframework.runtime.BaseSpecRunner$1.invoke(BaseSpecRunner.java:81)
[ERROR] 	at org.spockframework.runtime.BaseSpecRunner.invokeRaw(BaseSpecRunner.java:484)
[ERROR] 	at org.spockframework.runtime.BaseSpecRunner.invoke(BaseSpecRunner.java:467)
[ERROR] 	at org.spockframework.runtime.BaseSpecRunner.runSpec(BaseSpecRunner.java:73)
[ERROR] 	at org.spockframework.runtime.BaseSpecRunner.run(BaseSpecRunner.java:64)
[ERROR] 	at org.spockframework.runtime.Sputnik.run(Sputnik.java:63)
[ERROR] 	at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:283)
[ERROR] 	at org.apache.maven.surefire.junit4.JUnit4Provider.executeWithRerun(JUnit4Provider.java:173)
[ERROR] 	at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:153)
[ERROR] 	at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:128)
[ERROR] 	... 3 more
[ERROR] 
[ERROR] -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginExecutionException

Here is an archive with all source code files quoted above incl. Maven POM: Spock_PowerMockStaticJREMethods.zip

My actual question is not primarily a request to the Spock maintainers to make Spock compatible with PowerMock (even though that would be nice) but to explain what is happening here and how I can work around it. I assume the problem is related to Groovy code transformations taking place in Spock tests or maybe something about delegates, meta classes or how methods are discovered and called in general. The whole area of Groovy delegates, meta classes etc. is not my area of expertise, so I am asking, sorry.

Is there any way I can stop Spock from actually evaluating the original static method and instead doing whatever ought to be done in order to get these calls under PowerMock's control?

kriegaex avatar Aug 24 '19 05:08 kriegaex

I still found no explanation and also no workaround with mockStatic which I could later check interactions (number of calls) on with verifyStatic, but I did find a simple workaround for stubbing out the static methods:

package de.scrum_master.test

import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import org.powermock.modules.junit4.PowerMockRunnerDelegate
import org.spockframework.runtime.Sputnik
import spock.lang.Specification

import java.sql.Date

import static org.powermock.api.support.membermodification.MemberMatcher.method
import static org.powermock.api.support.membermodification.MemberModifier.stub

@RunWith(PowerMockRunner)
@PowerMockRunnerDelegate(Sputnik)
@PrepareForTest(ClassCallingStaticJREMethods)
class SpockTest extends Specification {
  def testUUID() {
    given:
    final String id = "493410b3-dd0b-4b78-97bf-289f50f6e74f";
    UUID uuid = UUID.fromString(id)
    stub(method(UUID, "randomUUID")).toReturn(uuid)
    ClassCallingStaticJREMethods underTest = new ClassCallingStaticJREMethods()

    expect:
    underTest.genUUID() == id
  }

  def testSQLDate() {
    given:
    def date = new Date(1971 - 1900, 5 - 1, 8)
    stub(method(Date, "valueOf", String)).toReturn(date)
    ClassCallingStaticJREMethods underTest = new ClassCallingStaticJREMethods()

    expect:
    underTest.genDate("1998-12-20") == date
  }
}

Please note that I need to prepare the class under test rather than the JRE classes with the static methods for test via @PrepareForTest(ClassCallingStaticJREMethods) as is described in the PowerMock wiki for stubbing static methods.

kriegaex avatar Aug 24 '19 16:08 kriegaex

I assume the problem is related to Groovy code transformations taking place in Spock tests or maybe something about delegates, meta classes or how methods are discovered and called in general. The whole area of Groovy delegates, meta classes etc. is not my area of expertise, so I am asking, sorry.

Is there any way I can stop Spock from actually evaluating the original static method and instead doing whatever ought to be done in order to get these calls under PowerMock's control?

Actually I found out that the same problem occurs in JUnit tests written in Groovy when using PowerMock. So this is not a Spock issue per se. While for JUnit tests I can just annotate the classs or methods using PowerMock with @CompileStatic, I cannot do that for Spock feature methods because they rely on Groovy dynamical features. The workaround is to isolate the code using PowerMock in helper methods or inside a static inner helper class and annotate the latter with @CompileStatic. Then the helpers can be called from Spock feature methods.

I have documented my findings and provided sample code in Powermock issue #1008. Maybe the Spock and PowerMock maintainers can talk to each other in order to find a way to avoid this problem. Maybe the Groovy people at Spock have ideas for the Java people at PowerMock how to implement this in a way to make it compatible with Groovy.

kriegaex avatar Oct 04 '19 05:10 kriegaex

This cannot be solved on either side unless Powermock would add explicit Groovy support which is probably very unlikely.

Powermock uses custom classloaders for non-system classes and if you @PrepareForTest a class, then its bytecode gets rewritten to enable the functionalities that are not possible without PowerMock. This is also the cause you need to @PrepareForTest the class that is calling the system class, not the system class, because the custom classloader does not have the system classes under control and cannot rewrite their byte code.

The same rewriting happens with the test class itself. PowerMock rewrites the byte code of the test class to not call the actual system class but to record / verify the calls. Due to the dynamic language features of Groovy where the calls are not made directly but resolved through meta classes and so on, the byte code to be rewritten is not as expected and does not work.

Only if you use @CompileStatic for a method or class, the dynamic language features at runtime are not used, but the methods are resolved at compile time and land in the byte code like when compiled from a Java class.

Due to that it works as expected if you @CompileStatic a helper method that does the calls that PowerMock needs to rewrite.

And as Spock is not going to support @CompileStatic generally, because it builds on the dynamic features of Groovy for some of its functionality, and it is unlikely that PowerMock will add general Groovy support, the only way use PowerMocks system class mocking is to use @CompileStatic.

One can try to use it on the feature method as some feature methods will work with @CompileStatic. And either generally or if it does not work, one needs to factor out the calls PowerMock needs to instrument to helper methods that are annotated with @CompileStatic.

Vampire avatar Oct 07 '19 09:10 Vampire

Thanks for the good explanation and for confirming my educated guesses.

Besides, the existing tickets to support mocking for static methods and final classes/methods in Spock for non-Groovy code would still be my favourites because not to need PowerMock at all when using Spock would be desirable.

kriegaex avatar Oct 08 '19 00:10 kriegaex

I agree. Well, as the issue (at least for final stuff) is opened by one of the spock maintainers and also labeled help-wanted, I'd say it is safe to assume that a PR that adds this would be accepted. So feel free to implement it and open a PR. ;-)

Vampire avatar Oct 08 '19 09:10 Vampire

Dear Björn,

the following rant is not directed personally at you and I don't think that you are one of the Spock maintainers (feel free to correct me if I am wrong). But I like to get something off my chest.

I don't like to be unfriendly or exude a sense of entitlement, but today I am not in a super friendly mood either, so let me be blunt: If every time I got 1 Euro for "feel free to implement a PR" as an answer to a bug report or feature request, I would be a rich man today.

I have not been a professional developer for a long time, last time I got money for coding was more than 15 years ago. I am an agile coach who still codes a little bit in his spare time for fun or uses his coding skills for coaching developers when necessary.

Still if I can I contribute to OSS projects, be it via PR (if I am technically capable of it), documentation contribution or just by filing and re-testing bugs etc. I do know that OSS also depends on third-party contributions. But I do think that this kind of answer is just unnecessary and evasive. Whether OSS maintainers like it or not, together with the software they created they also took a certain degree of responsibility and ought to live up to it. A killer phrase like "PRs are welcome" in order to avoid having to maintain the project is just not very helpful. To mere users who clearly lack the technical capability to contribute more than trivial bugfixes this feels like they are being mocked by the superior tech buffs.

Look, I speak some Java and am quite familiar with Groovy as a Spock user, but unless a maintainer takes a few hours to explain the internals of Spock and Groovy meta programming to me, it is completely unrealistic that I can implement that myself. If it was that simple, I am pretty sure a Spock maintainer would have done it already because many people ask for it. I know because I answer a lot of Spock questions on StackOverflow (which is also a kind of contribution to the project because the maintainers can concentrate more on the product instead of user support).

BTW, in the past I offered to OSS maintainers several times to spend some time on introducing me into the project's source code and other internals in order to help me get up to speed in becoming a co-maintainer or contributor, but usually they say they are too busy. But at the same time they are also too busy to fix bugs and implement features. So which should it be? It is like saying: "I don't have time to sharpen my saw/axe because I am so busy cutting down trees."

kriegaex avatar Oct 08 '19 10:10 kriegaex

I don't think that you are one of the Spock maintainers (feel free to correct me if I am wrong)

Correct. :-)

If every time I got 1 Euro for "feel free to implement a PR" as an answer to a bug report or feature request, I would be a rich man today.

Yeah, same for me, unfortunately it doesn't work that way. :-D

Be assured, I don't take it as a personal attack. Actually I pretty much feel the same you do. Everything I am answered with or asked to provide a PR, I also think "I already do many contributions by testing, reporting, making PRs if I can and want to, don't ask me to do more you demanding ****".

But then I always remind myself, that this is not fair either and decide to either provide the PR or just ignore the statement.

Still if I can I contribute to OSS projects, be it via PR (if I am technically capable of it), documentation contribution or just by filing and re-testing bugs etc.

And imho even recommending a software is important contribution as is reporting bugs, contributing documentations and so on. Be assured that I totally understand you and totally agree.

But I do think that this kind of answer is just unnecessary and evasive.

Here I have to disagree. I also each time think "Oh my god, just do it, I helped enough". But then realise, it is not unnecessary at all. Many people do not even consider createing a PR and hinting them that way might actually make them create one and maybe become an active contributor. For guys like you or me, this might be superfluous, but the one writing such a statement can most often not evaluate whether it is unnecessary in this case or not.

Whether OSS maintainers like it or not, together with the software they created they also took a certain degree of responsibility and ought to live up to it.

Sure, but it is still better to release the OSS, even if they don't have much time to maintain, as it is OSS and any member of the OSS community can help maintaining. If people would not open-source their stuff because they are afraid of having to maintain it, would be sad either, as then much less code would be open sourced.

You have to consider that most OSS is also created and maintained by the authors in their sparse spare time and their is most often not some company behind that pays them to maintain the software full-time.

Look, I speak some Java and am quite familiar with Groovy as a Spock user, but unless a maintainer takes a few hours to explain the internals of Spock and Groovy meta programming to me, it is completely unrealistic that I can implement that myself.

This has nothing to do with Groovy at all. If you do Groovy meta programming to mock these things, they are only effective when called from dynamic Groovy code. Instead you need to rewrite the class files by using some class loader or a Java agent to also rewrite system classes on byte code level, so that the changes are visible to Java classes that call those classes which makes this change non-trivial.

If it was that simple, I am pretty sure a Spock maintainer would have done it already because many people ask for it.

Just that many people ask for something doesn't mean it gets implemented soon, just that something is simple doesn't mean it gets implemented soon. In most OSS people do things in their spare time and this might be sparse, so not all that is highly demanded or simple can be worked on by the original maintainers, which makes PRs so valuable. :-)

BTW, in the past I offered to OSS maintainers several times to spend some time on introducing me into the project's source code and other internals in order to help me get up to speed in becoming a co-maintainer or contributor, but usually they say they are too busy. But they are also too busy to fix bug and implement features. So which should it be? It is like saying: "I don't have time to sharpen my saw/axe because I am so busy cutting down trees."

Yes, this is sad and often reading and understanding the source yourself is the only way to get up and running which can sometimes be frustrating and confusing. If you ever need help to contribute to a project where I have some insight, ping me, I might be able to give you a kickstart. :-)

Vampire avatar Oct 08 '19 14:10 Vampire

If anyone still needs mock static methods, have a look at spock-mockfree

wangdengwu avatar Sep 13 '22 08:09 wangdengwu

Spock now supports static mocks via mockito and the Spock API SpyStatic(). See #1756

AndreasTu avatar Feb 22 '24 20:02 AndreasTu