对象依赖导致实例化错误
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)
}
}
刚刚开始一个新的 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)
}
}