Unit Test Life Cycle
Goal
- Make unit tests more powerful and efficient by taking advantage of the JUnit life cycle.
- Learn about JUnit rules and how to use them.
Concepts
- class loader
- life cycle
- rule (JUnit)
- setup
- teardown
Library
com.globalmentor.java.Classes.resolveResourcePath(Class<?> contextClass, String resourceName)
java.lang.Class
java.lang.Class.getResourceAsStream(String name)
java.lang.ClassLoader
java.lang.ClassLoader.getResourceAsStream(String name)
java.io.File
java.io.File.toPath()
java.nio.file.Path
org.junit.After
org.junit.AfterClass
org.junit.Before
org.junit.BeforeClass
org.junit.Rule
org.junit.rules.ExternalResource
org.junit.rules.ExternalResource.after()
org.junit.rules.ExternalResource.before()
org.junit.rules.TemporaryFolder
org.junit.rules.TemporaryFolder.getRoot()
org.junit.rules.TemporaryFolder.newFile()
org.junit.rules.TemporaryFolder.newFile(String fileName)
org.junit.rules.TemporaryFolder.newFolder()
org.junit.rules.TemporaryFolder.newFolder(String folder)
org.junit.rules.TestRule
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:
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.
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.
@Before
Class 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.
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.
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.
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.
Timeout Rule
TODO
Error Collector Rule
TODO
Review
Summary
TODO provide life cycle summary diagram
Gotchas
- TODO
In the Real World
- TODO
Think About It
- TODO
Self Evaluation
- TODO
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.
- Create a test that uses a temporary directory for the repository.
FilePublicationRepository.initialize()
must detect the absence of a signature file. - Create a test that uses a temporary directory for the repository, and then creates a signature file for testing.
FilePublicationRepository.initialize()
must in this case correctly detect the absence of a signature file. - Make sure your logic for testing a signature file (given its
Path
) is contained in an independent method so that it can be tested separately. Create a unit test for testing just the signature file. You'll need to place a signature file in a temporary directory beforehand, as you did above. - You should already have a unit test for testing a method for checking the signature bytes via an
InputStream
. To illustrate that you understand how to access class resources at runtime, place the expected signature bytes in asignature.dat
test resource in your project, and in your unit test get an input stream to this resource to pass to your signature checking method.
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.
- You could put all your publication definitions in a
pub
or apublication
or amodel
subpackage. - If you have created any I/O utilities methods, they might go in an
IO
utilities class in autil
subpackage. - Your repository classes could probably go in a
repo
or arepository
subpackage. It would probably be a good idea to separate the interfaces from the implementations; the interfaces could go in therepo
subpackage, and the implementations could be placed inrepo.snapshot
andrepo.file
subpackages. Don't forget to place your repository unit tests in the same packages.