I am testing using Spring Boot with JavaFX (Based on some excellent YouTube videos that explain this).
To make it work with TestFX, I need to create the context like this:
@Override
public void init() throws Exception {
SpringApplicationBuilder builder = new SpringApplicationBuilder(MyJavaFXApplication.class);
builder.headless(false); // Needed for TestFX
context = builder.run(getParameters().getRaw().stream().toArray(String[]::new));
FXMLLoader loader = new FXMLLoader(getClass().getResource("main.fxml"));
loader.setControllerFactory(context::getBean);
rootNode = loader.load();
}
I now want to test this JavaFX application, for this I use:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class MyJavaFXApplicationUITest extends TestFXBase {
@MockBean
private MachineService machineService;
@Test
public void test() throws InterruptedException {
WaitForAsyncUtils.waitForFxEvents();
verifyThat("#statusText", (Text text ) -> text.getText().equals("Machine stopped"));
clickOn("#startMachineButton");
verifyThat("#startMachineButton", Node::isDisabled);
verifyThat("#statusText", (Text text ) -> text.getText().equals("Machine started"));
}
}
This starts a Spring context and replaces the "normal" beans with the mock beans as expected.
However, I now get a java.awt.HeadlessException
because this 'headless' property is not set to false like is done during normal startup. How to I set this property during the test?
EDIT:
Looking closer it seems that there are 2 context started, one that the Spring testing framework starts and the one I create manually in the init
method, so the application under test is not using the mocked beans. If somebody would have a clue how to get the test context reference in the init()
method, I would be very happy.
The comment from Praveen Kumar pointed in the good direction. When I run the test with -Djava.awt.headless=false
, then there is no exception.
To solve the other problem of the 2 Spring contexts, I had to do the following:
Suppose this is your main JavaFx startup class:
@SpringBootApplication
public class MyJavaFXClientApplication extends Application {
private ConfigurableApplicationContext context;
private Parent rootNode;
@Override
public void init() throws Exception {
SpringApplicationBuilder builder = new SpringApplicationBuilder(MyJavaFXClientApplication.class);
builder.headless(false); // Needed for TestFX
context = builder.run(getParameters().getRaw().stream().toArray(String[]::new));
FXMLLoader loader = new FXMLLoader(getClass().getResource("main.fxml"));
loader.setControllerFactory(context::getBean);
rootNode = loader.load();
}
@Override
public void start(Stage primaryStage) throws Exception {
Rectangle2D visualBounds = Screen.getPrimary().getVisualBounds();
double width = visualBounds.getWidth();
double height = visualBounds.getHeight();
primaryStage.setScene(new Scene(rootNode, width, height));
primaryStage.centerOnScreen();
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void stop() throws Exception {
context.close();
}
public void setContext(ConfigurableApplicationContext context) {
this.context = context;
}
}
And for testing, you use this abstract base class (Courtesy of this YouTube video by MVP Java):
public abstract class TestFXBase extends ApplicationTest {
@BeforeClass
public static void setupHeadlessMode() {
if (Boolean.getBoolean("headless")) {
System.setProperty("testfx.robot", "glass");
System.setProperty("testfx.headless", "true");
System.setProperty("prism.order", "sw");
System.setProperty("prism.text", "t2k");
System.setProperty("java.awt.headless", "true");
}
}
@After
public void afterEachTest() throws TimeoutException {
FxToolkit.hideStage();
release(new KeyCode[0]);
release(new MouseButton[0]);
}
@SuppressWarnings("unchecked")
public <T extends Node> T find(String query, Class<T> clazz) {
return (T) lookup(query).queryAll().iterator().next();
}
}
Then you can write a test like this:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class MyJavaFXApplicationUITest extends TestFXBase {
@MockBean
private TemperatureService temperatureService;
@Autowired
private ConfigurableApplicationContext context;
@Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("main.fxml"));
loader.setControllerFactory(context::getBean);
Parent rootNode = loader.load();
stage.setScene(new Scene(rootNode, 800, 600));
stage.centerOnScreen();
stage.show();
}
@Test
public void testTemperatureReading() throws InterruptedException {
when(temperatureService.getCurrentTemperature()).thenReturn(new Temperature(25.0));
WaitForAsyncUtils.waitForFxEvents();
assertThat(find("#temperatureText", Text.class).getText()).isEqualTo("25.00 C");
}
}
This allows to start the UI using mock services.
The @SpringBootTest uses SpringBootContextLoader class as context loader, so the ApplicationContext is load from the method SpringBootContextLoader.loadContext
as this:
SpringApplication application = getSpringApplication();
......
return application.run();
When invoke the method application.run()
, application configure the system headless property with it's internal headless property.
So, If we want to set 'headless' property in a Spring Boot test, just create a customize specific ContextLoader class extends SpringBootContextLoader class, and override the method getSpringApplication
with set the headless property as false, then assign the specific ContextLoader with annotation @ContextConfiguration for @SpringBootTest. The code:
public class HeadlessSpringBootContextLoader extends SpringBootContextLoader {
@Override
protected SpringApplication getSpringApplication() {
SpringApplication application = super.getSpringApplication();
application.setHeadless(false);
return application;
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.NONE)
@ContextConfiguration(loader = HeadlessSpringBootContextLoader.class)
public class ApplicationTests {
@Test
public void contextLoads() {
}
}