1

I am currently working on a simple JavaFX MVC/MVP to test if I could get any unit tests for JavaFX working. My goal is to test a JavaFX dialog or control in a unit test. To make my unit tests as independent as possible from any JavaFX calls I want to try to mock any JavaFX calls. But unfortunately I am struggling to get it working properly. I also don't want to work with TestFX because of the lack of proper documentation.

Until now I am still a bit new to unit testing in Java, but I do have experience in writing unit tests in c++ (google test/mock)

Specs:

  • JDK 8
  • Mockito 3.5.13
  • JUnit 4.12
  • Hamcrest 1.3

What do I have until now?

I have created a View class which is responsible for updating the dialog, for registering the actions for a button and for showing the dialog.

public class View {
    
    private Stage stage;
    private Label label;
    private Button button;

    public View(Stage stage, Label label, Button button) {
        this.stage = stage;
        this.label = label;
        this.button = button;
    }

    public void updateLabel(int count) {
        label.setText("Countdown: " + count);
    }

    public void ShowStage() {
        //button.setText("Count");
        //stage.setScene(new Scene(new VBox(label, button)));
        stage.show();
    }

    public void RegisterButtonAction(EventHandler<ActionEvent> value) {
        button.setOnAction(value);
    }
    
}

Additionally I also have a Unit test for the view, in which I have three tests:

  • for the constructor
  • for updating a label
  • for showing the dialog For this unit test I already have a JavaFX threading rule (see How do you mock a JavaFX toolkit initialization?), which is responsible for starting a JavaFX thread. For the button, the label and the stage I created mocks with mockito, they are injected via the constructor (fast hack for testing). As I created mocks, I expected that the calls to JavaFX are mocked, but it seems that there is still something missing.
public class ViewTest {
    
    @org.junit.Rule
    public JavaFXThreadingRule rule = new JavaFXThreadingRule();
    
    private Stage m_stageMock;
    private Label m_labelMock;
    private Button m_buttonMock;
    
    private View m_testee;
    
    @Before
    public void SetUp() throws InterruptedException {
        m_stageMock = mock(Stage.class);
        m_labelMock = mock(Label.class);
        m_buttonMock = mock(Button.class);
        m_testee = new View(m_stageMock, m_labelMock, m_buttonMock);
    }
    
    @After
    public void TearDown(){
        m_testee = null;
        m_stageMock = null;
        m_labelMock = null;
        m_buttonMock = null;
    }
    
    @Test
    public void Constructor_HappyPath_NoCrash() {
        
    }
    
    @Test
    public void UpdateLabel_IntegerGiven_PrintNumber() {
        int count = 8;
        doNothing().when(m_labelMock).setText("Countdown: " + count);
        m_testee.updateLabel(count);
    }
    
    @Test
    public void ShowStage_HappyPath_ShowsWindowWithButtonAndLabel() {
        VBox vbox = new VBox();
        Scene scene = new Scene(vbox);
        //doNothing().when(m_buttonMock).setText("Count");
        //doNothing().when(m_stageMock).setScene(any(Scene.class));
        doNothing().when(m_stageMock).show();
        m_testee.ShowStage();
    }
    
}

What additional things have I tried so far?

Maybe someone has a solution or an example for me, or I am just missing something small. I would be very grateful if I could get any help.

All the best

Fossa

Fossa
  • 31
  • 4

1 Answers1

0

I hope my answer helps after it's been a long time since you asked this question but it can help someone out there. It would have helped if you shared the output from your tests, error or not, but I was in your shoes a while ago. First, you need to understand how the platforms JavaFX, Mockito, and JUnit work. They are frameworks and unique in how they execute within the Java virtual machine. Any artifact that belongs to the domain of JavaFX will require JavaFX's runtime before they work the way you intend them to. This is why the main class (the class where the main method is contained) must subclass JavaFX's Application class and override its "start" method. When your application runs java knows to look for this method and your application runs within the domain of JavaFX, allowing all JavaFX's artifacts to execute as implemented in your code. In other words, a thread specific to JavaFX will be started and this is where your application will be executed, away from the main thread. Second, when you run your tests with Junit you are not required to create a main class and its main method. Junit's framework does this for you behind the scenes and executes your tests. Junit is not aware of JavaFX so your tests should fail, and throw IllegalStateException because the JavaFX thread hasn't been started. Junit does not do that automatically. JavaFX's main thread needs to be started and all JavaFX code needs to run on that thread. In my situation, I needed to test some business logic in my JavaFX application and these are the parts of the application that don't require JavaFX artifacts. I had one of them like so

@Override
    public void onVendRequest(VendRequestVendronEvent event) {
        // The paying amount entered by the customer on Vendron's sale UI is passed over to the application's GUI
        CardPaymentController.setPayingAmount(event.getRequiredPrice());

        // Show the GUI (there's a lag of about two to three seconds)
        Platform.runLater(() -> super.stage.show());
        this.socketPayLogger.info("Showing GUI");

        // More code below
    }

Well, you see the part that calls the GUI to be displayed, that's JavaFX code. This method runs on a separate thread because I employed concurrency in my application, and when I needed to call JavaFX code (like updating GUI components) from a thread that is not the main JavaFX thread I had to wrap my statements in a Runnable, hand it over to the "runLater" method and it safely executes them on the JavaFX's thread. Read about it here. Testing this method as said earlier will throw an IllegalStateException when JavaFx's thread is not running. So to bring JavaFX framework into my test I had to add the line below in my tests.

Platform.startup(runnable);

You just have to simply place your tests in the runnable object and call that line in your test method. This brings over the JavaFX framework into your tests and its artifacts are executed as they should all the while Junit is not compromised in any way. You can read up about the class in the JavaFX 17 docs here. Here's the test that worked for me

@Test
void testOnVendRequest(){
    Runnable runnable = () -> {
        try {
            n6DeviceConnection.createSerialConnection();
            assertTrue(n6DeviceConnection.isConnected());
             Optional<VendronSocketMessage> optional = controller.create("External PC Plugin;Vend Request;add_on_variable=&required_price=200#");

            if (optional.isPresent()){
                VendronEvent vendronEvent = vendronEventFactory.createVendronEvent(optional.get(), new Object());
                posPay.onVendRequest((VendRequestVendronEvent) vendronEvent);
            }
        } catch (SerialPortException e) {
            throw new RuntimeException(e);
        }
    };

    Platform.startup(runnable);
    while (true){
        if (posPay.isPaymentTransactionComplete()){
            String string = posPay.getFinishedTransactionReport();
            assertInstanceOf(String.class, string);
            break;
        }
    }
}

JUnit will call the JavaFX framework allowing smooth execution of your tests. So, always remember that all JavaFx artifacts should be executed or called on the JavaFX's thread and not any other thread. Calling Platform.startup(runnable); will ensure JavaFX's main thread is started or make your main class the subclass of Application and if you must execute any JavaFX from a thread that's not the JavaFX thread use Platform.runLater​(Runnable runnable)