I keep hearing this recommendation to always access Swing components on the EDT, including in tests. In plain Java, it usually means calling invokeLater()
/ invokeAndWait()
.
Indeed, some tests do encounter what seem to be race issues. However, I'm still not totally comfortable with how it looks. FEST's GuiActionRunner
makes it even a little bit uglier since GuiTask
, GuiQuery
are abstract classes and cannot be expressed as lambdas.
// example test
@Test
@SuppressWarnings({"OptionalGetWithoutIsPresent", "unchecked"})
void givenMatchPredicate_ifNoSearchTextSet_andNextButtonClicked_nothingHappens() {
/*
A
|__B
| |__C
|__D
*/
DefaultMutableTreeNode root = new DefaultMutableTreeNode(TreeObject.from("A"));
DefaultMutableTreeNode nodeB = new DefaultMutableTreeNode(TreeObject.from("B"));
root.add(nodeB);
TreeObject objectC = TreeObject.from("C");
DefaultMutableTreeNode nodeC = new DefaultMutableTreeNode(objectC);
nodeB.add(nodeC);
DefaultMutableTreeNode nodeD = new DefaultMutableTreeNode(TreeObject.from("D"));
root.add(nodeD);
SwingUtilities.invokeLater(() -> {
/* essentially, a modal dialog with a JTree, searchField and
arrow buttons to navigate between search matches */
TreeSelectDialog<TreeObject> dialog = TreeSelectDialog.of(null, () -> root);
BiPredicate<TreeObject, String> predicateMock = mock();
dialog.setMatchPredicate(predicateMock);
JTextComponent searchField = finder.find(dialog, JTextComponent.class).get();
JButton nextButton = finder.find(dialog, JButton.class, "nextButton").get();
tree = finder.find(dialog, JTree.class).get();
SwingUtilities.invokeLater(dialog::doModal);
robot.waitForIdle();
assumeTrue(searchField instanceof ValueField);
assumeFalse(((ValueField<?>) searchField).getPlaceholderText() == null);
Object initialSelectedObject = getSelectedObject();
nextButton.doClick();
then(predicateMock).shouldHaveNoInteractions();
assertEquals(initialSelectedObject, getSelectedObject());
assertEquals(Colors.placeholder(), searchField.getForeground());
dialog.dispose();
});
}
I tried to refactor it using InvocationInterceptor
. However, my waitForIdle()
calls fail since you "cannot call method from the event dispatcher thread". invokeLater()
also executes the code on EDT, including waitForIdle()
, but the exception is swallowed and no assertions are actually executed.
Wrapping doModal()
in invokeLater()
and then waitForIdle()
was my attempt to defer assumptions/assertions before the dialog gets actually displayed (doModal()
is essentially a decorated setVisible(true)
).
@ExtendWith(EdtExtension.class)
class TreeSelectDialogTest {
// outer SwingUtilities.invokeLater(...) removed
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
import org.junit.platform.commons.util.ExceptionUtils;
import javax.swing.SwingUtilities;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class EdtExtension implements InvocationInterceptor {
@Override
public void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) {
invokeInEdt(invocation);
}
private void invokeInEdt(Invocation<Void> invocation) {
try {
SwingUtilities.invokeAndWait(() -> {
try {
invocation.proceed();
} catch (Throwable e) {
ExceptionUtils.throwAsUncheckedException(e);
}
});
} catch (InterruptedException | InvocationTargetException e) {
ExceptionUtils.throwAsUncheckedException(e);
}
}
}
Caused by: java.lang.IllegalThreadStateException: Cannot call method from the event dispatcher thread
at org.fest.swing.core.BasicRobot.waitForIdle(BasicRobot.java:669)
at org.fest.swing.core.BasicRobot.waitForIdle(BasicRobot.java:654)
What it the most concise and readable way to refactor my test? If FEST can help, you can use it.
Java 8.
While creating a JUnit extension may work in some contexts (here's my implementation), it seems to have a serious limitation: you can't display a modal dialog and make assertions on it if the whole test is executed in the EDT. I means you'd have to feed it your test code bit by bit, typically only those parts that involve manipulating Swing objects (not assertions).
The test in question could be refactored like so.
@BeforeEach
void setUp() {
dialog = createDialog();
// extracting tree, searchField, nextButton from the dialog
// creating rootSupplierMock
}
@AfterEach
void tearDown() {
dialog.dispose();
}
TreeSelectDialog<TreeObject> createDialog() {
return GuiActionRunner.execute(new GuiQuery<TreeSelectDialog<TreeObject>>() {
protected TreeSelectDialog<TreeObject> executeInEDT() {
TreeSelectDialog<TreeObject> dialog = TreeSelectDialog.of(null, rootSupplierMock);
return dialog;
}
});
}
// ...
@Test
@SuppressWarnings("unchecked")
void givenMatchPredicate_ifNoSearchTextSet_andNextButtonClicked_nothingHappens() throws InterruptedException, InvocationTargetException {
BiPredicate<TreeObject, String> predicateMock = mock();
dialog.setMatchPredicate(predicateMock);
assumeTrue(searchField instanceof ValueField);
assumeFalse(((ValueField<?>) searchField).getPlaceholderText() == null);
Object initialSelectedObject = getSelectedObject();
SwingUtilities.invokeAndWait(nextButton::doClick);
then(predicateMock).shouldHaveNoInteractions();
assertEquals(initialSelectedObject, getSelectedObject());
assertEquals(Colors.placeholder(), searchField.getForeground());
}
private Object getSelectedObject() {
return Optional.ofNullable(tree.getLastSelectedPathComponent())
.map(DefaultMutableTreeNode.class::cast)
.map(DefaultMutableTreeNode::getUserObject)
.orElse(null);
}