- Adding Behaviors to the View
- Getting the Test to Pass
- What About Players?
- Showing the Hole Cards
- Whats Next?
Showing the Hole Cards
For the final bit of work, we want to support showing the hole cards when dealt. We’ll start at the view level. Listing 10 shows an appropriate test in SeatPanelTest; Listing 11 shows the updated SeatPanelTest with some additional minor refactorings.
Listing 10 Supporting hole cards in SeatPanelTest.
public void testAddHoleCards() { final String card1 = "AS"; final String card2 = "KD"; panel.addHoleCards(card1, card2); SwingTestUtil.assertLabelText(panel, SeatPanel.CARD1, card1); SwingTestUtil.assertLabelText(panel, SeatPanel.CARD2, card2); }
Listing 11 Updated SeatPanelTest.
public class SeatPanel extends JPanel { ... public static final String CARD1 = "SeatPanel.card1"; public static final String CARD2 = "SeatPanel.card2"; ... private void initialize() { setName(SeatPanel.NAME + position); setLayout(new BorderLayout()); addLabel(POSITION, position, BorderLayout.NORTH); addLabel(PLAYER_NAME, Bundle.get(SeatPanel.EMPTY), BorderLayout.SOUTH); } private void addLabel(String name, String text, String layout) { SwingUtil.addLabel(this, name, text, layout); } public void setPlayerName(String name) { SwingUtil.getLabel(this, PLAYER_NAME).setText(name); } public void addHoleCards(String card1, String card2) { JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); SwingUtil.addLabel(panel, CARD1, card1, BorderLayout.NORTH); SwingUtil.addLabel(panel, CARD2, card2, BorderLayout.SOUTH); add(panel, BorderLayout.CENTER); setSize(getMinimumSize()); validate(); } }
The new Swing utility method addLabel is shown in Listing 12, with accompanying test in Listing 13. I have no idea how to verify the layout position string. As with other layout-related code, we’ll punt.
Listing 12 SwingUtilTest for addLabel.
public void testAddLabel() { final String text = "text"; final String layout = BorderLayout.SOUTH; SwingUtil.addLabel(panel, NAME, text, layout); JLabel label = SwingUtil.getLabel(panel, NAME); assertEquals(text, label.getText()); }
Listing 13 addLabel.
public static void addLabel( JPanel panel, String name, String text, String layout) { JLabel label = new JLabel(text); label.setName(name); panel.add(label, layout); }
This test initially fails! The problem is that we’re putting components on a panel that we embed within another panel. Our Swing utility method to find a component by name looks at only top-level components. As with any defect, our job is to first write a test that duplicates the problem. See Listing 14 for the test and Listing 15 for the resolution, which solves the problem by using recursion.
Listing 14 Finding components within components (SwingUtilTest).
public void testGetEmbeddedComponent() { JPanel subpanel = new JPanel(); subpanel.add(button); panel.add(subpanel); assertSame(button, SwingUtil.getComponent(panel, NAME)); }
Listing 15 The updated SwingUtil method getComponent.
public static Component getComponent(Container panel, String name) { for (Component component: panel.getComponents()) { if (name.equals(component.getName())) return component; if (component instanceof Container) { Component subcomponent = getComponent((Container)component, name); if (subcomponent != null) return subcomponent; } } return null; }
Now that the SeatPanel supports setting hole cards, we can code everything else. Support for adding hole cards via a TablePanel object is shown in Listings 16 and 17. Maybe we could’ve used a mock here, though—it seems as if we’ve already written this test.
Listing 16 Adding hole cards via the TablePanel.
public void testAddHoleCards() { final String position = "1"; final String card1 = "JC"; final String card2 = "3D"; table.seat("x", position); table.addHoleCards(position, card1, card2); SeatPanel panel = table.getSeatPanel(position); SwingTestUtil.assertLabelText(panel, SeatPanel.CARD1, card1); SwingTestUtil.assertLabelText(panel, SeatPanel.CARD2, card2); }
Listing 17 New method in TablePanel.
public void addHoleCards(String position, String card1, String card2) { getSeatPanel(position).addHoleCards(card1, card2); }
We can now code testDeal in HoldEmAppTest (see Listing 18). We’ll use a mock technique, which seems simpler than determining whether or not all the lower-level things happened.
Listing 18 testDeal.
private Card[] top4; public void testDeal() { final Map<String, String[]> cardsDisplayed = new HashMap<String, String[]>(); TablePanel panel = new TablePanel(Game.CAPACITY) { @Override public void addHoleCards(String position, String card1, String card2) { cardsDisplayed.put(position, new String[] { card1, card2 }); } }; HoldEmFrame frame = new HoldEmFrame(); frame.setTablePanel(panel); Game game = new Game() { @Override public void startHand() { super.startHand(); top4 = deck().top(4); } }; app = new HoldEmApp(frame, game); app.addPlayer(PLAYER1); app.addPlayer(PLAYER2); SwingUtil.getButton(app.getFrame().getTablePanel(), TablePanel.DEAL_BUTTON).doClick(); assertPanelCards(cardsDisplayed.get("1"), top4[0], top4[2]); assertPanelCards(cardsDisplayed.get("2"), top4[1], top4[3]); } private void assertPanelCards(String[] actual, Card... expected) { assertEquals(expected.length, actual.length); for (int i = 0; i < expected.length; i++) assertEquals(expected[i].toString(), actual[i]); }
Does that make sense? The more you see this kind of code, the easier it is to read. I find it concise and reasonably easy to follow. First, we create a fake implementation of addHoleCards that simply tracks the arguments when it gets called. We create an override implementation in Game so that we can capture the top four cards from the deck after the hand starts, but before the hole cards are dealt. We create the app, add a couple of players, and then click the Deal button. Finally, we can verify that the application told the TablePanel to display the top four cards in proper position order.
If you adhere to zealot-level refactoring going forward, these test methods will become much simpler to write and even easier to read. You might consider a mock framework if you have need for a lot of sequencing tests. But my preference is to make sure that I don’t need a lot of sequencing tests in the first place. I can imagine that with some design changes, the view classes could more readily support sensing at the application layer.
Oh, yeah—Listing 19 shows the updated HoldEmApp code. Note the overloaded constructor, added to support our need to inject mocks.
Listing 19 HoldEmApp code.
public class HoldEmApp { private HoldEmFrame frame; private Game game; public HoldEmApp() { this(new HoldEmFrame(), new Game()); } public HoldEmApp(HoldEmFrame frame, Game game) { this.frame = frame; this.game = game; initialize(); } private void initialize() { frame.getTablePanel().addDealListener(new ActionListener() { public void actionPerformed(ActionEvent event) { dealHoleCards(); }}); } ... private void dealHoleCards() { game.startHand(); game.dealHoleCards(); for (int i = 0; i < game.players().size(); i++) { Player player = game.players().get(i); String card1 = player.holeCards().get(0).toString(); String card2 = player.holeCards().get(1).toString(); frame.getTablePanel().addHoleCards( TablePanel.getPosition(i), card1, card2); } } }
Don’t forget to run the application and make sure it actually works!
Ultimately, testing Swing applications is a bear, because building Swing applications is a bear. Building a robust Swing application requires a lot of code. Keeping your design clean by virtue of driving it through tests should help. I view this as a tradeoff: Sure, it’s painful to write unit tests for Swing code, particularly when you get to coding tests against the presenter. However, maintaining a poorly designed Swing application is much more painful. A typically coded Swing application is harder to comprehend, costs more when locating defects, is more brittle, and so on. Given that choice, I’ll always take the benefits that comprehensive tests give me.