Search code examples
groovymockingspockbyte-buddyobjenesis

Objenesis dependency causes instantiation error


Just starting a new Gradle project.

This test passes:

def 'Launcher.main should call App.launch'(){
    given:
    GroovyMock(Application, global: true)

    when:
    Launcher.main()

    then:
    1 * Application.launch( App, null ) >> null
}

... until, to get another test using a (Java) Mock to work, I have to add these dependencies:

testImplementation 'net.bytebuddy:byte-buddy:1.10.8'
testImplementation 'org.objenesis:objenesis:3.1'

(NB I assume these versions are OK for Groovy 3.+, which I'm now using ... both are the most up-to-date available at Maven Repo).

With these dependencies the above test fails:

java.lang.InstantiationError: javafx.application.Application
    at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:48)
    at org.objenesis.ObjenesisBase.newInstance(ObjenesisBase.java:73)
    at org.objenesis.ObjenesisHelper.newInstance(ObjenesisHelper.java:44)
    at org.spockframework.mock.runtime.MockInstantiator$ObjenesisInstantiator.instantiate(MockInstantiator.java:45)
    at org.spockframework.mock.runtime.MockInstantiator.instantiate(MockInstantiator.java:31)
    at org.spockframework.mock.runtime.GroovyMockFactory.create(GroovyMockFactory.java:57)
    at org.spockframework.mock.runtime.CompositeMockFactory.create(CompositeMockFactory.java:42)
    at org.spockframework.lang.SpecInternals.createMock(SpecInternals.java:47)
    at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:298)
    at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:288)
    at org.spockframework.lang.SpecInternals.GroovyMockImpl(SpecInternals.java:215)
    at core.AppSpec.Launcher.main should call App.launch(first_tests.groovy:30)

I confess that I have only the sketchiest notion of what "bytebuddy" and "objenesis" actually do, although I assume it is fiendishly clever. Edit: having just visited their respective home pages my notion is now slightly less sketchy, and yes, it is fiendishly clever.

If an orthodox solution to this is not available, is it by any chance possible to turn off the use of these dependencies for an individual feature (i.e. test)? Possibly using some annotation maybe?

Edit

This is an MCVE: Specs: Java 11.0.5, OS Linux Mint 18.3.

build.gradle:

plugins {
    id 'groovy'
    id 'java'
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.8'
}
repositories { mavenCentral() }
javafx {
    version = "11.0.2"
    modules = [ 'javafx.controls', 'javafx.fxml' ]
}
dependencies {
    implementation 'org.codehaus.groovy:groovy:3.+'
    testImplementation 'junit:junit:4.12'
    testImplementation 'org.spockframework:spock-core:2.0-M2-groovy-3.0'
    testImplementation 'net.bytebuddy:byte-buddy:1.10.8'
    testImplementation 'org.objenesis:objenesis:3.1'
    // in light of kriegaex's comments:
    implementation group: 'cglib', name: 'cglib', version: '3.3.0'
}
test { useJUnitPlatform() }
application {
    mainClassName = 'core.Launcher'
}
installDist{}

main.groovy:

class Launcher {
    static void main(String[] args) {
        Application.launch(App, null )
    }
}
class App extends Application {
    void start(Stage primaryStage) {
    }
}

first_tests.groovy:

class AppSpec extends Specification {
    def 'Launcher.main should call App.launch'(){
        given:
        GroovyMock(Application, global: true)
        when:
        Launcher.main()
        then:
        1 * Application.launch( App, null ) >> null
    }
}

The reason why this project needs something to call the Application subclass is explained here: it's so that it is possible to do an installDist which bundles in JavaFX.


Solution

  • Don't we have to use a global GroovyMock?

    If you want to check the interaction, yes. But actually you are testing the JavaFX launcher rather than your application. So I doubt that there is any benefit. I would focus on testing the App class instead. Also imagine for a moment that you would write the classes with main methods in Java instead of Groovy. Groovy mocks would not work when called from Java code, especially not global ones. Then you would end up testing via Powermockito from Spock, which would also work but still you would test the JavaFX launcher rather than your application.

    Also isn't it slightly extreme to say any use of Groovy mocks is wrong?

    I did not say that. I said: "probably something is wrong with your application design". The reason I said that is because the use of Groovy mocks and things like mocking static methods are test code smells. You can check the smell and then decide it is okay, which IMO in most cases it is not. Besides, instead of application design the problem can also be in the test itself, which in this case I would say it is. But that is arguable, so I am going to present a solution to you further below.

    In this case technically the global Application mock is your only way if you do insist to test the JavaFX launcher because even a global mock on App would not work as the launcher uses reflection in order to call the App constructor and that is not intercepted by the mock framework.

    you say that Spock spock-core:2.0-M2-groovy-3.0 is a "pre-release". I can't see anything on this page (...) which says that. How do you know?

    You found out already by checking out the GitHub repository, but I was just seeing it in the unusual version number containing "M2" like "milestone 2" which is similar to "RC" (or "CR") for release candidates (or candidate releases).


    As for the technical problem, you can either not declare Objenesis in your Gradle script because it is an optional dependency, then the test compiles and runs fine, as you already noticed yourself. But assuming you need optional dependencies like Objenesis, CGLIB (actually cglib-nodep), Bytebuddy and ASM for other tests in your suite, you can just tell Spock not to use Objenesis in this case. So assuming you have a Gradle build file like this:

    plugins {
      id 'groovy'
      id 'java'
      id 'application'
      id 'org.openjfx.javafxplugin' version '0.0.8'
    }
    
    repositories { mavenCentral() }
    
    javafx {
      version = "11.0.2"
      modules = ['javafx.controls', 'javafx.fxml']
    }
    
    dependencies {
      implementation 'org.codehaus.groovy:groovy:3.+'
      testImplementation 'org.spockframework:spock-core:2.0-M2-groovy-3.0'
    
      // Optional Spock dependencies, versions matching the ones listed at
      // https://mvnrepository.com/artifact/org.spockframework/spock-core/2.0-M2-groovy-3.0
      testImplementation 'net.bytebuddy:byte-buddy:1.9.11'
      testImplementation 'org.objenesis:objenesis:3.0.1'
      testImplementation 'cglib:cglib-nodep:3.2.10'
      testImplementation 'org.ow2.asm:asm:7.1'
    }
    
    test { useJUnitPlatform() }
    
    application {
      mainClassName = 'de.scrum_master.app.Launcher'
    }
    
    installDist {}
    

    My version of your MCVE would looks like this (sorry, I added my own package names and also imports because otherwise it is not really an MCVE):

    package de.scrum_master.app
    
    import javafx.application.Application
    import javafx.scene.Scene
    import javafx.scene.control.Label
    import javafx.scene.layout.StackPane
    import javafx.stage.Stage
    
    class App extends Application {
      @Override
      void start(Stage stage) {
        def javaVersion = System.getProperty("java.version")
        def javafxVersion = System.getProperty("javafx.version")
        Label l = new Label("Hello, JavaFX $javafxVersion, running on Java $javaVersion.")
        Scene scene = new Scene(new StackPane(l), 640, 480)
        stage.setScene(scene)
        stage.show()
      }
    }
    
    package de.scrum_master.app
    
    import javafx.application.Application
    
    class Launcher {
      static void main(String[] args) {
        Application.launch(App, null)
      }
    }
    
    package de.scrum_master.app
    
    import javafx.application.Application
    import spock.lang.Specification
    
    class AppSpec extends Specification {
      def 'Launcher.main should call App.launch'() {
        given:
        GroovyMock(Application, global: true, useObjenesis: false)
    
        when:
        Launcher.main()
    
        then:
        1 * Application.launch(App, null)
      }
    }
    

    The decisive detail here is the useObjenesis: false parameter.


    Update: Just for reference, this is how you would do it with a launcher class implemented in Java using PowerMockito.

    Attention, this solution needs the Sputnik runner from Spock 1.x which was removed in 2.x. So in Spock 2 this currently does not work because it is based on JUnit 5 and can no longer use @RunWith(PowerMockRunner) and @PowerMockRunnerDelegate(Sputnik) because PowerMock currently does not support JUnit 5. But I tested it with Spock 1.3-groovy-2.5 and Groovy 2.5.8.

    package de.scrum_master.app
    
    import javafx.application.Application
    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 static org.mockito.Mockito.*
    import static org.powermock.api.mockito.PowerMockito.*
    
    @RunWith(PowerMockRunner)
    @PowerMockRunnerDelegate(Sputnik)
    @PrepareForTest(Application)
    class JavaAppSpec extends Specification {
      def 'JavaLauncher.main should launch JavaApp'() {
        given:
        mockStatic(Application)
    
        when:
        JavaLauncher.main()
    
        then:
        verifyStatic(Application, times(1))
        Application.launch(JavaApp)
      }
    }