Unit Test Life Cycle

Goal

Concepts

Library

Dependencies

Lesson

The unit tests you have written so far have been simple and straightforward: each method that is annotated with @Test is executed as a separate test without further ado. But executing a unit test is only part of the story. Each unit test goes through a life cycle, with the @Test method being only one step in the testing process.

Life Cycle

@Before and @After

Just as JUnit provides the @Test to indicate JUnit which method should be used as a test, JUnit also provides two annotations to indicate methods that should be run before and after a test. These are the org.junit.Before and org.junit.After annotations, respectively.

Let's say that you are writing a series of tests for input streams, each of which need a sample byte input stream to test reading. Your test might look like this:

Recreating a stream of test bytes for each unit test.
public class MyInputStreamTest {

  @Test
  public void testSimpleRead() {
    //create an input stream
    final byte[] testBytes = new byte[] { 0x74, 0x6F, 0x75, 0x63, 0x68, (byte)0xC3, (byte)0xA9 };
    try(final InputStream testInputStream = new ByteArrayInputStream(testBytes)) {
      //TODO do the test with testInputStream
    }
  }

  @Test
  public void testBufferedRead() {
    //create an input stream
    final byte[] testBytes = new byte[] { 0x74, 0x6F, 0x75, 0x63, 0x68, (byte)0xC3, (byte)0xA9 };
    try(final InputStream testInputStream = new ByteArrayInputStream(testBytes)) {
      //TODO do the test with testInputStream
    }
  }

  @Test
  public void testMySpecialRead() {
    //create an input stream
    final byte[] testBytes = new byte[] { 0x74, 0x6F, 0x75, 0x63, 0x68, (byte)0xC3, (byte)0xA9 };
    try(final InputStream testInputStream = new ByteArrayInputStream(testBytes)) {
      //TODO do the test with testInputStream
    }
  }

}

Notice that the test input stream you create is the same each time. You could prevent this duplication by using JUnit's @Before and @After annotations.

Using @Before and @After to consolidate creation and closing of a stream of test bytes for each unit test.
public class MyInputStreamTest {

  private InputStream testInputStream;

  @Before
  public void setupTestInputStream() {
    final byte[] testBytes = new byte[] { 0x74, 0x6F, 0x75, 0x63, 0x68, (byte)0xC3, (byte)0xA9 };
    testInputStream = new ByteArrayInputStream(testBytes);
  }

  @Test
  public void testSimpleRead() {
    //TODO do the test with testInputStream
  }

  @Test
  public void testBufferedRead() {
    //TODO do the test with testInputStream
  }

  @Test
  public void testMySpecialRead() {
    //TODO do the test with testInputStream
  }

  @After
  public void cleanupStream() throws IOException {
    testInputStream.close();
  }

}

A new instance of the test class (here MyInputStreamTest) will be created for each test (that is, each method annotated with @Test) is run. Any method annotated with @Before will be called before each test run. Similarly any method annotated with @After will be called after each test run.

@BeforeClass and @AfterClass

It's useful to perform some action before and after each test, but sometimes you might want to perform some action before and/or after all of the unit tests in the test class. This can be accomplished with the the org.junit.BeforeClass and org.junit.AfterClass annotations, respectively.

In the above examples we needed to create a new input stream for each test, because if we reused an input stream the position would not start at the beginning. In addition, we didn't want the input stream in one test to risk being defiled in any way by other tests.

The series of bytes from which we read, however, never changes. We could create that byte array once up front, before all the tests are ran. Each input stream could then be instantiated from the same byte array input.

Using @BeforeClass and @AfterClass to consolidate initialization and release of a sequence of bytes for all tests in the class.
public class MyInputStreamTest {

  private static testBytes;

  private InputStream testInputStream;

  @BeforeClass
  public static void setupBytes() {
   testBytes = new byte[] { 0x74, 0x6F, 0x75, 0x63, 0x68, (byte)0xC3, (byte)0xA9 };
  }

  @Before
  public void setupTestInputStream() {
    testInputStream = new ByteArrayInputStream(testBytes);
  }

  @Test
  public void testSimpleRead() {
    //TODO do the test with testInputStream
  }

  @Test
  public void testBufferedRead() {
    //TODO do the test with testInputStream
  }

  @Test
  public void testMySpecialRead() {
    //TODO do the test with testInputStream
  }

  @After
  public void cleanupStream() throws IOException {
    testInputStream.close();
  }

  @AfterClass
  public static void cleanupBytes() {
    testBytes = null;  //release the bytes to free up memory
  }

}

Rules

JUnit provides many rules, annotated by the org.junit.Rule annotation, which provide another way to consolidate functionality for various parts of the unit test life cycle. Usually the @Rule annotation is used with a field that implements org.junit.rules.TestRule. The following are some TestRule implementations you can use with the @Rule annotation.

Temporary Folder Rule

The org.junit.rules.TemporaryFolder rule takes care of creating a temporary directory before each test is run, and then deleting the temporary directory after the test is finished. Your test does not know ahead of time—or care—where this temporary folder will be located.

Automatically creating a temporary folder for each test using the TemporaryFolder rule.
public class MyInputStreamTest {

  @Rule
  public TemporaryFolder tempFolder = new TemporaryFolder();

  @Test
  public void testWriteBytesToNamedFile() {
    final Path tempDirRootPath = tempFolder.getRoot().toPath();
    final Path testFilePath = tempDirRootPath.resolve("foobar.dat");
    //TODO test writing to foobar.dat
  }

  @Test
  public void testCheckFileExistance() {
    final Path testFilePath = tempFolder.newFile().toPath();
    assertThat(Files.exists(testFilePath), is(true));
  }

}

External Resources

The TemporaryFolder rule is nothing you could not have created yourself using a combination of @Before and @After annotations on methods that set up and tear down a temporary directory. The key difference is that TemporaryFolder encapsulates a set of before and after behaviors, allowing you to reuse the functionality as a rule.

TemporaryFolder accomplishes this by extended an abstract class ExternalResource, which comes with ExternalResource.before() and ExternalResource.after() methods for you to implement. Each of these methods functions the same as the annotation equivalent, and will be called similarly by JUnit if the external resource is annotated with the @Rule annotation.

Let us assume that we have a file named test.dat that serves as a standard source of known test bytes. We want to use the contents of this file as an InputStream source for various tests. We can there create an implementation of ExternalResource named com.example.TestInputStreamResource and get an input stream to test.dat file once for every test.

ExternalResource implementation for providing an InputStream to byte in the same resource for each test.
public class TestInputStreamResource extends ExternalResource {

  public static final String TEST_RESOURCE_NAME = "test.dat";

  private InputStream inputStream;

  /** @return An input stream to the test bytes. */
  public InputStream getInputStream() {
    return inputStream;
  }

  @Override
  protected void before() throws Throwable {
    inputStream = getClass().getResourceAsStream(TEST_RESOURCE_NAME);
  };

  @Override
  protected void after() {
    try {
      inputStream.close();
    } catch(final IOException ioException) {
      throw new UncheckedIOException(ioException);
    }
    inputStream = null;
  };
}
public class MyInputStreamTest {

  @Rule
  public TestInputStreamResource testInputStreamResource = new TestInputStreamResource();

  @Test
  public void testSimpleRead() {
    final InputStream testInputStream = testInputStreamResource.getInputStream();
    //TODO do the test with testInputStream
  }

  @Test
  public void testBufferedRead() {
    final InputStream testInputStream = testInputStreamResource.getInputStream();
    //TODO do the test with testInputStream
  }

  @Test
  public void testMySpecialRead() {
    final InputStream testInputStream = testInputStreamResource.getInputStream();
    //TODO do the test with testInputStream
  }
}
Class Loaders

In the TestInputStreamResource example we access an input stream directly to the bytes of a resource stored in one of the directories. This resource may be in a separate directory in the file system, or it may be bundled in a JAR file distributed with the application.

When a class is first accessed, Java uses a class loader, an instance of java.lang.ClassLoader, to load the class from the .class file. The class loader chosen knows how to access the .class file as well as any related resources. The class loader may have a certain permission configuration to be able to access certain packages.

The easiest way to request access to a resource via a class loader is to go through an instance of java.lang.Class, because each class keeps a reference to the class loaders used to load it. The Class.getResourceAsStream(String name) will as the associated class loader to return an InputStream to the named resource, as you saw in the full TestInputStreamResource example above.

Getting an InputStream to a resource related to a class using Class.getResourceAsStream(…).
public class TestInputStreamResource extends ExternalResource

  public static final String TEST_RESOURCE_NAME = "test.dat";

  …

  @Override
  protected void before() throws Throwable {
    inputStream = getClass().getResourceAsStream(TEST_RESOURCE_NAME);
  };

  …
}

Timeout Rule

TODO

Error Collector Rule

TODO

Review

Summary

TODO provide life cycle summary diagram

Gotchas

In the Real World

Think About It

Self Evaluation

Task

When you created your FilePublicationRepository class in a previous lesson, you realized that you could not test the repository without there being a designated directory containing a signature file. You likely created such a directory on your own computer for ad-hoc testing, but this assumption cannot be placed in a unit test because unit tests can be run on the machines of other developers. Unit tests must not assume that the file system has been put in a certain state before the tests are run.

But now you have the tools to create a temporary directory on the fly for your tests. Implement unit tests for FilePublicationRepository.initialize() that uses the JUnit life cycle to create a temporary directory for testing.

Before you start creating your tests, take some time to clean up your package structure. Your Booker application is getting cluttered with all sorts of files in the same package. Separate out the files by their meaning and purpose. Here are some examples, but use your own judgment to create a well-organized arrangement.

See Also

References