如何测试 JavaFX (MVC) 控制器逻辑?

How to test JavaFX (MVC) Controller Logic?

我们如何为 JavaFX 控制器逻辑正确编写 unit/integration 测试? 假设我正在测试的控制器 class 被命名为 LoadController,并且它的单元测试 class 是 LoadControllerTest,我的困惑源于:

我现在使用的是前一种方法,还有更好的方法吗?

TestFX

答案 涉及 TestFX,它 @Tests 基于主应用程序的 start 方法 而不是 控制器 class .它显示了一种使用

测试控制器的方法
     verifyThat("#email", hasText("test@gmail.com"));

但这个答案涉及 DataFX - 而我只是询问 JavaFX 的 MVC 模式。大多数 TestFX 讨论都集中在它的 GUI 功能上,所以我很好奇它是否也适用于控制器。

以下示例展示了我如何向控制器注入 VBox 以便它在测试期间不为空。有没有更好的办法?请具体

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

更新

这是一个基于 hotzst 的建议的完整演示,但是 returns 这个错误:

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

您可以使用 Mockito 之类的测试框架将您的依赖项注入控制器。因此,您可能可以放弃大部分设置器,至少是那些仅用于方便测试的设置器。

根据您提供的示例代码,我调整了测试中的 class(为 FileSorter 定义了一个内部 class):

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

@FXML 注释在这里没有任何意义,因为没有附加 fxml 文件,但它似乎对代码或测试没有任何影响。

您的测试 class 可能看起来像这样:

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

此测试成功运行,输出如下:

NON-NULL VBOX

@Rule JavaFXThreadingRule 可以省略,因为当这样测试时,您不会 运行 通过任何应该在 JavaFX 中执行的代码部分线程。

@Mock 注释与 MockitoJUnitRunner 一起创建一个模拟实例,然后将其注入到用 @InjectMocks 注释的实例中。

可以找到一个很好的教程here. There are also other frameworks for mocking in tests like EasyMock and PowerMock,但 Mockito 是我使用和最熟悉的。

我使用了 Java 8 (1.8.0_121) 和 Mockito 1.10.19.

如果您想通过与 UI 交互来测试控制器,TestFX 可能是您的一个选择。

我创建了一个简单的测试项目来展示它的功能:
https://github.com/ArchibaldBienetre/javaFxTestGradle

在此处查找完整的测试用例:https://github.com/ArchibaldBienetre/javaFxTestGradle/blob/main/src/integrationTest/java/com/example/javafxtest/integrationtest/FileChooserApplicationTest.java