我使用 TestFX 编写了以下 JUnit4 测试,以测试特定产品(待办事项列表)的 GUI (JavaFX),以及两个必要的类。第一个类是管理整个 GUI 的主类,而第二个是文本字段类。如有必要,完整的源代码位于此处(它是已提交的学校项目的一部分)。
如果我只是在 Eclipse 中使用 F11 热键或“运行方式 -> JUnit 测试”来运行它,那么该测试工作得非常好。但是,当我选择“Coverage”时,它会在第一个测试用例上出错(无论我选择设置为第一个)。具体来说,它“键入”第一个测试用例的前两个字符(此处的示例用例中的 sh),然后给我检测到用户输入的错误([TestFX] User mouse movement detected. Aborting test.
)然后转到下一个测试用例。
我自己无法弄清楚,而且我似乎在网上找不到太多关于这个的帮助。任何帮助将不胜感激!根据堆栈跟踪,它看起来与线程有关,但我看不出覆盖运行将如何导致这种情况(当正常测试没有时)。
我不得不缩短堆栈跟踪,因为我达到了限制。
java.lang.RuntimeException: java.lang.ThreadDeath
at org.loadui.testfx.utils.FXTestUtils.awaitEvents(FXTestUtils.java:104)
at org.loadui.testfx.FXScreenController.release(FXScreenController.java:131)
at org.loadui.testfx.GuiTest.release(GuiTest.java:1110)
at org.loadui.testfx.GuiTest.type(GuiTest.java:1069)
at org.loadui.testfx.GuiTest.type(GuiTest.java:1008)
at org.loadui.testfx.GuiTest.type(GuiTest.java:990)
at gui.UserInterfaceTest.test1ShowUndoneEmpty(UserInterfaceTest.java:38)
Caused by: java.lang.ThreadDeath
at java.lang.Thread.stop(Unknown Source)
at org.loadui.testfx.utils.UserInputDetector.userInputDetected(UserInputDetector.java:58)
at org.loadui.testfx.utils.UserInputDetector.assertPointsAreEqual(UserInputDetector.java:42)
at org.loadui.testfx.utils.UserInputDetector.run(UserInputDetector.java:27)
at java.lang.Thread.run(Unknown Source)
用户界面.java
package gui;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.scene.Scene;
import object.Task;
import type.CommandType;
import type.KeywordType;
import logic.FeedbackHelper;
import logic.LogicController;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
//@@author A0112882H
public class UserInterface extends Application {
private static final int ROW_HEIGHT = 30;
private static BorderPane _root = new BorderPane();
private static Scene _defaultScene = new Scene(_root, 750, 580);
private static VBox _vbox = new VBox();
private static VBox _tables = new VBox();
private static UIButton _taskButton = new UIButton("Tasks & Events");
private static UIButton _floatingButton = new UIButton("Floating Tasks");
private static UITextField _field = new UITextField();
private static TextArea _cheatSheet = new TextArea();
private static Label _feedBack = new Label();
private static int commandIndex;
private static UITable _taskTable = new UITable(false);
private static UITable _floatingTable = new UITable(true);
private final KeyCombination _undoKey = new KeyCodeCombination(KeyCode.U, KeyCombination.CONTROL_DOWN);
private final KeyCombination _redoKey = new KeyCodeCombination(KeyCode.R, KeyCombination.CONTROL_DOWN);
private final KeyCombination _homeKey = new KeyCodeCombination(KeyCode.H, KeyCombination.CONTROL_DOWN);
private static ArrayList<String> commandHistory = new ArrayList<String>();
private static ArrayList<Task> _displayList = new ArrayList<Task>();
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
_root.setOnKeyPressed(hotKeyEvents);
_field.setOnKeyPressed(hotKeyEvents);
setScene();
setUpCommandPrompt();
setUpTables();
setKeywordsHighlighting();
primaryStage.setScene(_defaultScene);
primaryStage.setTitle("F2DO");
primaryStage.show();
}
public BorderPane getRootNode() {
return _root;
}
private void setScene() {
String css = UserInterface.class.getResource("style.css").toExternalForm();
_defaultScene.getStylesheets().add(css);
_defaultScene.heightProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
int displaySize = (int) Math.floor(_taskTable.getHeight() / ROW_HEIGHT) - 1;
LogicController.setNonFloatingDisplaySize(displaySize);
updateDisplayList();
}
});
}
/**
* Set the hot keys. Ctrl + U: undo operation. Ctrl + R: redo operation.
* Ctrl + H: home page. F1: help page. F2: show all. F3: show undone tasks.
* F4: show done tasks. ESC: exit application.
*/
private EventHandler<KeyEvent> hotKeyEvents = new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
String showUndone = "show undone";
String showDone = "show done";
String showAll = "show all";
if (_undoKey.match(event)) {
String feedbackMsg = LogicController.undo();
_feedBack.setText(feedbackMsg);
updateDisplayList();
} else if (_redoKey.match(event)) {
String feedbackMsg = LogicController.redo();
_feedBack.setText(feedbackMsg);
updateDisplayList();
} else if (_homeKey.match(event)) {
initialiseScene();
setUpCommandPrompt();
setUpTables();
} else if (event.getCode().equals(KeyCode.F3)) {
String feedbackMsg = LogicController.process(showUndone, _displayList);
_feedBack.setText(feedbackMsg);
updateDisplayList();
} else if (event.getCode().equals(KeyCode.F4)) {
String feedbackMsg = LogicController.process(showDone, _displayList);
_feedBack.setText(feedbackMsg);
updateDisplayList();
} else if (event.getCode().equals(KeyCode.F2)) {
String feedbackMsg = LogicController.process(showAll, _displayList);
_feedBack.setText(feedbackMsg);
updateDisplayList();
} else if (event.getCode().equals(KeyCode.F1)) {
try {
initialiseScene();
setUpCommandPrompt();
setCheatSheetContent();
} catch (Exception e) {
}
} else if (event.getCode().equals(KeyCode.ESCAPE)) {
exit();
} else if (event.getCode().equals(KeyCode.ENTER)) {
String userInput = _field.getText();
commandHistory.add(userInput);
commandIndex = commandHistory.size() - 1;
_field.clear();
event.consume();
String feedbackMsg = LogicController.process(userInput, _displayList);
if (feedbackMsg == FeedbackHelper.MSG_HELP) {
try {
initialiseScene();
setUpCommandPrompt();
setCheatSheetContent();
} catch (Exception e) {
e.printStackTrace();
}
} else if (feedbackMsg == FeedbackHelper.MSG_HOME) {
initialiseScene();
setUpCommandPrompt();
setUpTables();
} else {
_feedBack.setText(feedbackMsg);
updateDisplayList();
}
} else if (event.getCode().equals(KeyCode.UP)) {
if (!commandHistory.isEmpty()) {
_field.replaceText(commandHistory.get(commandIndex));
int length = commandHistory.get(commandIndex).length();
commandIndex--;
Platform.runLater(new Runnable() {
@Override
public void run() {
_field.positionCaret(length);
}
});
if (commandIndex < 0) {
commandIndex = 0;
}
}
} else if (event.getCode().equals(KeyCode.DOWN)) {
_field.showPopup();
}
}
};
/**
* Set up command prompt and feedback
*/
private void setUpCommandPrompt() {
setTextArea();
setFeedback();
_field.setId("textarea");
_feedBack.setId("feedback");
_vbox.setAlignment(Pos.CENTER);
_vbox.setSpacing(5);
_vbox.getChildren().addAll(_field, _feedBack);
BorderPane.setMargin(_vbox, new Insets(20, 20, 0, 20));
_root.setTop(_vbox);
}
/**
* Set up labels and tables
*/
private void setUpTables() {
updateDisplayList();
BorderPane.setMargin(_tables, new Insets(8, 20, 30, 20));
BorderPane.setAlignment(_tables, Pos.CENTER);
_floatingTable.setId("floatingTable");
_taskTable.setId("taskTable");
_taskButton.setMaxWidth(Double.MAX_VALUE);
_floatingButton.setMaxWidth(Double.MAX_VALUE);
_taskButton.setStyle("-fx-font-size: 13.5; -fx-font-weight: bold");
_floatingButton.setStyle("-fx-font-size: 13.5; -fx-font-weight: bold");
_tables.setAlignment(Pos.CENTER);
_tables.getChildren().addAll(_taskButton, _taskTable, _floatingButton, _floatingTable);
_tables.setSpacing(7);
_root.setCenter(_tables);
}
/**
* Update tables.
*/
private static void updateDisplayList() {
ArrayList<Task> nonFloatingList = LogicController.getNonFloatingList();
ArrayList<Task> floatingList = LogicController.getFloatingList();
_displayList.clear();
_displayList.addAll(nonFloatingList);
_displayList.addAll(floatingList);
_taskTable.updateTable(nonFloatingList, floatingList);
_floatingTable.updateTable(nonFloatingList, floatingList);
_field.updateDisplayList(_displayList);
}
/**
* Set the design of textArea
*/
private void setTextArea() {
_field.setPrefHeight(25);
_field.setMaxHeight(25);
_field.setPadding(new Insets(2, 2, 2, 2));
_field.setWrapText(true);
_field.setStyle("-fx-border-color: lightblue; -fx-font-size: 14");
}
/**
* Set the design of feedback.
*
* @param feedback
*/
private void setFeedback() {
_feedBack.setText("Welcome to F2DO, your personalised task manager(:\n" + "Type " + "\"Help\""
+ " for a list of commands to get started.");
_feedBack.setMouseTransparent(true);
}
/**
* Set highlighting of the keyword.
*/
private void setKeywordsHighlighting() {
_field.textProperty().addListener((observable, oldValue, newValue) -> {
// check if the first word is a keyword - happens in most cases
// for commands e.g. like add, search, edit, delete
String firstWord = getFirstWord(newValue);
if (isValidCmd(firstWord)) {
_field.setStyle(0, firstWord.length(), "-fx-font-weight: bold; -fx-fill: red");
if (newValue.length() > firstWord.length()) {
_field.setStyle(firstWord.length() + 1, newValue.length(),
"-fx-font-weight: normal; -fx-fill: black");
}
String[] result = newValue.substring(firstWord.length()).split("\\s");
int currentIndex = firstWord.length();
for (int i = 0; i < result.length; i++) {
String word = result[i];
if (isValidKeyword(word)) {
_field.setStyle(currentIndex, currentIndex + word.length(),
"-fx-font-weight: bold; -fx-fill: blue");
}
currentIndex += word.length() + 1;
}
} else {
_field.setStyle(0, newValue.length(), "-fx-font-weight: normal; -fx-fill: black");
}
});
}
/**
* Get the first word of the command.
*
* @param newCommand
* - input command
* @return first word
*/
private String getFirstWord(String newCommand) {
String[] textTokens = newCommand.split(" ");
if (textTokens.length > 0) {
return textTokens[0];
}
return null;
}
/**
* Check if the entered word is a valid command.
*
* @param word
* - input word
* @return true if the word is a valid command; false otherwise
*/
private boolean isValidCmd(String word) {
if (CommandType.toCmd(word) != CommandType.INVALID) {
return true;
}
return false;
}
/**
* Check if the entered word is a valid keyword.
*
* @param word
* - input word
* @return true if the word is a valid keyword; false otherwise
*/
private boolean isValidKeyword(String word) {
if (KeywordType.toType(word) != KeywordType.INVALID) {
return true;
}
return false;
}
private void initialiseScene() {
_vbox.getChildren().clear();
_tables.getChildren().clear();
_root.getChildren().clear();
}
private void setCheatSheetContent() throws IOException {
String text;
StringBuilder content = new StringBuilder();
_cheatSheet.setEditable(false);
BorderPane.setMargin(_cheatSheet, new Insets(8, 20, 25, 20));
InputStream is = getClass().getResourceAsStream("cheatsheet.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is));
while ((text = br.readLine()) != null) {
content.append(text).append("\n");
}
_cheatSheet.clear();
_cheatSheet.appendText(content.toString());
_root.setCenter(_cheatSheet);
br.close();
}
private void exit() {
Platform.exit();
}
}
UITextField.java
package gui;
import org.fxmisc.richtext.InlineCssTextArea;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import object.Task;
import type.CommandType;
import type.TaskType;
//@@author A0118005W
public class UITextField extends InlineCssTextArea {
private static ArrayList<Task> _displayList = new ArrayList<Task>();
private ContextMenu popupMenu = new ContextMenu();
public UITextField() {
super();
setAutoFill();
}
/**
* Update the display list in TextField.
*
* @param displayList
* - display list
*/
public void updateDisplayList(ArrayList<Task> displayList) {
_displayList = displayList;
}
/**
* Show pop-up menu.
*/
public void showPopup() {
if (!popupMenu.isShowing()) {
popupMenu.show(this, Side.BOTTOM, 0, 0);
}
}
/**
* Set up auto fill in of the text field.
*/
private void setAutoFill() {
this.textProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
String text = UITextField.this.getText();
String[] textTokens = text.split(" ");
popupMenu.hide();
int spaceCount = 0;
for (int i = 0; i < text.length() && spaceCount < 2; i++) {
if (text.charAt(i) == ' ') {
spaceCount += 1;
}
}
if (textTokens.length == 2 && spaceCount == 2) {
String firstToken = textTokens[0];
CommandType cmd = CommandType.toCmd(firstToken);
int index = getInteger(textTokens[1]) - 1;
boolean isWithinRange = ((index >= 0) && (index < _displayList.size()));
if (cmd == CommandType.EDIT && isWithinRange) {
Task task = _displayList.get(index);
populatePopup(index, task);
if (!popupMenu.isShowing()) {
popupMenu.show(UITextField.this, Side.BOTTOM, 0, 0);
}
}
} else if (textTokens.length <= 2) {
// Hide pop up
popupMenu.hide();
popupMenu.getItems().clear();
}
}
});
}
/**
* Get the integer from an input string. If the input cannot be parsed,
* return -1.
*
* @param input
* - input string
* @return parsed integer
*/
private int getInteger(String input) {
try {
int integer = Integer.parseInt(input);
return integer;
} catch (NumberFormatException e) {
return -1;
}
}
/**
* Populate the pop-up box.
*
* @param index
* - index of the task
* @param task
* - task to be displayed
*/
private void populatePopup(int index, Task task) {
ArrayList<String> displayList = getDisplayItems(index, task);
ArrayList<CustomMenuItem> menuItems = new ArrayList<CustomMenuItem>();
for (int i = 0; i < displayList.size(); i++) {
String str = displayList.get(i);
Label label = new Label(str);
CustomMenuItem item = new CustomMenuItem(label, true);
item.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
replaceText(str);
positionCaret(str.length());
}
});
menuItems.add(item);
}
popupMenu.getItems().clear();
popupMenu.getItems().addAll(menuItems);
}
/**
* Get the command input to be displayed in the pop-up menu.
*
* @param index
* - index of the task
* @param task
* - task to be displayed
* @return display items
*/
private ArrayList<String> getDisplayItems(int index, Task task) {
ArrayList<String> items = new ArrayList<String>();
TaskType taskType = task.getTaskType();
Integer displayIndex = index + 1;
String floatingStr = "edit " + displayIndex.toString() + " " + task.getTaskName() + " ";
String eventStr = floatingStr;
String alternateEventStr = floatingStr;
String deadlineStr = floatingStr;
Calendar tmrCalendar = Calendar.getInstance();
Calendar afterTmrCalendar = Calendar.getInstance();
tmrCalendar.add(Calendar.DAY_OF_MONTH, 1);
afterTmrCalendar.add(Calendar.DAY_OF_MONTH, 2);
Date tomorrow = tmrCalendar.getTime();
Date afterTomorrow = afterTmrCalendar.getTime();
Date startDate = task.getStartDate();
Date endDate = task.getEndDate();
SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm");
// Set event string
if (startDate != null && endDate != null) {
eventStr += "from " + dateFormat.format(startDate) + " ";
eventStr += "to " + dateFormat.format(endDate);
alternateEventStr += "on " + dateFormat.format(startDate);
} else if (startDate != null) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(startDate);
calendar.add(Calendar.DAY_OF_MONTH, 1);
eventStr += "on " + dateFormat.format(startDate);
alternateEventStr += "from " + dateFormat.format(startDate) + " ";
alternateEventStr += "to " + dateFormat.format(calendar.getTime());
} else if (endDate != null) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(endDate);
calendar.add(Calendar.DAY_OF_MONTH, 1);
eventStr += "from " + dateFormat.format(endDate) + " ";
eventStr += "to " + dateFormat.format(calendar.getTime());
alternateEventStr += "on " + dateFormat.format(endDate);
} else {
eventStr += "from " + dateFormat.format(tomorrow) + " ";
eventStr += "to " + dateFormat.format(afterTomorrow);
alternateEventStr += "on " + dateFormat.format(tomorrow);
}
// Set deadline string
if (endDate != null) {
deadlineStr += "by " + dateFormat.format(endDate);
} else if (startDate != null) {
deadlineStr += "by " + dateFormat.format(startDate);
} else {
deadlineStr += "by " + dateFormat.format(tomorrow);
}
// Assign display order
int eventIndex = 0;
int floatingIndex = 1;
int alternateEventIndex = 2;
int deadlineIndex = 3;
int firstIndex = -1;
String[] eventList = { eventStr, floatingStr, alternateEventStr, deadlineStr };
switch (taskType) {
case EVENT:
if (endDate == null) {
items.add(eventList[alternateEventIndex]);
firstIndex = alternateEventIndex;
} else {
items.add(eventList[eventIndex]);
firstIndex = eventIndex;
}
break;
case DEADLINE:
items.add(eventList[deadlineIndex]);
firstIndex = deadlineIndex;
break;
case FLOATING:
items.add(eventList[floatingIndex]);
firstIndex = floatingIndex;
break;
default:
// Do nothing
}
for (int i = 0; i < eventList.length; i++) {
if (i != firstIndex) {
items.add(eventList[i]);
}
}
return items;
}
}
用户界面测试.java
package gui;
import org.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;
import org.junit.Test;
import org.loadui.testfx.Assertions;
import org.loadui.testfx.GuiTest;
import org.loadui.testfx.utils.FXTestUtils;
import javafx.scene.Parent;
import javafx.scene.input.KeyCode;
//@@author A0112882H-reused
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class UserInterfaceTest {
private static GuiTest controller;
@BeforeClass
public static void setUpClass() throws InterruptedException {
FXTestUtils.launchApp(UserInterface.class);
Thread.sleep(7000); // Giving the program time to startup. The likely problematic line.
controller = new GuiTest() {
@Override
protected Parent getRootNode() {
return stage.getScene().getRoot();
}
};
System.out.println("GUI TEST START");
}
// @@author A0112882H
@Test
public void test1ShowUndoneEmpty() throws Exception {
UITextField textField = (UITextField) GuiTest.find("#textarea");
controller.click(textField).type("show undone").push(KeyCode.ENTER);
// Assertions.assertNodeExists("");
}
@Test
public void test2AddFloatingTask() throws Exception {
UITextField textField = (UITextField) GuiTest.find("#textarea");
controller.click(textField).type("add Meeting with boss").push(KeyCode.ENTER);
Assertions.assertNodeExists("Meeting with boss");
}
@Test
public void test3Search() throws Exception {
UITextField textField = (UITextField) GuiTest.find("#textarea");
controller.click(textField).type("search Meeting with boss").push(KeyCode.ENTER);
Assertions.assertNodeExists("Meeting with boss");
}
@Test
public void test4ShowUndone() throws Exception {
UITextField textField = (UITextField) GuiTest.find("#textarea");
controller.click(textField).type("show undone").push(KeyCode.ENTER);
Assertions.assertNodeExists("Meeting with boss");
}
@Test
public void test5MarkDone() throws Exception {
UITextField textField = (UITextField) GuiTest.find("#textarea");
controller.click(textField).type("done 1").push(KeyCode.ENTER);
// Assertions.assertNodeExists("Meeting with boss");
}
}
PS对不起,如果我添加了不必要的标签。不确定我应该包括什么。
PPS 我从来没有让测试文件中的断言完全工作。如果你愿意,你可以忽略它们,因为我不想学习如何解决这个问题(现在)。