3

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.

mike rodent
  • 14,126
  • 11
  • 103
  • 157
  • As you are working with a Groovy version for which no official Spock release exists, your exact setup would be even more interesting than usual and an [MCVE](https://stackoverflow.com/help/mcve) - yes, I ask for it again, you remember me - is what you should provide. For instance, you are not even mentioning which Spock pre-release version you use, e.g. 2.0-M2-groovy-3.0 which itself depends on cglib 3.2.10, bytebuddy 1.9.11 and objenesis 3.0.1 with which you should better stick when first setting up a bleeding edge type of project. Which is your Java version? – kriegaex Mar 19 '20 at 02:56
  • By the way, if you need a Groovy mock, especially a global mock, probably something is wrong with your application design and you should refactor so as to make it easier to inject the `Applicaction` mock into your `Launcher`. – kriegaex Mar 19 '20 at 02:59
  • Thanks, these comments are very helpful. Please see the edit: MCVE. If you can suggest a refactoring of some kind that would also be helpful: the problem being that Gradle's `application` plugin requires a "main class", and that `Application.launch()` is also `static`. Don't we have to use a global `GroovyMock`? Also isn't it slightly extreme to say any use of Groovy mocks is wrong? – mike rodent Mar 19 '20 at 07:39
  • Just a final thing: you say that Spock spock-core:2.0-M2-groovy-3.0 is a "pre-release". I can't see anything on this page (https://mvnrepository.com/artifact/org.spockframework/spock-core/2.0-M2-groovy-3.0) which says that. How do you know? By the way, I also tried with the versions of bytebuddy, objenesis and cglib stated on that page: same error. – mike rodent Mar 19 '20 at 08:05
  • Ah, yes, seen it here: https://github.com/spockframework/spock/releases. I switched back to Groovy 2.5.9 and Spock 1.3, got rid of `test { useJUnitPlatform() }`, used the "right" versions for bytebuddy, objenesis and cglib for Spock 1.3: same error. – mike rodent Mar 19 '20 at 08:26

1 Answers1

2

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)
  }
}
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • 1
    Thanks, this is great. I went searching on this `useObjenesis` parameter and found very little in the way of documentation, just this really: https://github.com/spockframework/spock/blob/master/docs/interaction_based_testing.adoc. So I'm intrigued how you know this. Concerning the actual choice to test this: having done a "spike" (developed for a bit without testing), I'm now going back to do the same thing using the strictest TDD possible. The simplest principle is, obviously, do nothing without a test. So the first test was to check that the Launcher launches the App instance. – mike rodent Mar 20 '20 at 07:51
  • Also, it was at first trivial (using a global `GroovyMock`) to do this test, which is the first I did, until I *afterwards* had to add the objenesis and bytebuddy dependencies, because a simple Spock Mock was giving this error: "Mocking of non-interface types requires a code generation library." – mike rodent Mar 20 '20 at 08:58
  • From the manual chapter about à la carte mocks I know that mocks, stubs, spies can take optional parameters. The rest was taking a look into the [source code](https://github.com/spockframework/spock/blob/spock-1.3/spock-core/src/main/java/spock/mock/MockingApi.java#L921) via Ctrl-B in IntelliJ IDEA or F3 in Eclipse. – kriegaex Mar 20 '20 at 09:36
  • As for using a normal mock, it would not work because a normal mock, being a dynamic proxy, has no effect on static methods. – kriegaex Mar 20 '20 at 09:37
  • Sorry, in my second comment there about the simple Spock Mock, I was in fact referring to *another* test, this other test being the reason I had to include objenesis and bytebuddy. Thanks for the explanation about exploring additional parameters. – mike rodent Mar 20 '20 at 11:04
  • PS I was wondering why I couldn't see these Spock `Mock` `@NamedParam`s in my Spock 1.3 Javadoc. It appears that that's because all these classes are annotated `@Beta`. Presumably in a future Spock release all this documentation will be made clearer. – mike rodent Mar 20 '20 at 11:15