【问题标题】:Objenesis dependency causes instantiation errorObjenesis 依赖导致实例化错误
【发布时间】:2020-03-18 19:47:04
【问题描述】:

刚刚开始一个新的 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,操作系统 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
    }
}

这个项目需要调用Application 子类的原因解释了here:这样就可以在JavaFX 中进行捆绑的installDist

【问题讨论】:

  • 当您使用的 Groovy 版本不存在正式的 Spock 版本时,您的确切设置将比平时更有趣,MCVE - 是的,我再次要求它,你记得我 - 是你应该提供的。例如,您甚至没有提及您使用的是哪个 Spock 预发布版本,例如2.0-M2-groovy-3.0 本身依赖于 cglib 3.2.10、bytebuddy 1.9.11 和 objenesis 3.0.1,在首次设置前沿类型的项目时最好坚持使用它们。你的 Java 版本是什么?
  • 顺便说一句,如果您需要一个 Groovy 模拟,尤其是全局模拟,那么您的应用程序设计可能有问题,您应该重构以便更容易注入 Applicaction 模拟你的Launcher
  • 谢谢,这些 cmets 非常有帮助。请参阅编辑:MCVE。如果您可以建议某种类型的重构也将有所帮助:问题在于 Gradle 的 application 插件需要一个“主类”,而 Application.launch() 也是 static。我们不是必须使用全局GroovyMock 吗?说任何使用 Groovy 模拟是错误的,这不是有点极端吗?
  • 最后一件事:您说 Spock spock-core:2.0-M2-groovy-3.0 是“预发布”。我在此页面 (mvnrepository.com/artifact/org.spockframework/spock-core/…) 上看不到任何内容。你怎么知道的?顺便说一句,我还尝试了该页面上声明的 bytebuddy、objenesis 和 cglib 的版本:同样的错误。
  • 啊,是的,在这里看到了:github.com/spockframework/spock/releases。我切换回 Groovy 2.5.9 和 Spock 1.3,摆脱了 test { useJUnitPlatform() },为 Spock 1.3 使用了 bytebuddy、objenesis 和 cglib 的“正确”版本:同样的错误。

标签: groovy mocking spock byte-buddy objenesis


【解决方案1】:

我们不是必须使用全局 GroovyMock 吗?

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

任何使用 Groovy 模拟是错误的,这不是有点极端吗?

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

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

您说 Spock spock-core:2.0-M2-groovy-3.0 是“预发布”。我在此页面(...)上看不到任何内容。你怎么知道的?

您已经通过查看 GitHub 存储库发现了它,但我只是在包含“M2”的不寻常版本号中看到它,例如“里程碑 2”,它类似于发布候选者的“RC”(或“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 中实现的启动器类的方法。

注意,此解决方案需要 Spock 1.x 中的 Sputnik 运行器,该运行器已在 2.x 中删除。所以在 Spock 2 中这目前不起作用,因为它基于 JUnit 5 并且不能再使用 @RunWith(PowerMockRunner)@PowerMockRunnerDelegate(Sputnik) 因为 PowerMock 目前不支持 JUnit 5。但是我用 Spock 1.3-groovy-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)
  }
}

【讨论】:

  • 谢谢,太好了。我搜索了这个useObjenesis 参数,发现文档很少,真的是:github.com/spockframework/spock/blob/master/docs/…。所以我很好奇你是怎么知道的。关于测试这个的实际选择:已经做了一个“尖峰”(开发了一点没有测试),我现在回去用最严格的 TDD 做同样的事情。显然,最简单的原则是不经过测试就什么也不做。所以第一个测试是检查 Launcher 是否启动了 App 实例。
  • 另外,起初做这个测试很简单(使用全局GroovyMock),这是我做的第一个测试,直到我后来不得不添加对象和 bytebuddy 依赖项,因为一个简单的 Spock Mock 给出了这个错误:“非接口类型的模拟需要代码生成库。”
  • 从关于点菜模拟的手册章节中,我知道模拟、存根、间谍可以采用可选参数。剩下的就是通过 IntelliJ IDEA 中的 Ctrl-B 或 Eclipse 中的 F3 查看source code
  • 至于使用普通的mock,是不行的,因为普通的mock,作为动态代理,对静态方法没有影响。
  • 抱歉,在我关于简单 Spock Mock 的第二条评论中,我实际上指的是 another 测试,这个其他测试是我必须包括 objenesis 和 bytebuddy 的原因.感谢您对探索其他参数的解释。
猜你喜欢
  • 2020-03-18
  • 2015-10-03
  • 2019-12-05
  • 1970-01-01
  • 2013-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-24
相关资源
最近更新 更多