【问题标题】:How to test JavaFX (MVC) Controller Logic?如何测试 JavaFX (MVC) 控制器逻辑?
【发布时间】:2017-06-03 14:42:23
【问题描述】:

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

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

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

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

TestFX

答案here 涉及TestFX,它具有基于主应用程序的start 方法不是 Controller 类的@Tests。它显示了一种使用

测试控制器的方法
     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 建议的完整演示,但它返回此错误:

org.mockito.exceptions.base.MockitoException:无法实例化 @InjectMocks 字段名为'loadController',类型为'class com.mypackage.LoadController'。你 没有在字段声明中提供实例,所以我试图 构造实例。但是构造函数或初始化 块抛出异常: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 未知,所以我使用了一个空的静态内部类,在测试中JavaFXThreadingRule 可以省略。
  • @hotzst 你是说我的代码可以正常工作,打印"NON-NULL VBOX"?我知道您没有 FileSorter 类,但这只是为了代表我可能拥有的任何私人实例。我的控制器恰好有大约 10 个类似的实例化,但 @Mock 注释似乎没有对它们生效。为什么FileSorter 会在这里创建一个真实的对象而不使用测试的模拟?好奇怪!
  • @hotzst 如果我只是注释掉private FileSorter fileSorter = new FileSorter();,那么代码就可以工作,返回"NON-NULL VBOX"。为什么@Mock private FileSorter fileSorter;代码不生效?
  • 是的,运行测试时它成功运行,我在控制台上打印的输出确实是ON-NULL VBOXfileSorter 不应该造成问题,即使在使用 new 实例化变量时,它应该被测试中的模拟覆盖。您可以尝试将初始化移动到构造函数中。
  • @hotzst 你已经证明了你的观点,应该得到 +1。虽然,我觉得我最初的一些困惑仍然存在,例如......这是最合适的方法吗?二传手很笨重,但也许他们更好地保持封装。 @InjectMocks 似乎是类似于通过WhiteBox 注入的反射黑客。想法?

标签: java unit-testing javafx controller testfx


【解决方案1】:

您可以使用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 注释的实例中。

可以在here 找到一个很好的教程。还有其他用于模拟测试的框架,例如 EasyMockPowerMock,但 Mockito 是我使用并且最熟悉的。

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

【讨论】:

  • 很抱歉,但我不明白这如何回答我的问题。您只展示了如何实例化模拟的mainVBox,但展示了如何将其注入到 loadController 对象中,这正是我感到困惑的地方。您说 Mockito 可以做到这一点,但您的示例并未显示如何将模拟的 VBox 实际注入到 LoadController 类(SUT)中。另外,我假设我在setUp 方法中继续做loadController.initialize(url, rb);?你的回答还没有教给我任何东西,所以我不能投票,请澄清
  • 鉴于您的代码,我的 SUT 仍会在 if (mainVBox != null) {... 这一行返回 null,因为它的值未设置或模拟。
  • @Mathomatic,你是对的,代码错过了那一点(@InjectMocks 注释),我现在添加了一些解释。
  • 谢谢,但模拟的FileSorter 似乎没有被注入,因为被测类仍在尝试实例化它!我已经更新了我的问题以包括我收到的错误以及一个完整的“工作”演示,它显示了错误发生的位置。我做错了什么?
【解决方案2】:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-05-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多