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 newLoadController
object viaLoadController 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
'sloader.getController()
method to retrieve theloadController
it will properly initialize the FXML controls but the controller'sinitialize()
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("[email protected]"));
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 ...
}
UPDATE
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!
}
}
}
FileSorter
was unknown, so I used an empty static inner class and in the Test theJavaFXThreadingRule
can be omitted. – Bawdry"NON-NULL VBOX"
? I understand you don't have theFileSorter
class, but it's merely to represent any privately instatiated class I might have. My controller happens to have ~10 similar instantiations but the@Mock
annotation doesn't seem to take effect for them. Why the heck wouldFileSorter
be creating a real object here and not using the test's mock? So strange! – Dealateprivate FileSorter fileSorter = new FileSorter();
then the code works, returning"NON-NULL VBOX"
. Why is the@Mock private FileSorter fileSorter;
code not taking effect? – DealateON-NULL VBOX
. ThefileSorter
should not pose a problem, even when instantiating the variable with new, which should be overriden by the mock in the test. What you can try is to move the initialisation into the constructor. – Bawdry@InjectMocks
seems like a reflection hack similar to injecting viaWhiteBox
. Thoughts? – Dealate