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

如何测试JavaFX(MVC)控制器逻辑?,java,unit-testing,javafx,controller,testfx,Java,Unit Testing,Javafx,Controller,Testfx,我们如何正确地为JavaFX控制器逻辑编写单元/集成测试? 假设我正在测试的控制器类名为LoadController,它的单元测试类名为LoadControllerTest,我的困惑源于: 如果LoadControllerTest类通过 LoadController LoadController=新的LoadController()我可以 然后通过(多个)设置器将值注入控制器。这似乎是使用反射(遗留代码)的唯一方法。如果我没有将这些值注入FXML控件,那么这些控件显然还没有初始化,返回null

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

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

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

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

TestFX

答案涉及TestFX,它基于主应用程序的
启动
方法而不是控制器类进行
@Tests
。文中给出了一种测试控制器的方法

     verifyThat("#email", hasText("test@gmail.com"));
但是这个答案涉及到DataFX,而我只是问一下JavaFX的MVC模式。大多数关于TestFX的讨论都集中在它的GUI功能上,所以我很好奇它是否也是控制器的理想选择

下面的示例显示了如何向控制器注入
VBox
,以便在测试过程中不为null。有更好的办法吗?请具体说明

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


更新 下面是一个基于hotzst建议的完整演示,但它返回以下错误:

org.mockito.exceptions.base.MockitoException:无法实例化 @InjectMocks字段名为“loadController”,类型为“class com.mypackage.loadController”。你 没有在字段声明中提供实例,所以我尝试 构造实例。但是,构造函数或初始化 块引发异常:null



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

按照您提供的示例代码,我调整了测试中的类(为
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
注释的实例中

可以找到一个优秀的教程。在测试中也有其他的模拟框架,比如和,但是Mockito是我最熟悉的


我将Java8(1.8.0121)与Mockito 1.10.19一起使用。

对不起,我不知道这是如何回答我的问题的。您只演示了如何实例化模拟的
mainVBox
,但没有演示如何将其注入loadController对象,这正是我所困惑的。您说Mockito可以做到这一点,但您的示例没有显示如何将模拟的
VBox
注入
LoadController
类(SUT)。另外,我假设我继续执行
loadController.initialize(url,rb)
设置中
方法?你的答案还没有教给我任何东西,所以我无法对它进行升级,请澄清,如果(mainVBox!=null){…
,我的SUT在这一行上仍然会返回null,因为它的值没有设置或模拟。@Mathomatic,你是对的,代码遗漏了那一位(
@InjectMocks
注释),我现在添加了一些解释。谢谢,但是模拟的
FileSorter
似乎没有被注入,因为被测试的类仍然试图实例化它!我已经更新了我的问题,以包括我收到的错误以及完整的“工作”演示显示了错误发生的位置。我做错了什么?我无法重现您提到的错误,但是我必须稍微调整一下您的示例:
FileSorter
未知,所以我使用了一个空的静态内部类,在测试中可以省略
JavaFXThreadingRule
。@hotzst您是说您得到了我的cod吗要正确工作,请打印
“非空VBOX”
?我知道你没有
FileSorter
类,但它只是代表我可能拥有的任何私人安装的类。我的控制器碰巧有10个类似的实例化,但
@Mock
注释似乎对它们无效。为什么
FileSorter
会创建一个真正的对象这里没有使用测试的模拟?太奇怪了!@hotzst如果我简单地注释掉
private FileSorter FileSorter=new FileSorter();
,那么代码就会工作,返回
“非空VBOX”
。为什么
@Mock private FileSorter FileSorter;
代码没有生效?是的,当运行测试时,它成功运行,并且我在控制台上打印的输出确实是
on-NULL VBOX
FileSorter
不应该引起问题,即使在用new实例化变量时,这应该是be由主运行中心覆盖
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!
        }
    }
}
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 {}
}
@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();
    }
}