对象依赖导致实例化错误

Objenesis dependency causes instantiation error

刚刚开始一个新的 Gradle 项目。

此测试通过:

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

    when:
    Launcher.main()

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

... 直到,为了使用 (Java) Mock 进行另一个测试,我必须添加这些依赖项:

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

(注意,我假设这些版本适用于 Groovy 3.+,我现在正在使用...两者都是 Maven Repo 上可用的最新版本)。

使用这些依赖项,上述测试失败:

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)

我承认我对 "bytebuddy" 和 "objenesis" 的实际作用只有最粗略的概念,尽管我认为它非常聪明。编辑:刚刚访问了他们各自的主页,我的想法现在稍微不那么粗略了,是的,它非常聪明。

如果对此没有正统的解决方案,是否有可能关闭对单个功能(即测试)的这些依赖项的使用?可能使用一些注释?

编辑

这是一个 MCVE: 规格:Java 11.0.5,OS Linux 薄荷 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
    }
}

为什么这个项目需要一些东西来调用 Application 子类的原因解释了 :这样就可以做一个 installDist 捆绑在 JavaFX.

Don't we have to use a global GroovyMock?

如果你想检查交互,是的。但实际上您正在测试 JavaFX 启动器而不是您的应用程序。所以我怀疑有什么好处。我会专注于测试 App class。也想象一下,您将使用 Java 中的主要方法编写 classes 而不是 Groovy。 Groovy 模拟在从 Java 代码调用时不起作用,尤其是全局代码。然后您将通过 Spock 的 Powermockito 进行测试,这也可以,但您仍然会测试 JavaFX 启动器而不是您的应用程序。

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

不是我说的。我说:“可能 您的应用程序设计有问题”。我这么说的原因是因为使用 Groovy 模拟和模拟静态方法之类的东西是测试代码的味道。您可以检查气味,然后确定它没问题,而在大多数情况下 IMO 则不是。此外,问题也可能出在测试本身,而不是应用程序设计,在这种情况下我会说是。但这是有争议的,所以我将在下面进一步为您提供解决方案。

在这种情况下,从技术上讲,如果您坚持测试 JavaFX 启动器,全局 Application 模拟是您唯一的方法,因为即使 App 上的全局模拟也无法正常工作启动器使用反射来调用 App 构造函数,并且不会被模拟框架拦截。

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?

你已经通过查看 GitHub 存储库发现了它,但我只是在包含 "M2" 的异常版本号中看到它,例如 "milestone 2" 类似于 [=55] =](或"CR")表示候选版本(或候选版本)。


至于技术问题,您可以不在 Gradle 脚本中声明 Objenesis,因为它是一个可选的依赖项,然后测试编译并运行良好,正如您自己已经注意到的那样。但是假设您需要可选的依赖项,如 Objenesis、CGLIB(实际上是 cglib-nodep)、Bytebuddy 和 ASM 以用于套件中的其他测试,您可以告诉 Spock 在这种情况下不要使用 Objenesis。所以假设你有一个像这样的 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 '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 {}

我的 MCVE 版本看起来像这样(抱歉,我添加了自己的包名称并导入,否则它就不是真正的 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)
  }
}

这里的决定性细节是useObjenesis: false参数。


更新: 仅供参考,这就是使用 PowerMockito 在 Java 中实现的启动器 class 的处理方式。

注意,此解决方案需要来自 Spock 1.x 的 Sputnik 转轮,该转轮已在 2.x 中删除。所以在 Spock 2 中这目前不起作用,因为它基于 JUnit 5 并且不能再使用 @RunWith(PowerMockRunner)@PowerMockRunnerDelegate(Sputnik) 因为 PowerMock 当前不支持 JUnit 5。但是我用 Spock 1.3-[ 测试了它=70=]-2.5 和 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)
  }
}