我们如何正确编写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!
}
}
}
答案 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。在EasyMock和PowerMock等测试中还有其他用于模拟的框架,但Mockito是我使用的并且最熟悉的。
我使用Java 8(1.8.0_121)和Mockito 1.10.19。