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

时间:2017-01-19 02:16:53

标签: java unit-testing javafx controller testfx

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

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

  • 如果我改为使用FXMLLoader loader.getController()方法检索loadController,它会正确初始化FXML控件但 因此调用控制器initialize()导致运行速度非常慢,并且由于无法注入模拟的依赖关系,因此更多的集成测试写得不好。

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

TestFX

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

测试控制器的方法
     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'类型'类com.mypackage.LoadController'。您   没有在现场声明中提供实例,所以我尝试了   构造实例。但是构造函数还是初始化   block抛出异常: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!
        }
    }
}

1 个答案:

答案 0 :(得分:2)

您可以使用像Mockito这样的测试框架在控制器中注入依赖项。因此,您可以放弃大多数的设置者,至少是那些仅用于促进测试的设置者。

使用您提供的示例代码我调整了测试中的类(定义{{​​1}}的内部类):

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

此测试通过以下输出成功运行:

  

非空VBOX

可以省略@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(); } } @Rule,因为在这样的testin中,你没有运行应该在JavaFX线程中执行的任何代码部分。

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

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

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