Chapter 7. Passing the First Test

Building the Test Rig

At the start of every test run, we need our test script to start up the Openfire server, create accounts for the Sniper and Auction, and then run the tests. Each test will start instances of the application and fake auction and test their communication through the server. At first, we’ll run everything on the same host. Later, as the infrastructure stabilises, we can consider running different components on different machines, which will be a better match to the real deployment. This leaves us with two components to write for the test infrastructure: ApplicationRunner and FakeAuctionServer.

The Application Runner

An ApplicationRunner is an object that wraps up everything to do with managing and communicating with the Swing application we're building. It runs the application as if from the command line, obtains and holds a reference to it's main window for querying the state of the GUI and finally shutting down the application at the end of the test.

We don't have to do much here, because we can rely on WindowLicker to do the hard work: find and control Swing GUI components, synchronise with Swing's threads and event queue and and wrap that all up behind a simple API.[2] WindowLicker has the concept of a ComponentDriver an object that can manipulate a feature in a Swing user interface. If a ComponentDriver can't find the Swing Component it refers to, it will time out with an error. For this test, we're just looking for a Label component that shows a given string. If our code doesn't produce this Label, we'll get an exception. Here's the implementation, with the constants left out for clarity, and some explanation.

public class ApplicationRunner {
  public static final String SNIPER_ID = "sniper";
  public static final String SNIPER_PASSWORD = "sniper";
  private AuctionSniperDriver driver;

  public void startBiddingIn(final FakeAuctionServer auction) {
    Thread thread = new Thread("Test Application") {
      @Override public void run() { 1
        try {
          Main.main(XMPP_HOSTNAME, Integer.toString(XMPP_PORT_NUMBER), 
                    SNIPER_ID, SNIPER_PASSWORD,
                    auction.getItemId()); 2 
        } catch (Exception e) {
          e.printStackTrace(); 3 
        }
      }
    };
    thread.setDaemon(true);
    thread.start();
    
    driver = new AuctionSniperDriver(1000); 4
    driver.showsSniperStatus(STATUS_JOINING); 5
  }

  public void showsSniperHasLostAuction() {
    driver.showsSniperStatus(STATUS_LOST);  6
  }

  public void stop() {
    if (driver != null) {
      driver.dispose(); 7
    }
  }
}

1

We call the application through its main() function to make sure we've assembled the pieces correctly. We're following the convention that the entry point to the application is a Main class in the top-level package. WindowLicker can control Swing components if they're in the same JVM, so we start the Sniper in a new thread. Ideally, the test would start the Sniper in a new process but that would be much harder to test, so we think this is a reasonable compromise.

2

To keep things simple at this stage, we'll assume that we're only bidding for one item and pass the identifier to main().

3

If main() throws an exception we just write it out for now. Whatever test we're running will have failed and we can look for the stack trace in the output.

4

We turn down the timeout period for finding frames and components. The default values are longer than we need for a simple application like this one and will slow down the tests when they fail. We use one second, which is enough to smooth over minor runtime delays.

5

We wait for the status to change to “Joining” so we know the application has attempted to connect. This assertion says that somewhere in the user interface there's a label that describes the Sniper's state.

6

When the Sniper loses the auction, we expect it to show a “Lost” status.

7

After the test, we tell the Driver to dispose of the window to make sure it won't be picked up in another test before being garbage collected.

The AuctionSniperDriver is simply an extension of a WindowLicker JFrameDriver specialised for our tests.

public class AuctionSniperDriver extends JFrameDriver {
  public AuctionSniperDriver(int timeoutMillis) {
    super(new GesturePerformer(), 
          JFrameDriver.topLevelFrame(
            named(Main.MAIN_WINDOW), 
            showingOnScreen()),
            new AWTEventQueueProber(timeoutMillis, 100));
  }

  public void showsSniperStatus(String status) {
    new JLabelDriver(this, 
                     named(Main.STATUS_LABEL_NAME)).hasText(equalTo(status));
  }
}

On construction, it attempts to find a visible top level window for the Auction Sniper within the given timeout. The method showsSniperStatus() looks for the relevant label in the user interface and confirms that it shows the given status.

The Fake Auction

A FakeAuctionServer is a substitute server that allows the test to check how the AuctionSniper will interact with an Auction using XMPP messages. It has three responsibilities: it must connect to the XMPP broker and accept a request to join the chat from the Sniper; it must receive chat messages from the Sniper or fail if no message arrives within some timeout; and it must allow the test to send messages back to the Sniper as specified by Southabee's interface documentation.

Smack is event-driven, so the FakeAuction has to register listener objects for it to call back. There are two levels of event: events about a chat, such as people joining, and events within a chat, such as messages being received. We need to listen for both.

We'll start by implementing the startSellingItem() method. First it connects to the XMPP broker, using the Item identifier to construct the login name, then it registers a ChatManagerListener. Smack will call this listener with a Chat object that represents the session when a Sniper connects in. The FakeAuction holds on to the chat so it can exchange messages with the Sniper. So far, we have:

Need a picture here to explain Smack callbacks

public class FakeAuctionServer {
  public static final String ITEM_ID_AS_LOGIN = "auction-%s"; 
  public static final String AUCTION_RESOURCE = "Auction";
  public static final String XMPP_HOSTNAME = "localhost";
  public static final int XMPP_PORT_NUMBER = 5222;
  private static final String AUCTION_PASSWORD = "auction";

  private final String itemId;
  private final XMPPConnection connection;
  private Chat currentChat;

  public FakeAuctionServer(String itemId) {
    this.itemId = itemId;
    this.connection = new XMPPConnection(XMPP_HOSTNAME);
  }

  public void startSellingItem() throws XMPPException {
    connection.connect(); 
    connection.login(format(ITEM_ID_AS_LOGIN, itemId), 
                     AUCTION_PASSWORD, AUCTION_RESOURCE);
    connection.getChatManager().addChatListener(new ChatManagerListener() {
      public void chatCreated(Chat chat, boolean createdLocally) {
        currentChat = chat;
      }
    });
  }

  public String getItemId() {
    return itemId;
  }
}

A minimal fake implementation

We want to emphasise again that this fake is a minimal implementation just to support testing. For example, we use a single instance variable to hold the chat object. A real Auction server would manage multiple chats for all the bidders but this is a fake: it's only purpose is to support the test, so it only needs one.

Next we have to add a MessageListener to the chat to accept messages from the Sniper. This means that we need to coordinate between the thread that runs the test and the Smack thread that feeds messages to the listener—the test has to wait for messages to arrive and time out if they don't—so we'll use a single element BlockingQueue from the java.util.concurrent package. Just as we only have one chat in the test, we expect to process only one message at a time. To make our intentions clearer, we wrap the queue in a helper class SingleMessageListener. Here's the rest of FakeAuctionServer.

public class FakeAuctionServer {
  private final SingleMessageListener messageListener = 
                                             new SingleMessageListener();
  
  public void startSellingItem() throws XMPPException {
    connection.connect(); 
    connection.login(format(ITEM_ID_AS_LOGIN, itemId), 
                            AUCTION_PASSWORD, AUCTION_RESOURCE);
    connection.getChatManager().addChatListener(new ChatManagerListener() {
      public void chatCreated(Chat chat, boolean createdLocally) {
        currentChat = chat;
        chat.addMessageListener(messageListener);
      }
    });
  }
    
  public void receivesJoinRequestFromSniper() throws InterruptedException {
    messageListener.receivesAMessage(); 1
  }
  
  public void announceClosed() throws XMPPException {
    currentChat.sendMessage(new Message()); 2
  }

  public void stop() {
    connection.disconnect(); 3
  }
}
public class SingleMessageListener implements MessageListener {
  private final ArrayBlockingQueue<Message> messages = 
                              new ArrayBlockingQueue<Message>(1);

  public void processMessage(Chat chat, Message message) {
    messages.add(message);
  }

  public void receivesAMessage() throws InterruptedException {
    assertThat("Message", messages.poll(5, SECONDS), is(notNullValue())); 1
  }
}

1

The test needs to know when a Join message has arrived. We just check whether any message has arrived, since the Sniper will only be sending Join messages to start with; we'll fill in more detail as we grow the application. This implementation will fail if no message is received within 5 seconds.

2

The test needs to be able to simulate the Auction announcing when it closes, which is why we held onto the currentChat when it opened. As with the Join request, the FakeAuction just sends an empty message, since this is the only event we support so far.

3

stop() just closes the connection.

1

The clause is(notNullValue()) uses the Matcher syntax. We'll describe Matchers in the section called “Methods And Expectations” but, for now, we'll just say that this checks that the Listener has received a message within the timeout period.

The Message Broker

There's one more component to mention which doesn't involve any coding from us, the installation of an XMPP Message Broker. In this case, we set up an instance of Openfire on our local host. The Sniper and FakeAuction in our end-to-end tests, even though they're running in the same process, will communicate through this server. We also set up logins to match the small number of Item identifiers that we'll be using in our tests.

A working compromise

As we wrote before, we are cheating a little at this stage to keep development moving. We want all the developers to have their own environments so they don't interfere with each other when running their tests; we have seen teams make their lives very complicated because they didn't want to create a database instance for each developer. In a professional organisation, we would also expect to see at least one test rig that represented the production environment, including the distribution of processing across a network, and a build cycle to make sure it works.

Failing and passing the test

We have enough infrastructure in place to run the test and watch it fail. For the rest of this chapter we'll add functionality, a tiny slice at a time until eventually we make the test pass. When we first started using this technique it felt too fussy, "Just write the code, you know what to do!" Over time, we realised that it didn't take any longer and that our progress was much more predictable. Focusing on just one aspect at a time helps us to make sure we understand it and that, as a rule, when we get something working it stays working. Where there's no need to discuss the solution, many of these steps take hardly any time at all — they take longer to explain than to implement.

We start by writing a build script for ant. We'll skip over the details of its content, since it's standard practice these days, but the important point is that we always have a single command that reliably compiles, builds, deploys, and tests the application, and that we can run it repeatedly; we only start coding once we have the automated build and test working.

At this stage, we'll describe each step, taking each test failure in turn. Later we'll speed up the pace.

First user interface

Test failure

The test can't find a user interface component with the name "AuctionSniper Main".

java.lang.AssertionError: 
Tried to look for...
    exactly 1 JFrame (with name "Auction Sniper Main" and showing on screen)
    in all top level windows
but...
    all top level windows
    contained 0 JFrame (with name "Auction Sniper Main" and showing on screen)
  [...]
  at auctionsniper.ApplicationRunner.stop()
  at auctionsniper.AuctionSniperEndToEndTest.stopApplication()
  [...]

WindowLicker is verbose in its error reporting to try to make failures easy to understand. In this case, we couldn't even find the top level frame so JUnit failed before even starting the test. This stack trace comes from the @After method that stops the application.

Implementation

We need a top-level window for our application. We write a auctionsniper.ui.MainWindow class that extends Swing's JFrame, and call it from main(). All it will do is create a window with the right name.

public class Main {
  private MainWindow ui;
  
  public Main() throws Exception {
    startUserInterface();
  }

  public static void main(String... args) throws Exception {
    Main main = new Main();
  }
  
  private void startUserInterface() throws Exception {
    SwingUtilities.invokeAndWait(new Runnable() {
      public void run() {
        ui = new MainWindow();
      }
    });
  }
}
public class MainWindow extends JFrame {
  public MainWindow() {
    super("Auction Sniper");
    setName(MAIN_WINDOW_NAME);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setVisible(true);
  }
}

Unfortunately, this is a little complicated because Swing requires us to create the user interface on its event dispatch thread. We've further complicated the implementation so we can hang on to the main window object in our code; it's not strictly necessary here but we thought we'd get it over with.

Notes

The user interface in Figure 7.1, “Just a top-level window” really is minimal. It does not look like much but it confirms that we can start up an application window and connect to it.

Just a top-level window

Figure 7.1. Just a top-level window


Our test still fails, but we've moved on a step. Now we know that our harness is working, which is one less thing to worry about as we move on to more interesting functionality.

Showing the Sniper state

Test failure

The test finds a top-level window, but no display of the current state of the Sniper. To start with, the Sniper should show Joining while waiting for the Auction to respond.

java.lang.AssertionError: 
Tried to look for...
    exactly 1 JLabel (with name "sniper status")
    in exactly 1 JFrame (with name "Auction Sniper Main" and showing on screen)
    in all top level windows
and check that its label text is "Joining"
but...
    all top level windows
    contained 1 JFrame (with name "Auction Sniper Main" and showing on screen)
    contained 0 JLabel (with name "sniper status")
  at com.objogate.wl.AWTEventQueueProber.check()
  [...]
  at AuctionSniperDriver.showsSniperStatus()
  at ApplicationRunner.startBiddingIn()
  at AuctionSniperEndToEndTest.sniperJoinsAuctionUntilAuctionCloses()
  [...]

Implementation

We add a label representing the Sniper's state to MainWindow.

public class MainWindow extends JFrame {
  public static final String AUCTION_STATUS_NAME = "auctionStatus";
  private final JLabel auctionStatus = createLabel(STATUS_JOINING);
  
  public MainWindow(){
    super("Auction Sniper");
    setName(MAIN_WINDOW_NAME);
    add(auctionStatus);
    pack();
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setVisible(true);
  }

  private static JLabel createLabel(String initialText) {
    JLabel result = new JLabel(initialText);
    result.setName(AUCTION_STATUS_NAME, );
    result.setBorder(new LineBorder(Color.BLACK));
    return result;
  }
}

Notes

Another minimal change, but now we can show some content in our application Figure 7.2, “Showing Joining status”.

Showing Joining status

Figure 7.2. Showing Joining status


Connecting to the Auction

Test failure

Our user interface is working, but the Auction does not receive a Join request from the Sniper.

java.lang.AssertionError: 
Expected: is not null
     got: null
  at org.junit.Assert.assertThat()
  at SingleMessageListener.receivesAMessage()
  at FakeAuctionServer.receivesJoinRequestFromSniper()
  at AuctionSniperEndToEndTest.sniperJoinsAuctionUntilAuctionCloses()
  [...]

This failure message is a bit cryptic, but the names in the stack trace tell us what's wrong.

Implementation

We write a simplistic implementation to get us past this failure, it connects to the chat in Main and sends an empty message. We create a null MessageListener to allow us to create a Chat for sending the empty initial message, since we don't yet care about receiving messages.

public class Main {
  public static final String AUCTION_RESOURCE = "Auction";
  public static final String ITEM_ID_AS_LOGIN = "auction-%s";
  public static final String AUCTION_ID_FORMAT = 
                               ITEM_ID_AS_LOGIN + "@%s/" + AUCTION_RESOURCE;

  [...]

  public static void main(String... args) throws Exception {
    Main main = new Main();
    XMPPConnection connection = connect(args[0], args[1], args[2]);
    Chat chat = connection.getChatManager().createChat(
        auctionId(args[3], connection), 
        new MessageListener() {
          public void processMessage(Chat aChat, Message message) {
            // nothing yet
          }
        });
    chat.sendMessage(new Message());
  }

  private static XMPPConnection connect(String hostname, 
                                        String username, 
                                        String password) 
    throws XMPPException 
  {
    XMPPConnection connection = new XMPPConnection(hostname);
    connection.connect();
    connection.login(username, password, AUCTION_RESOURCE);
  
    return connection;
  }

  private static String auctionId(XMPPConnection connection, String itemId) {
    return String.format(AUCTION_ID_FORMAT, itemId, 
                         connection.getServiceName()); 
  }
  [...]
}

Notes

This shows that we can establish a connection from the Sniper to the Auction, which means we had to sort out details such as interpreting the item and user credentials from the command-line arguments, and using the Smack library. We're leaving the message contents until later because we only have one message type, so sending an empty value is enough to prove the connection.

This implementation may seem gratuitously naive, after all we should be able to design a structure for something as simple as this, but we've often found it worth writing a small amount of ugly code and seeing how it falls out. It helps us to test our ideas before we've gone too far and sometimes the results can be surprising. The important point is to make sure we don't leave it ugly.

We make a point of keeping the connection code out of the Swing invokeAndWait() call that creates the MainWindow, because we want the user interface to settle before we try anything more complicated.

Receiving a response from the Auction

Test failure

With a connection established, the Sniper should receive and display the Lost response from the Auction, of course it doesn't.

java.lang.AssertionError: 
Tried to look for...
    exactly 1 JLabel (with name "sniper status")
    in exactly 1 JFrame (with name "Auction Sniper Main" and showing on screen)
    in all top level windows
and check that its label text is "Lost"
but...
    all top level windows
    contained 1 JFrame (with name "Auction Sniper Main" and showing on screen)
    contained 1 JLabel (with name "sniper status")
    label text was "Joining"
  [...]
  at AuctionSniperDriver.showsSniperStatus()
  at ApplicationRunner.showsSniperHasLostAuction()
  at AuctionSniperEndToEndTest.sniperJoinsAuctionUntilAuctionCloses()
  [...]

Implementation

We need to attach the user interface to the chat so it can receive the response from the Auction, so we create a connection and pass it to Main to create the Chat object. joinAction() creates a MessageListener that sets the status label, using an invokeLater() call to avoid blocking the Smack library. As with the Join message, we don't bother with the contents of the incoming message since there's only one possible response the Auction can send at the moment. While we're at it, we rename connect() to connection() to make the code read better.

public class Main {
  @SuppressWarnings("unused") private Chat notToBeGCd;
  [...]
  public static void main(String... args) throws Exception {
    Main main = new Main();
    main.joinAuction(connection(args[0], args[1], args[2]), args[3]);
  }

  private void joinAuction(XMPPConnection connection, String itemId) 
    throws XMPPException
  {
    final Chat chat = connection.getChatManager().createChat(
        auctionId(itemId, connection), 
        new MessageListener() {
          public void processMessage(Chat aChat, Message message) {
            SwingUtilities.invokeLater(new Runnable() {
              public void run() {
                ui.showStatus(MainWindow.STATUS_LOST);
              }
            });
          }
        });
    this.notToBeGCd = chat;
    
    chat.sendMessage(new Message());
  }

Why the chat field?

You'll notice that we've assigned the chat that we create to the field notToBeGCd in Main, this is to make sure that the chat is not garbage collected by the Java runtime. There's a note at the top of the ChatManager that says:

The chat manager keeps track of references to all current chats. It will not hold any references in memory on its own so it is neccesary to keep a reference to the chat object itself.

If the chat is garbage collected, the Smack runtime will hand the message to a new Chat which it will create for the purpose. In an interactive application we would listen for and show these new chats, but our needs are different so we add this quirk to stop it happening.

We made this implementation clumsy on purpose to highlight in the code why we're doing it. We also know that we're likely to come up with a better solution in a while.

We implement the display method in the user interface and, finally, the whole test passes.

public class MainWindow extends JFrame {
  [...]
  public void showStatus(String status) {
    auctionStatus.setText(status);
  }
}

Notes

Here (Figure 7.3, “Showing Lost status”) is visible confirmation that the code works.

Showing Lost status

Figure 7.3. Showing Lost status


It may not look like much, but it confirms that a Sniper can establish a connection with an Auction, accept a response, and display the result.

The necessary minimum

In one of his school reports Steve was noted as “a fine judge of the necessary minimum”. It seems he's found his calling in writing software since that's a critical skill during Iteration Zero.

What we hope you've seen in this chapter is the degree of focus that's required to put together the first Walking Skeleton. The point is to design and validate the initial structure of the end-to-end system, to prove that our choices of packages, libraries, and tooling will actually work—where end-to-end includes deployment to a working environment. The team needs a sense of urgency that will help them to strip the functionality down to the absolute minimum that will test their assumptions. That's why we didn't put any content in our Sniper messages, it's a diversion from making sure that the communication and event handling work. We didn't sweat too hard over the detailed code design, partly because there isn't much but mainly because we're just getting the pieces in place; that effort will come soon enough.

Of course, all we've written up are the edited highlights. We've left out the diversions and discussions that happened along the way as we figured out which pieces to use and how to make them work, crawling through the product documentation and discussion lists. We've also left out some of the discussions that we went through about what this project is for. Iteration Zero usually brings up project chartering issues as the team looks for criteria to guide its decisions, so the project sponsors should expect to field some deep questions about its motivation.

We have something visible we can present as a sign of progress, so we can cross off the first item on our list, as in Figure 7.4, “First Item Done”.

First Item Done

Figure 7.4. First Item Done


The next step is to start building out real functionality.



[2] We're assuming that you know how Swing works, as there are many other books that do a good job of describing it. The essential point from our point of view is that it's an event-driven framework that creates its own internal threads to dispatch events, so we can't be precise about when things will happen.