如何测试 JavaFX (MVC) 控制器逻辑?
How to test JavaFX (MVC) Controller Logic?
我们如何为 JavaFX 控制器逻辑正确编写 unit/integration 测试?
假设我正在测试的控制器 class 被命名为 LoadController
,并且它的单元测试 class 是 LoadControllerTest
,我的困惑源于:
如果 LoadControllerTest
class 通过实例化一个新的 LoadController
对象
LoadController loadController = new LoadController();
我可以
然后通过(许多)设置器将值注入控制器。这似乎是不使用反射(遗留代码)的唯一方法。如果我不将值注入 FXML 控件,那么控件显然还没有初始化,返回 null。
如果我改为使用 FXMLLoader
的 loader.getController()
方法来检索 loadController
,它将正确初始化 FXML 控件,但是
因此调用了控制器的 initialize()
,这导致非常慢的 运行,并且由于无法注入模拟的依赖项,所以它更像是一个写得不好的集成测试。
我现在使用的是前一种方法,还有更好的方法吗?
TestFX
答案 涉及 TestFX,它 @Tests
基于主应用程序的 start
方法 而不是 控制器 class .它显示了一种使用
测试控制器的方法
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 的建议的完整演示,但是 returns 这个错误:
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
之类的测试框架将您的依赖项注入控制器。因此,您可能可以放弃大部分设置器,至少是那些仅用于方便测试的设置器。
根据您提供的示例代码,我调整了测试中的 class(为 FileSorter
定义了一个内部 class):
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 文件,但它似乎对代码或测试没有任何影响。
您的测试 class 可能看起来像这样:
@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();
}
}
此测试成功运行,输出如下:
NON-NULL VBOX
@Rule
JavaFXThreadingRule
可以省略,因为当这样测试时,您不会 运行 通过任何应该在 JavaFX 中执行的代码部分线程。
@Mock
注释与 MockitoJUnitRunner
一起创建一个模拟实例,然后将其注入到用 @InjectMocks
注释的实例中。
可以找到一个很好的教程here. There are also other frameworks for mocking in tests like EasyMock and PowerMock,但 Mockito 是我使用和最熟悉的。
我使用了 Java 8 (1.8.0_121) 和 Mockito 1.10.19.
如果您想通过与 UI 交互来测试控制器,TestFX 可能是您的一个选择。
我创建了一个简单的测试项目来展示它的功能:
https://github.com/ArchibaldBienetre/javaFxTestGradle
我们如何为 JavaFX 控制器逻辑正确编写 unit/integration 测试?
假设我正在测试的控制器 class 被命名为 LoadController
,并且它的单元测试 class 是 LoadControllerTest
,我的困惑源于:
如果
LoadControllerTest
class 通过实例化一个新的LoadController
对象LoadController loadController = new LoadController();
我可以 然后通过(许多)设置器将值注入控制器。这似乎是不使用反射(遗留代码)的唯一方法。如果我不将值注入 FXML 控件,那么控件显然还没有初始化,返回 null。如果我改为使用
FXMLLoader
的loader.getController()
方法来检索loadController
,它将正确初始化 FXML 控件,但是 因此调用了控制器的initialize()
,这导致非常慢的 运行,并且由于无法注入模拟的依赖项,所以它更像是一个写得不好的集成测试。
我现在使用的是前一种方法,还有更好的方法吗?
TestFX
答案 @Tests
基于主应用程序的 start
方法 而不是 控制器 class .它显示了一种使用
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 的建议的完整演示,但是 returns 这个错误:
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
之类的测试框架将您的依赖项注入控制器。因此,您可能可以放弃大部分设置器,至少是那些仅用于方便测试的设置器。
根据您提供的示例代码,我调整了测试中的 class(为 FileSorter
定义了一个内部 class):
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 文件,但它似乎对代码或测试没有任何影响。
您的测试 class 可能看起来像这样:
@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();
}
}
此测试成功运行,输出如下:
NON-NULL VBOX
@Rule
JavaFXThreadingRule
可以省略,因为当这样测试时,您不会 运行 通过任何应该在 JavaFX 中执行的代码部分线程。
@Mock
注释与 MockitoJUnitRunner
一起创建一个模拟实例,然后将其注入到用 @InjectMocks
注释的实例中。
可以找到一个很好的教程here. There are also other frameworks for mocking in tests like EasyMock and PowerMock,但 Mockito 是我使用和最熟悉的。
我使用了 Java 8 (1.8.0_121) 和 Mockito 1.10.19.
如果您想通过与 UI 交互来测试控制器,TestFX 可能是您的一个选择。
我创建了一个简单的测试项目来展示它的功能:
https://github.com/ArchibaldBienetre/javaFxTestGradle