如何使用mockito模拟JavaFX控件?

mo49yndu  于 2023-10-18  发布在  Java
关注(0)|答案(1)|浏览(127)

我目前正在开发一个简单的JavaFX MVC/MVP来测试我是否可以让JavaFX的任何单元测试工作。我的目标是在单元测试中测试JavaFX对话框或控件。为了使我的单元测试尽可能独立于任何JavaFX调用,我想尝试模拟任何JavaFX调用。但不幸的是,我正在努力让它正常工作。我也不想使用TestFX,因为缺乏适当的文档。
到目前为止,我对Java中的单元测试还是有点陌生,但我确实有用c++编写单元测试的经验(google test/mock)
规格:

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

到现在为止我有什么?
我已经创建了一个View类,它负责更新对话框,注册按钮的操作以及显示对话框。

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);
    }
    
}

此外,我还有一个视图的单元测试,其中有三个测试:

  • 对于构造函数
  • 用于更新标签
  • 对于这个单元测试,我已经有了一个JavaFX线程规则(参见How do you mock a JavaFX toolkit initialization?),它负责启动一个JavaFX线程。对于我用mockito创建的按钮、标签和stage,它们是通过构造函数注入的(用于测试的快速破解)。当我创建模拟时,我希望对JavaFX的调用被模拟,但似乎仍然缺少一些东西。
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();
    }
    
}

到目前为止,我还尝试了哪些其他东西?

也许有人有一个解决方案或一个例子给我,或者我只是错过了一些小的东西。如果我能得到任何帮助,我将不胜感激。
所有最好的

64jmpszr

64jmpszr1#

我希望我的回答能对你有帮助,因为你已经很久没有问过这个问题了,但它可以帮助别人。如果你分享你的测试输出,不管有没有错误,这都会有所帮助,但是我之前也是这样。首先,您需要了解JavaFX、Mockito和JUnit平台是如何工作的。它们是框架,在Java虚拟机中执行的方式是独特的。任何属于JavaFX领域的工件都需要JavaFX的运行时,然后才能按照您的预期方式工作。这就是为什么main类(包含main方法的类)必须子类化JavaFX的Application类并覆盖它的“start”方法。当你的应用程序运行时,java知道要寻找这个方法,并且你的应用程序在JavaFX的域中运行,允许所有JavaFX的工件在你的代码中执行。换句话说,一个特定于JavaFX的线程将被启动,这就是你的应用程序将被执行的地方,远离主线程。其次,当您使用Junit运行测试时,您不需要创建一个主类及其main方法。Junit的框架在后台为您完成了这一工作,并执行了您的测试。Junit不知道JavaFX,所以你的测试应该失败,并抛出IllegalStateException,因为JavaFX线程还没有启动。Junit不会自动完成这一任务。需要启动JavaFX的主线程,所有JavaFX代码都需要在该线程上运行。在我的情况下,我需要测试我的JavaFX应用程序中的一些业务逻辑,这些是应用程序中不需要JavaFX工件的部分。我有一个这样的

@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
    }

你可以看到调用GUI来显示的部分,那是JavaFX代码。这个方法在一个单独的线程上运行,因为我在应用程序中使用了并发,当我需要从一个不是主JavaFX线程的线程调用JavaFX代码(比如更新GUI组件)时,我必须将我的语句 Package 在Runnable中,将其交给“runLater”方法,它会在JavaFX的线程上安全地执行它们。在这里阅读关于它。如前所述测试此方法将在JavaFx的线程未运行时抛出IllegalStateException。因此,为了将JavaFX框架引入到我的测试中,我不得不在我的测试中添加下面这行代码。

Platform.startup(runnable);

你只需要简单地将你的测试放在可运行对象中,并在你的测试方法中调用那一行。这将JavaFX框架引入到您的测试中,并且它的工件会按照它们应该的方式执行,而Junit不会以任何方式受到损害。你可以在这里的JavaFX 17文档中阅读关于这个类的内容。这是对我有效的测试

@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将调用JavaFX框架,允许您顺利执行测试。因此,请始终记住,所有JavaFX工件都应该在JavaFX的线程上执行或调用,而不是在任何其他线程上执行或调用。调用Platform.startup(runnable);将确保JavaFX的主线程启动,或者使您的主类成为Application的子类,如果您必须从非JavaFX线程的线程执行任何JavaFX,请使用Platform.runLater​(Runnable runnable)

相关问题