Reputation: 917
I want to test a function in which a new View is created within TornadoFX. When i call the function however, i get this error.
java.lang.ExceptionInInitializerError
at tornadofx.ControlsKt.button(Controls.kt:190)
at tornadofx.ControlsKt.button$default(Controls.kt:190)
at view.PeopleMenuView$setupTopBox$1$1.invoke(PeopleMenuView.kt:33)
at view.PeopleMenuView$setupTopBox$1$1.invoke(PeopleMenuView.kt:8)
at tornadofx.LayoutsKt.vbox(Layouts.kt:388)
at tornadofx.LayoutsKt.vbox$default(Layouts.kt:103)
at view.PeopleMenuView$setupTopBox$1.invoke(PeopleMenuView.kt:31)
at view.PeopleMenuView$setupTopBox$1.invoke(PeopleMenuView.kt:8)
at tornadofx.LayoutsKt.hbox(Layouts.kt:384)
at tornadofx.LayoutsKt.hbox$default(Layouts.kt:96)
at view.PeopleMenuView.setupTopBox(PeopleMenuView.kt:29)
at view.PeopleMenuView.<init>(PeopleMenuView.kt:15)
at presenter.MainMenuPresenter.managePeoplePressed(MainMenuPresenter.kt:11)
at presenter.TestMainMenuPresenter.testManagePeoplePressed(TestMainMenuPresenter.kt:16)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.IllegalStateException: Toolkit not initialized
at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:273)
at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:268)
at com.sun.javafx.application.PlatformImpl.setPlatformUserAgentStylesheet(PlatformImpl.java:550)
at com.sun.javafx.application.PlatformImpl.setDefaultPlatformUserAgentStylesheet(PlatformImpl.java:512)
at javafx.scene.control.Control.<clinit>(Control.java:87)
... 36 more
It is because a new instance of a view is created in the function. The simplified code looks like this:
fun managePeoplePressed() {
view.replaceWith(PeopleMenuView())
}
When i call the method from a test, i get the error. I googled around but there's not much to find about this.
I want to be able to test methods that create a view. Thank you.
Upvotes: 0
Views: 448
Reputation: 29
In general: you don't want to test UI views. Hold tight, I'm still going to tell you how. I just want you to know it's a rare case that can be indicating design problems.
Without TestFx such tests are quite difficult to write. With TestFx they're easier, but still both very slow and very fragile, and you'll have to take extra steps in any of the standard CI environments to allow them to run on the build boxes, which are normally not equipped to run JavaFx and not equipped with virtual displays.
The biggest problem you (and TestFx) encounter is with getting your threads right inside the test. The tests are on one thread. The visual parts of JavaFx are on another. Your own application and JavaFx itself frequently pour their tasks into Platform.RunLater(), and if you don't account for the emptying of that queue, you'll get results that are either uniformly wrong or, worse, flicker-y. Sleeps work in some cases, but a) are sleeps and slow you down, and b) won't work as well when you run on a slower box, like a low-configured windows box in the cloud.
Back to your general question: It likely means that your intra-View connections are complex, where UI component X depends on UI component Y. Broadly, you want UI component X to depend on properties in the Model, and you want UI component Y to depend on properties in the Model, and you want the Model handling the complex interactions between properties. TornadoFx has direct support for this, and Model classes don't need the UI running to be tested. For some cases, the Controller is the place to put that interconnected logic, but that's relatively rare. Most of what I do just doesn't call for that, but it does call for Model classes. The key insight that (Model != Domain) is well-supported in TornadoFx's ViewModel and ItemViewModel classes.
Having said all that, if you need robot-control over the view, the way to do it is to use TestFx. If you don't, this is agreeing with and elaborating on Edvin's answer, is to have something like this:
class UiTest {
companion object {
private var javaFxRunning: Boolean = false
fun start() {
StartWith.isUi = true
Errors.reallyShow = false
try {
runJavaFx()
} catch (e: InterruptedException) {
throw RuntimeException(e)
}
}
@Throws(InterruptedException::class)
private fun runJavaFx() {
if (javaFxRunning) return
val latch = CountDownLatch(1)
SwingUtilities.invokeLater {
JFXPanel()
latch.countDown()
}
latch.await()
javaFxRunning = true
}
}
}
In the @BeforeEach or even in individual @Tests, call UiTest.start(), which will only start the javafx one time no matter how many tests need it running.
My actual experience: the JavaFx Component <-> property relationship "just works". That is, I gain very little from testing it, as it's not my code, and it works "every time". What is my code and doesn't work every time is the relationship between the properties. That's why I use ViewModel, put the interactions between properties there, and test them rigorously with microtests, which doesn't require the JavaFx thread to be running. (JavaFx properties use no multi-threading in their callbacks, so addListener targets are called synchronously.) This is particularly handy when you need to be clever with your bindings. Inlining complex JavaFx bindings inside the TornadoFx view builder is a mug's game. Extracting them to the model is dead-easy.
I learned all this by trial and error. There is a third approach, described in this stack overflow, that can be made to work. It amounts to making 100% of your tests run in the JavaFx thread. I found it difficult, because most of my work isn't on that thread in production. Basic JUnit test for JavaFX 8
Good luck, and reach out if you need more help! GeePawHill
P.S. Shouts out to Edvin: Great work on TornadoFx. I use it every day.
Upvotes: 2
Reputation: 7297
You need to initialize the JavaFX Toolkit. If you're using TestFX you can make a call to FxToolkit.registerPrimaryStage()
, if not you can instantiate a JFXPanel
to achieve the same goal.
Upvotes: 1