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.
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() {
try {
Main.main(XMPP_HOSTNAME, Integer.toString(XMPP_PORT_NUMBER),
SNIPER_ID, SNIPER_PASSWORD,
auction.getItemId());
} catch (Exception e) {
e.printStackTrace();
}
}
};
thread.setDaemon(true);
thread.start();
driver = new AuctionSniperDriver(1000);
driver.showsSniperStatus(STATUS_JOINING);
}
public void showsSniperHasLostAuction() {
driver.showsSniperStatus(STATUS_LOST);
}
public void stop() {
if (driver != null) {
driver.dispose();
}
}
}We call the application through its
| |
To keep things simple at this stage, we'll assume that we're
only bidding for one item and pass the identifier to
| |
If | |
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. | |
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. | |
When the Sniper loses the auction, we expect it to show a “Lost” status. | |
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.
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;
}
}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();
}
public void announceClosed() throws XMPPException {
currentChat.sendMessage(new Message());
}
public void stop() {
connection.disconnect();
}
}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()));} }
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. | |
The test needs to be able to simulate the Auction announcing
when it closes, which is why we held onto the
| |
| |
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. |
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.
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.
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.
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.
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.
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.
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.
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()
[...]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;
}
}Another minimal change, but now we can show some content in our application Figure 7.2, “Showing Joining status”.
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.
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());
}
[...]
}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.
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()
[...]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());
}
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);
}
}Here (Figure 7.3, “Showing Lost status”) is visible confirmation that the code works.
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.
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”.
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.
Copyright © 2008 Steve Freeman and Nat Pryce