Extreme Programming Perspectives

Suppose that your boss wants you to write a tool that performs gets on HTTP servers. You decide to write a class called HTTPGet, shown in Listing 37.1, and following XP practices, you write your unit tests and code (in that order).

Listing 37.1 The HTTPGet class

package xpPerspectives; import java.net.*; import java.io.*; public class HTTPGet { static final String GET_REQUEST = "GET / HTTP/1.0\n\n"; private Socket socket; void setSocket(Socket newSocket) { socket = newSocket; } public static void main(String [] arguments) throws IOException { HTTPGet httpGet = new HTTPGet(); String response = httpGet.mainInternal(arguments); System.out.println(response); } public String usage() { return this.getClass().getName() + " <hostname>"; } String mainInternal(String [] arguments) throws IOException { if (arguments.length != 1) { return usage(); } String host = arguments[0]; connectToServer(host); return getRoot(); } private void connectToServer(String host) throws IOException { // don't overwrite if socket is being mocked if (socket == null) socket = new Socket(host,80); } private String getRoot() throws IOException { OutputStream output = socket.getOutputStream(); output.write(GET_REQUEST.getBytes()); InputStream input = socket.getInputStream(); StringBuffer buffer = new StringBuffer(); while (true) { int c = input.read(); if (c == -1) break; buffer.append((char) c); } output.close(); input.close(); return buffer.toString(); } }

Here is one of the unit tests that you write.

public void testGetRoot() throws Exception { HTTPGet httpGet = new HTTPGet(); String response = httpGet.mainInternal( new String[] {"mastodon"}); assertTrue(response.startsWith("HTTP/1.1 200")); }

Unfortunately, you find that sometimes the server (mastodon) is down and the test fails. Another time, you take the code home, and the test fails because you aren't on the intranet. Frustrating!

Then it occurs to you that instead of using an actual socket in HTTPGet, why not replace it with a mock socket that fools the tool into thinking it is talking to a real socket, when in fact it is talking to your mock socket.

In keeping with XP, you want to do the simplest thing that possibly works. So instead of going to the trouble of creating a complete mock socket class, you create an anonymous class in your unit test that extends the socket, as shown in Listing 37.2.

You are feeling pretty smug until your pair points out that your Web client's calls to the mock socket could be leaking through to the real socket. To trap all leaks, you write a guard class and extend your anonymous mock socket from your guard class, as shown in Listing 37.3.

Phew, that was a lot of work. But now you are ready to refactor your tests to use the guard object, as shown in Listing 37.4.

Listing 37.2 Anonymous class for the mock socket

public void testGetRootUsingUnguardedMockObjects() throws Exception { String expectedResponse = "HTTP/1.1 200 OK\n"; final ByteArrayOutputStream output = new ByteArrayOutputStream(); final ByteArrayInputStream input = new ByteArrayInputStream(expectedResponse.getBytes()); Socket mockSocket = new Socket() { public InputStream getInputStream() { return input; } public OutputStream getOutputStream() { return output; } public void close() { } }; HTTPGet httpGet = new HTTPGet(); httpGet.setSocket(mockSocket); String response = httpGet.mainInternal( new String[] {"mastodon"}); assertEquals(expectedResponse, response); assertEquals(HTTPGet.GET_REQUEST, output.toString()); }

You and your pair are feeling pretty good about your tests because now they don't depend on servers being up and the Internet being accessible, and your unit tests run faster. But writing the guard object did take a lot of work. You wish there was a tool that did this automatically for you.

Listing 37.3 Guard class for the mock socket

package xpPerspectives; public class GuardSocket extends java.net.Socket { public GuardSocket() { } public java.io.InputStream getInputStream() throws java.io.IOException { throw new RuntimeException("unimplemented getInputStream() invoked"); } // override all 41 public methods of java.net.Socket in the same way }

Listing 37.4 Using the guard object

public void testGetRootUsingGuardedMockObjects() throws Exception { String expectedResponse = "HTTP/1.1 200 OK\n"; final ByteArrayOutputStream output = new ByteArrayOutputStream(); final ByteArrayInputStream input = new ByteArrayInputStream(expectedResponse.getBytes()); GuardSocket mockSocket = new GuardSocket() { public InputStream getInputStream() { return input; } public OutputStream getOutputStream() { return output; } public void close() { } }; HTTPGet httpGet = new HTTPGet(); httpGet.setSocket(mockSocket); String response = httpGet.mainInternal( new String[]{"mastodon"}); assertEquals(expectedResponse, response); assertEquals(HTTPGet.GET_REQUEST, output.toString()); }

So we wrote one for you. To generate the previous guard object, use the following command:

java tools.GuardGenerator java.net.Socket xpPerpspectives.GuardSocket

In general use:

java tools.GuardGenerator <class to mock> <guard class>

Категории