如何测试JavaFX(MVC)控制器逻辑?
我们如何正确编写JavaFX Controller逻辑的单元/集成测试?
假设我正在测试的Controller类名为 LoadController
,它的单元测试类是 LoadControllerTest
,我的困惑源于:
How do we properly write unit/integration tests for the JavaFX Controller logic?
Assuming the Controller class I'm testing is named LoadController
, and it's unit test class is LoadControllerTest
, my confusion stems from:
-
如果
LoadControllerTest
类实例化一个新的LoadController
对象通过LoadController loadController = new LoadController();
我可以
然后注入值通过(许多)设置器进入控制器。这似乎是使用反射(遗留代码)的唯一方法。如果我没有将值注入FXML控件,那么控件显然还没有初始化,返回null。
If the
LoadControllerTest
class instantiates a newLoadController
object viaLoadController loadController = new LoadController();
I can then inject values into the controller via (many) setters. This seems the only way short of using reflection (legacy code). If I don't inject the values into the FXML controls then the controls obviously aren't initialized yet, returning null.
如果我改为使用 FXMLLoader
的 loader.getController()
检索 loadController
的方法将正确初始化FXML控件,但
因此调用控制器的 initialize()
,这会导致运行速度非常慢,并且因为没有办法注入模拟的依赖项,它更像是一个写得不好的集成测试。
If I instead use FXMLLoader
's loader.getController()
method to retrieve the loadController
it will properly initialize the FXML controls but
the controller's initialize()
is thus invoked which results in a very slow run, and since there's no way to inject the mocked dependencies, it's more of an integration test poorly written.
我现在正在使用前一种方法,但是在那里更好的方法?
I'm using the former approach right now, but is there a better way?
TestFX
答案这里涉及TestFX,它基于主应用程序的开始
方法不 Controller类。它显示了一种测试控制器的方法 @Tests
The answer here involves TestFX which has @Tests
based around the main app's start
method not the Controller class. It shows a method of testing the controller with
verifyThat("#email", hasText("test@gmail.com"));
但这个答案涉及 DataFX - 而我只是在询问JavaFX的MVC模式。大多数TestFX的讨论都集中在它的GUI功能上,所以我很好奇它是否也是控制器的理想选择。
but this answer involves DataFX - whereas I'm simply asking about JavaFX's MVC pattern. Most TestFX discussion focuses on it's GUI capabilities, so I'm curious whether it's ideal for the controller too.
以下示例显示了如何使用 VBox 以便在测试期间它不为空。有没有更好的办法?请具体说明
The following example shows how I inject the controller with a VBox
so that it isn't null during the test. Is there a better way? Please be specific
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 ...
}
UPDATE
这是基于hotzst建议的完整演示,但它返回此错误:
UPDATE
Here's a complete demo based on hotzst's suggestions, but it returns this error:
org.mockito.exceptions.base.MockitoException:无法实例化名为'loadController的
@InjectMocks字段'类型'类com.mypackage.LoadController'。你
没有在字段声明中提供实例,所以我尝试
构造实例。但是构造函数或初始化
块引发了异常:null
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
等测试框架在控制器中注入依赖项。因此,您可以放弃大多数设置器,至少是那些仅用于促进测试的设置器。
You can use a test framework like Mockito
to inject your dependencies in the controller. Thereby you can forgo probably most of the setters, at least the ones that are only present to facilitate testing.
使用您提供的示例代码我调整了测试类(定义 FileSorter
的内部类):
Going with the example code you provided I adjusted the class under test (define an inner class for the 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文件,但它似乎没有任何影响代码或测试。
The @FXML
annotation does not make any sense here, as no fxml file is attached, but it does not seem to have any effect on the code or Test.
您的测试类可能看起来像这样:
Your test class could then look something like this:
@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();
}
}
此测试通过以下输出成功运行:
This test runs through successfully with the following output:
NON-NULL VBOX
NON-NULL VBOX
@Rule
JavaFXThreadingRule
可以省略,因为在这样的testin中你没有运行应该执行的代码的任何部分在JavaFX线程中。
The @Rule
JavaFXThreadingRule
can be ommited, as when testin like this you are not running through any part of code that should be executed in the JavaFX Thread.
@Mock
注释以及 MockitoJUnitRunner
创建一个模拟实例,然后将其注入到使用 @InjectMocks
注释的实例中。
The @Mock
annotation together with the MockitoJUnitRunner
creates a mock instance that is then injected into the instance annotated with @InjectMocks
.
一个优秀的教程可以在这里找到。在 EasyMock 和 PowerMock ,但Mockito是我使用的并且最熟悉的。
An excellent tutorial can be found here. There are also other frameworks for mocking in tests like EasyMock and PowerMock, but Mockito is the one I use and am most familiar with.
我使用的是Java 8( 1.8.0_121)和Mockito 1.10.19。
I used Java 8 (1.8.0_121) together with Mockito 1.10.19.