JaVa
   

File Dummies

We wrote a test for the FileLogger class in . The ugly thing about this test was that we had to state the name of a test file as constant. The consequence was that we became dependent not only upon the file system used, but also upon things like security settings and available disk space. Although the class java.io.File since JDK 1.2 offers us a mechanism to create temporary files regardless of a constant file path, the dependence upon the file system's availability and its access privileges still prevails in this method. Why not use our newly gained knowledge about dummy and mock classes to program our own MockFile class? Easier said than done, because the JDK developers built a few obstacles to make things a little more difficult for us. The biggest obstacle is the fact that the class java.io.File does not hold what its name promises. It is not an abstraction of all the things we would like to do with files, but merely an abstraction of a file name and its access path within a specific file system. The actual file functionality is hidden in the classes java.io.FileInputStream and java.io.FileOutputStream. Is it worth our while to create mock classes for these two streams by overriding several read and write methods? In most cases, it is not worthwhile, because the functionality of streams which write their data into buffers and read data from buffers are already available in the form of various other stream classes (e.g., ByteArrayInput-Stream and ByteArrayOutputStream). These can then take the place of file streams for testing purposes. To better understand this, we will use our FileLogger example again (see ). First, we turn FileLogger into a StreamLogger, which offers us two constructors:

import java.io.*;
public class StreamLogger implements Logger {
 private PrintWriter writer;
 public StreamLogger(OutputStream out) throws IOException {
 writer = new PrintWriter(out);
 }
 public StreamLogger(String filename) throws IOException {
 this(new FileOutputStream(filename));
 }
 public void close() {
 writer.close();
 }
 public void logLine(String logMessage) {
 writer.println(logMessage);
 }
}


Now we can pass an arbitrary InputStream within the constructor, allowing us to test independently of any file access:

public void testLogLine() throws IOException {
 ByteArrayOutputStream out = new ByteArrayOutputStream();
 StreamLogger logger = new StreamLogger(out);
 logger.logLine("Line 1");
 logger.logLine("Line 2");
 logger.close();
 ByteArrayInputStream in =
 new ByteArrayInputStream(out.toByteArray());
 BufferedReader reader =
 new BufferedReader(new InputStreamReader(in));
 assertEquals("Line 1", reader.readLine());
 assertEquals("Line 2", reader.readLine());
 assertNull("end of file reached", reader.readLine());
 reader.close();
}


All this messing about with different Stream and Writer classes can now be nicely put inside a reusable mock class:

import java.io.*;
import java.util.*;
public class MockTextOutputStream extends OutputStream {
 private ByteArrayOutputStream outputStream;
 private List expectedLines = new ArrayList();
 private boolean streamClosed = false;
 public MockTextOutputStream() {
 outputStream = new ByteArrayOutputStream();
 }
 public void addExpectedLine(String line) {
 expectedLines.add(line);
 }
 public void close() throws IOException {
 streamClosed = true;
 outputStream.close();
 }
 public void flush() throws IOException {
 outputStream.flush();
 }
 private InputStreamReader getReader() {
 InputStream input =
 new ByteArrayInputStream(outputStream.toByteArray());
 return new InputStreamReader(input);
 }
 public void verify() throws IOException {
 if (!streamClosed) {
 Assert.fail("Stream was not closed");
 }
 BufferedReader reader =
 new BufferedReader(this.getReader());
 Iterator i = expectedLines.iterator();
 while(i.hasNext()) {
 String expectedLine = (String) i.next();
 String actualLine = reader.readLine();
 Assert.assertEquals(expectedLine, actualLine);
 }
 Assert.assertNull("EOF expected", reader.readLine());
 }
 public void write(byte[] b) throws IOException {
 outputStream.write(b);
 }
 public void write(int b) throws IOException {
 outputStream.write(b);
 }
}


We can use this MockTextOutputStream whenever it is a matter of validating an OutputStream. Normally, we will have to adapt this mock class to the local requirements. Our unmodified test, using MockTextOutputStream, now looks like this:

public void testLogLine() throws IOException {
 MockTextOutputStream mockStream =
 new MockTextOutputStream();
 mockStream.addExpectedLine("Line 1");
 mockStream.addExpectedLine("Line 2");
 StreamLogger logger = new StreamLogger(mockStream);
 logger.logLine("Line 1");
 logger.logLine("Line 2");
 logger.close();
 mockStream.verify();
}



Note that the actual test code has become shorter. The more tests of this kind we have, the more rewarding our investment in the mock stream. We can see in this example that the testLogLine() method is very similar to the last version of the testSimpleLogging() method in LogServerTest (see ). The reason is that StreamLogger does nothing but move incoming log lines into a PrintWriter. Whether or not this sheer delegation requires its own test at all may be viewed differently, but we are in favor of creating that test. Even when we currently see that the code of our logLine() method does exactly what it should do, the situation could be totally different after the next refactoring.

Following the MockTextOutputStream example, we recommend at this point to implement a MockTextInputStream class as a practical exercise, or better yet a coffee break. ...


JaVa
   
Comments