How do we properly write unit/integration tests for the JavaFX Controller logic?
Assuming the Controller class I'm testing is named LoadController
, and it's unit test class is LoadControllerTest
, my confusion stems from:
If the LoadControllerTest
class instantiates a new LoadController
object via
LoadController loadController = new LoadController();
I can
then inject values into the controller via (many) setters. This seems the only way short of using reflection (legacy code). If I don't inject the values into the FXML controls then the controls obviously aren't initialized yet, returning null.
If I instead use FXMLLoader
's loader.getController()
method to retrieve the loadController
it will properly initialize the FXML controls but
the controller's initialize()
is thus invoked which results in a very slow run, and since there's no way to inject the mocked dependencies, it's more of an integration test poorly written.
I'm using the former approach right now, but is there a better way?
TestFX
The answer here involves TestFX which has @Tests
based around the main app's start
method not the Controller class. It shows a method of testing the controller with
verifyThat("#email", hasText("test@gmail.com"));
but this answer involves DataFX - whereas I'm simply asking about JavaFX's MVC pattern. Most TestFX discussion focuses on it's GUI capabilities, so I'm curious whether it's ideal for the controller too.
The following example shows how I inject the controller with a VBox
so that it isn't null during the test. Is there a better way? Please be specific
public class LoadControllerTest {
@Rule
public JavaFXThreadingRule javafxRule = new JavaFXThreadingRule();
private LoadController loadController;
private FileSorter fileSorter;
private LocalDB localDB;
private Notifications notifications;
private VBox mainVBox = new VBox(); // VBox to inject
@Before
public void setUp() throws MalformedURLException {
fileSorter = mock(FileSorter.class); // Mock all dependencies
when(fileSorter.sortDoc(3)).thenReturn("PDF"); // Expected result
loadController = new LoadController();
URL url = new URL("http://example.com/");
ResourceBundle rb = null;
loadController.initialize(url, rb); // Perhaps really dumb approach
}
@Test
public void testFormatCheck() {
loadController.setMainVBox(mainVBox); // set value for FXML control
assertEquals("PDF", loadController.checkFormat(3));
}
}
public class LoadController implements Initializable {
@FXML
private VBox mainVBox; // control that's null unless injected/instantiated
private FileSorter fileSorter = new FileSorter(); // dependency to mock
@Override
public void initialize(URL location, ResourceBundle resources) {
//... create listeners
}
public String checkFormat(int i) {
if (mainVBox != null) { // This is why injection was needed, otherwise it's null
return fileSorter.sortDoc(i);
}
return "";
}
public void setMainVBox(VBox menuBar) {
this.mainVBox = mainVBox; // set FXML control's value
}
// ... many more setters ...
}
Here's a complete demo based on hotzst's suggestions, but it returns this error:
org.mockito.exceptions.base.MockitoException: Cannot instantiate @InjectMocks field named 'loadController' of type 'class com.mypackage.LoadController'. You haven't provided the instance at field declaration so I tried to construct the instance. However the constructor or the initialization block threw an exception : null
import javafx.scene.layout.VBox;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class LoadControllerTest {
@Rule
public JavaFXThreadingRule javafxRule = new JavaFXThreadingRule();
@Mock
private FileSorter fileSorter;
@Mock
private VBox mainVBox;
@InjectMocks
private LoadController loadController;
@Test
public void testTestOnly(){
loadController.testOnly(); // Doesn't even get this far
}
}
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.layout.VBox;
import java.net.URL;
import java.util.ResourceBundle;
public class LoadController implements Initializable {
private FileSorter fileSorter = new FileSorter(); // Fails here since creates a real object *not* using the mock.
@FXML
private VBox mainVBox;
@Override
public void initialize(URL location, ResourceBundle resources) {
//
}
public void testOnly(){
if(mainVBox==null){
System.out.println("NULL VBOX");
}else{
System.out.println("NON-NULL VBOX"); // I want this to be printed somehow!
}
}
}
You can use a test framework like Mockito
to inject your dependencies in the controller. Thereby you can forgo probably most of the setters, at least the ones that are only present to facilitate testing.
Going with the example code you provided I adjusted the class under test (define an inner class for the FileSorter
):
public class LoadController implements Initializable {
private FileSorter fileSorter = new FileSorter();
@FXML
private VBox mainVBox;
@Override
public void initialize(URL location, ResourceBundle resources) {
//
}
public void testOnly(){
if(mainVBox==null){
System.out.println("NULL VBOX");
}else{
System.out.println("NON-NULL VBOX");
}
}
public static class FileSorter {}
}
The @FXML
annotation does not make any sense here, as no fxml file is attached, but it does not seem to have any effect on the code or Test.
Your test class could then look something like this:
@RunWith(MockitoJUnitRunner.class)
public class LoadControllerTest {
@Mock
private LoadController.FileSorter fileSorter;
@Mock
private VBox mainVBox;
@InjectMocks
private LoadController loadController;
@Test
public void testTestOnly(){
loadController.testOnly();
}
}
This test runs through successfully with the following output:
NON-NULL VBOX
The @Rule
JavaFXThreadingRule
can be ommited, as when testin like this you are not running through any part of code that should be executed in the JavaFX Thread.
The @Mock
annotation together with the MockitoJUnitRunner
creates a mock instance that is then injected into the instance annotated with @InjectMocks
.
An excellent tutorial can be found here. There are also other frameworks for mocking in tests like EasyMock and PowerMock, but Mockito is the one I use and am most familiar with.
I used Java 8 (1.8.0_121) together with Mockito 1.10.19.