Cas*_* B. 5 java unit-testing controller javafx testfx
我们如何为 JavaFX 控制器逻辑正确编写单元/集成测试?假设我正在测试的 Controller 类名为LoadController,它的单元测试类是LoadControllerTest,我的困惑源于:
如果LoadControllerTest该类LoadController通过LoadController loadController = new LoadController();I实例化一个新对象,则
可以通过(许多)setter 将值注入控制器。这似乎是不使用反射(遗留代码)的唯一方法。如果我不将值注入 FXML 控件,则控件显然尚未初始化,返回 null。
如果我改用FXMLLoader'sloader.getController()方法来检索loadController它,它将正确初始化 FXML 控件,但initialize()因此调用了控制器的方法,这会导致运行非常缓慢,并且由于无法注入模拟依赖项,因此它更像是一个写得不好的集成测试.
我现在正在使用前一种方法,但有更好的方法吗?
测试效果
这里的答案涉及 TestFX,它@Tests基于主应用程序的start方法而不是Controller 类。它显示了一种测试控制器的方法
verifyThat("#email", hasText("test@gmail.com"));
Run Code Online (Sandbox Code Playgroud)
但是这个答案涉及DataFX - 而我只是询问 JavaFX 的 MVC 模式。大多数 TestFX 讨论都集中在它的 GUI 功能上,所以我很好奇它是否也适合控制器。
以下示例显示了我如何使用 a 注入控制器,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));
}
}
Run Code Online (Sandbox Code Playgroud)
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 ...
}
Run Code Online (Sandbox Code Playgroud)
这是基于 hotzst 建议的完整演示,但它返回此错误:
org.mockito.exceptions.base.MockitoException:无法实例化名为“loadController”的@InjectMocks 字段,类型为“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
}
}
Run Code Online (Sandbox Code Playgroud)
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!
}
}
}
Run Code Online (Sandbox Code Playgroud)
您可以使用测试框架,例如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 {}
}
Run Code Online (Sandbox Code Playgroud)
该@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();
}
}
Run Code Online (Sandbox Code Playgroud)
此测试成功运行,输出如下:
非空 VBOX
可以@Rule JavaFXThreadingRule省略,因为当像这样进行测试时,您不会运行应在 JavaFX 线程中执行的代码的任何部分。
该@Mock注释与 一起MockitoJUnitRunner创建一个模拟实例,然后将其注入到用 注释的实例中@InjectMocks。
可以在这里找到一个很棒的教程。还有其他用于测试中模拟的框架,例如EasyMock和PowerMock,但 Mockito 是我使用且最熟悉的一个。
我将 Java 8 (1.8.0_121) 与 Mockito 1.10.19 一起使用。