5

我们如何正确地为 JavaFX 控制器逻辑编写单元/集成测试?假设我正在测试的 Controller 类名为LoadController,它的单元测试类是LoadControllerTest,我的困惑源于:

  • 如果LoadControllerTest该类LoadController通过 I 实例化一个新对象, LoadController loadController = new LoadController();则可以通过(许多)setter 将值注入控制器。这似乎是使用反射(遗留代码)的唯一方法。如果我不将值注入 FXML 控件,那么这些控件显然还没有初始化,返回 null。

  • 如果我改为使用FXMLLoader'loader.getController()方法来检索loadController它,它将正确初始化 FXML 控件,但控制器initialize()会因此被调用,这会导致运行速度非常慢,并且由于无法注入模拟的依赖项,因此它更像是一个编写不佳的集成测试.

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

测试外汇

这里的答案涉及@Tests基于主应用程序start方法而不是Controller 类的 TestFX。它显示了一种测试控制器的方法

     verifyThat("#email", hasText("test@gmail.com"));

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

下面的示例显示了我如何向控制器注入 aVBox以使其在测试期间不为空。有没有更好的办法?请具体

 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 建议的完整演示,但它返回此错误:

org.mockito.exceptions.base.MockitoException:无法实例化名为 'loadController' 类型的 'class com.mypackage.LoadController' 的 @InjectMocks 字段。您没有在字段声明时提供实例,所以我尝试构建实例。但是构造函数或初始化块抛出异常: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!
        }
    }
}
4

2 回答 2

2

您可以使用测试框架,例如Mockito在控制器中注入您的依赖项。因此,您可以放弃可能的大多数二传手,至少是那些仅用于方便测试的二传手。

使用您提供的示例代码,我调整了被测类(为 定义一个内部类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 {}
}

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

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

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

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

非空 VBOX

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

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

可以在这里找到一个优秀的教程。还有其他用于在测试中进行模拟的框架,例如EasyMockPowerMock,但 Mockito 是我使用并且最熟悉的框架。

我将 Java 8 (1.8.0_121) 与 Mockito 1.10.19 一起使用。

于 2017-01-19T09:42:46.137 回答
0

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

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

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

于 2021-12-21T19:29:41.890 回答