Alright, so I know that Java on the desktop has never reached the level of popularity that it enjoys on the server side, but regardless of popularity, there are cases where the requirements favor or maybe even necessitate a desktop solution. Testing a user interface can get kinda sticky, but with the right abstractions, you’ll find it easier to write better tests.
The first step to writing testable GUIs is to actually enforce the separation of the View from the Model/Controller. Swing was designed with MVC in mind, so we have the tools to do it, but it’s often that wiring the M-V-C together is where we find the most difficulty. It can be quite tempting sometimes to pass data objects in and write our ActionListeners in with the code where we construct the GUI. In almost every case, it’s convenient to write, but usually at the cost of making it difficult to test. Your manual testing methodology won’t scale, and unless you love mindlessly clicking buttons for a living (and your boss is cool with that), you need a better plan. Oh yeah, when getting started, it’s important to resist the urge to write lots of the brittle robot-button-pushing style integration tests. You’ll really want to max out your code coverage with your unit tests first, and keep the robot testing to a minimum, at least until the UI design and feature set reach some minimal level of concreteness.
Let’s get started filling out an example (you can find the code here). We’re going to build a swing app that displays a list of strings and we’ll add actions that manipulate those strings. To clean up the code, we’ll add a little dependency injection using Guice and a slick little library for working with JLists and JTables (and a variety of other things) called Glazed Lists. I’m going to assume a little bit of Guice knowledge, but for those not very familiar with Glazed Lists, you can think of it as an observer pattern on a java.util.List that automatically handles interactions with JLists and JTables (then it adds some really powerful filtering and sorting constructs). Okay, here’s the GUI:
public class TestGui extends JFrame { private static final long serialVersionUID = -7750893881169224217L; @Inject public TestGui(AppActionFactory actions, @Named(STRING_LIST) EventList<String> strings) { EventListModel<String> model = new EventListModel<String>(strings); JList list = new JList(model); getContentPane().add(new JScrollPane( list, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS)); JMenuBar menubar = new JMenuBar(); JMenu things = menubar.add(new JMenu("Menu")); things.add(new JMenuItem("Make UpperCase")) .addActionListener(actions.proxy(MakeUpperCaseAction.class)); setJMenuBar(menubar); } public static void main(String[] args) { final Injector inject = Guice.createInjector(new TestGuiModule()); SwingUtilities.invokeLater(new Runnable() { public void run() { TestGui gui = inject.getInstance(TestGui.class); gui.setDefaultCloseOperation(EXIT_ON_CLOSE); inject.getInstance(DemoData.class).actionPerformed(null); gui.pack(); gui.setVisible(true); }}); } }
A couple of things to note here. The first is injecting the AppActionFactory (we’ll get to that part in a moment), and the EventList containing our strings, the core data of our application. The second thing to notice is that this class only displays a single JFrame, and no other dialogs, frames, views, etc. You can load it without needing to start the application and click through several items to get to it. We’ve also effectively documented the data requirements for this specific view without polluting a constructor with other unnecessary objects needed for downstream views. More test friendly? Definitely.
What’s the AppActionFactory? Instead of creating a new instance of the ActionListener we want our buttons and menus to notify (with all of it’s dependencies), we’ll create a proxy for the ActionListener and let Guice resolve the action’s dependencies for us. As an exercise to the student, we could also create an ActionProxy that executes the action on a background ExecutorService as a runnable, thus preventing us from committing the cardinal sin of blocking the Event Dispatching Thread. Glazed Lists can ensure that your list modifications are dispatched to the ListModel on the EDT, so you won’t have to worry about bouncing between various threads in your application.
public class DefaultAppActionFactory implements AppActionFactory { @Inject Injector injector; @Override public ActionProxy proxy(Class<? extends ActionListener> action) { return new ActionProxy(injector, action); } } public class ActionProxy implements ActionListener { private final Injector injector; private final Class<? extends ActionListener> action; public ActionProxy(Injector injector, Class<? extends ActionListener> action) { this.injector = injector; this.action = action; } @Override public void actionPerformed(final ActionEvent event) { injector.getInstance(action).actionPerformed(event); } }
Let’s move on to one of these actions, the MakeUpperCaseAction class. As a tip, make it an interface. Then you’ll be able to override the default Guice binding with another implementation for use in tests. You could also create a view of your GUI that doesn’t actually perform all of the operations, or performs mock operations which is ideal for early prototyping your application with users.
@ImplementedBy(MakeUpperCase.class) public interface MakeUpperCaseAction extends ActionListener {} public class MakeUpperCase implements MakeUpperCaseAction { @Inject @Named(STRING_LIST) EventList<String> list; public void actionPerformed(ActionEvent e) { Lock lock = list.getReadWriteLock().writeLock(); lock.lock(); try { for (int i = 0; i < list.size(); i++) { list.set(i, list.get(i).toUpperCase()); } } finally { lock.unlock(); } } }
Granted, the action is simple, but it highlights some good practices. In general, we’ve done a pretty good job of avoiding the Top 10 Things Which Make Your Code Hard To Test. It’s not a coincidence that Wealthfront‘s Kawala Query Engine queries are written in a very similar manner. It’s also worth mentioning is that Glazed Lists has lots of neat built-in concurrency things which look a little cluttery now, but come in handy as your application gets bigger, and is essential to making sure your application plays nicely with the EDT.
Here’s the test for it:
public class MakeUpperCaseTest { private BasicEventList<String> list; @Before public void setup() { list = new BasicEventList<String>(); list.addAll(ImmutableList.of("one", "Two", "THREE")); } @Test public void testUpperCase() { MakeUpperCase action = new MakeUpperCase(); action.list = list; action.actionPerformed(null); Lock lock = list.getReadWriteLock().readLock(); lock.lock(); try { assertEquals(3, list.size()); assertEquals("ONE", list.get(0)); assertEquals("TWO", list.get(1)); assertEquals("THREE", list.get(2)); } finally { lock.unlock(); } } }
Another reason that this action is so easy to test is because it doesn’t prompt for any user interaction, which is something you’ll obviously want to do in a real application (i.e. do you really want to delete this important file dialog). You should abstract out the GUI code here as well. Consider using a pattern like this for a Yes/No confirmation box:
@ImplementedBy(SwingConfirmBox.class) public interface ConfirmBox { public boolean isYes(Component parent, Object message, String title); } public class SwingConfirmBox implements ConfirmBox { @Override public boolean isYes(Component parent, Object message, String title) { return JOptionPane.showConfirmDialog(parent, message, title, JOptionPane.YES_NO_OPTION) == JOptionPane.OK_OPTION; } }
Let’s add it to the MakeUpperCaseAction:
public class MakeUpperCase implements MakeUpperCaseAction { @Inject ConfirmBox go; @Inject @Named(STRING_LIST) EventList<String> list; public void actionPerformed(ActionEvent e) { if (!isYes(null, "Make uppercase?", "Action")) { return; } Lock lock = list.getReadWriteLock().writeLock(); lock.lock(); try { for (int i = 0; i < list.size(); i++) { list.set(i, list.get(i).toUpperCase()); } } finally { lock.unlock(); } } }
And the test is now:
@Test public void testUpperCase() { MakeUpperCase action = new MakeUpperCase(); action.go = new ConfirmBox() { @Override public boolean isYes(Component parent, Object message, String title) { return true; } }; action.list = list; action.actionPerformed(null); Lock lock = list.getReadWriteLock().readLock(); lock.lock(); try { assertEquals(3, list.size()); assertEquals("ONE", list.get(0)); assertEquals("TWO", list.get(1)); assertEquals("THREE", list.get(2)); } finally { lock.unlock(); } }
Finally, we can get to the integration testing, but hopefully by this point, we’ve done so much testing that all that remains is to verify that our actions are wired up correctly in the GUI and that Guice is able to resolve all of your dependencies. Fortunately, there are already some nice libraries to assist with this part as well, such as the FEST Swing Module and Marathon.
So, hopefully I’ve given you a few good ideas for improving the testability of your GUI code using Guice and Glazed Lists. Coincidentally (or perhaps not by coincidence), Jesse Wilson has been involved in both of these awesome projects, so make sure to give him a high five and a thank you when you see him.
Again, here’s the code for the examples: https://s3-us-west-1.amazonaws.com/hitchings/testing-gui-master.zip