Clean Integration Testing with JUnit Rules
The benefits of JUnit Rules, especially when dealing with integration testing, can hardly be overrated. In this post, we’ll shine a light on the usefulness of ExternalResource
extensions. These simplify fixture control in cases where we have to work with external-resource-abstracting third-party libraries. As an example, we’ll have a look at how to verify the correct retrieval of a list of entries, based on Git commit log messages.
What is Integration Testing?
‘Separation of Concerns’ is probably the single most important concept in software design and implementation.
Pragmatic Unit Testing [HUTH03]
In general, we use unit tests to check whether a small piece of production code works as expected. But it’s important to understand that these kind of tests are restricted to code for which the developer is responsible. To clarify this point, consider the incorporation of third-party libraries to manage access to files, databases, webservices, and the like.
Tests would implicitly invoke code of third-party components, because our system under test (SUT) depends on these components (DOC) [MESZ07]. In case one of the external resources is not available, they would fail although there might be nothing wrong with the developer’s code. Furthermore, access to these resources is usually pretty slow, and setting up the test fixture gets often quite cumbersome. Let alone the fragility, which is caused by potential semantical changes of different library versions.
All these disadvantages suggests to separate the application’s code from third-party code by means of an adapter abstraction [FRPR10]. It’s not just that the abstracting adapter component can provide an expressive API in terms of the application’s problem domain, it also allows to replace the implementation based on the third-party code by a lightweight stand-in test double, often denoted as mock.
Testing with JUnit
Testing with JUnit is one of the most valuable skills a Java developer can learn. No matter what your specific background, whether you’re simply interested in building up a safety net to reduce regressions of your desktop application or in improving your server-side reliability based on robust and reusable components, unit testing is the way to go.
Frank has written a book that gives a profound entry point in the essentials of testing with JUnit and prepares you for test-related daily work challenges.
This eliminates the dependency problems listed previously with respect to unit testing. Test doubles are cheap to setup, isolate the system under test from third-party code, and keep tests fast and reliable [MESZ07]. However, it leaves us with the task of testing the proper behavior of the adapter component. This is when integration testing comes into play.
The term refers to the phase in software testing in which individual software modules are combined and tested as a group [INTTES]. It’s fair to say that we use adapter abstractions to group one or more third-party modules together to provide a certain functionality. Since such adapters are low level components from the application’s point of view, this strategy implicitly leads to a bottom up approach, where the lowest level components are tested first, and then might be used to facilitate the testing of the higher level ones.
You may wonder if it isn’t a bad thing to adjust a design for testing purposes. But, by using adapters, you determine a clear boundary between your application and the third-party code. In case a new library version introduces a slightly different behavior, you simply have to adjust your adapter code to make an according integration test pass again. Your actual application code, including the unit tests, will stay unaffected! In addition, you can switch to another vendor easily by providing an appropriate adapter. Hence, following this practice consequently leads also to a healthier application design out of the box. [APPE15]
Handling of External Resources
Unfortunately, when writing integration tests, we have to face the problems circumvented for unit tests by the usage of test doubles. From the coding angle in particular, setting up the test fixture often requires a considerable amount of effort. On top of this, we also have to take care of proper housekeeping [MESZ07]. This means we might need to reset the state of external resources after test execution, for example. The latter could be important to ensure that subsequent tests run in isolation. This way resource modifications done by a test cannot falsify the verification results of its succesors.
To reduce the recurring overhead of setup and teardown code, it seems natural to swap common passages into test helper classes. Think of the creation, deletion, or manipulation of system environment variables, master data records, and the like. JUnit rules are special test helpers which intercept test method calls similar as an AOP framework would do. Comparable to an around advice in AspectJ, they can do useful things before and/or after the actual test execution. It’s possible, for example, to register a REST service resource before a test run, and remove it automatically once it’s over.
JUnit provides a convenient base class ExternalResource
for rules that set up an external resource before a test (a file, socket, server, database connection, etc.), and guarantee to tear it down afterwards [EXRAPI]. The following listing ServerRule
shows the principle.
public class ServerRule extends ExternalResource { private final int port; public ServerRule( int port ) { this.port = port; } @Override protected void before() throws Throwable { System.out.println( "start server on port: " + port ); } @Override protected void after() { System.out.println( "stop server on port: " + port ); } }
The constructor of ServerRule
takes a port number for our fictive server type. To demonstrate the concept, we actually do not start a real one, but only print out a this number containing message on invocations of the before
and after
callback hooks. The next listing shows the usage of the ServerRule
.
public class MyServerITest { @Rule public final ServerRule serverRule = new ServerRule( 5050 ); @Test public void foo() { System.out.println( "code that fails without server access" ); } }
Please note, how the rule is registered by a public, non static field annotated with @Rule
. Running the test case leads to the following output.
start server on port: 5050 code that fails without server access stop server on port: 5050
As you can see, the rule ensures that the test code gets executed within the expected environmental prerequisite and automatically takes care of housekeeping. To deepen this topic, let’s have a look at a more detailed example, illustrating the interplay of a rule managed fixture and the component under test.
Designing a Rule for Git Integration Tests
The title image shows a timeline component, which retrieves its list of Item
s via a configurable ItemProvider
adapter. The adapter type used, while capturing the picture, reads the entries from a Git repository. Each item represents a commit of the current repository branch. The illustration is based on a screenshot of the sample app I developed for my book Testing with JUnit. Because it was out of the volume’s scope, I take this opportunity to deliver, belatedly, an explanation of the GitRule
helper I applied for writing the JGit integration tests.
The driving force is to provide a utility class, whose purpose is to ease the task of setting up a git fixture repository containing arbitrary commits, branches, and the like. To do so, I created a GitRepository
type. This handles the repository interactions on a local repository by means of JGit. The following excerpt should clarify the concept.
public class GitRepository { private final File location; GitRepository( File location ) { this.location = location; } public RevCommit commitFi1e( String fileName, String content, String message ) throws IOException { createFi1e( fileName, content ); addFi1es(); return commit( message ); } [...] }
As you can see, a GitRepository
instance takes a constructor parameter that refers to the working directory of a local Git repository. But note the visibility restriction of the constructor. This is because the abstraction is not responsible for handling the lifecycle of the repository resource. For the latter we use a ExternalResource
derivate as shown in the next listing.
public class GitRule extends ExternalResource { private final Set<File> repositories; public GitRule() { repositories = new HashSet<>(); } @Override protected void after() { repositories.forEach( repository -> delete( repository ) ); } public GitRepository create( File location ) { createRepositoryOnDisk( location ); GitRepository result = new GitRepository( location ); repositories.add( location); return result; } private void createRepositoryOnDisk( File location ) { InitCommand init = Git.init(); init.setDirectory( location ); init.setBare( false ); callInit( init ); } private static void callInit( InitCommand init ) { try { init.call().close(); } catch( GitAPIException exception ) { throw new GitOperationException( exception ); } } }
The GitRule
serves as factory for as many repository resources as you might need for a specific test. Furthermore, it tracks their location required for a proper disposal once test execution has been finished. The shown version creates only local repositories on disk, but this can be enhanced to clone remote ones too, of course.
The ItemProvider
interface relies on a generic type parameter that extend the type Item
. Thus, a GitItemProvider
type returns GitItem
instances as lookup results, and each git item is an encapsulation of a JGit RevCommit
. Said this, it should be clear that third-party code abstractions may affect more than a single class. The follwing snippet shows a simple integration test scenario. The GitRule
provides a repository applicable for the creation of a real commit. The latter serves to verify the correct instantiation of a GitItem
instance.
public class GitItemTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Rule public final GitRule gitRule = new GitRule(); @Test public void ofCommit() throws IOException { GitRepository repository = gitRule.create( temporaryFolder.newFolder() ); RevCommit commit = repository.commitFi1e( "file", "content", "message" ); GitItem actual = GitItem.ofCommit( commit ); assertThat( actual ) .hasId( getId( commit ) ) .hasTimeStamp( getTimeStamp( commit ) ) .hasContent( getContent( commit ) ) .hasAuthor( getAuthor( commit ) ); } [...] }
The test avails a TemporaryFolder
rule to ensure the repository gets created under an accessible directory. Actually, the usage of the temporary folder rule should make the resource removal of the GitRule
superfluous. But, since its default cleanup mechanism does not check whether resource deletions were successful (a hard check is only available with the latest JUnit version anyway), I opted for not relying on that. This is important, beccause with JGit one easily runs into open file handle issues.
Moreover, the test’s verifications are done by means of a custom tailored GitItemAssert
assertion class and a few utility methods (static imports). Having this in place, we are ready to have a look at a bit more complex scenario.
public class GitItemProviderITest { private static final String CLONE_NAME = "test"; private static final int INITIAL_COMMIT_COUNT = 6; @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Rule public final GitRule gitRule = new GitRule(); private GitRepository repository; private GitItemProvider provider; private File remoteLocation; private File destination; @Before public void setUp() throws IOException { remoteLocation = temporaryFolder.newFolder(); repository = createRepository( remoteLocation ); destination = temporaryFolder.newFolder(); provider = new GitItemProvider( remoteLocation.toURI().toString(), destination, CLONE_NAME ); } @Test public void fetchItems() throws IOException { int fetchCount = INITIAL_COMMIT_COUNT / 3; List<GitItem> actual = provider.fetchItems( null, fetchCount ); assertThat( actual ) .isEqualTo( subList( 0, fetchCount ) ) .hasSize( fetchCount ); } private List<GitItem> subList( int fromIndex, int toIndex ) { return repository .logAll() .stream() .map( commit -> ofCommit( commit ) ) .collect( toList() ) .subList( fromIndex, toIndex ); } [...] }
The setup is similar than in the previous test. However, our fixture repository is created by delegating to a createRepository
method. I ommit the details for brevity here, since the method only creates a repository with an amount of INITIAL_COMMIT_COUNT
commits. The GitItemProvider
component under test takes three constructor parameters. The first one is the location of the fixture repository which will be cloned by the provider. For this purpose the second parameter defines a destination directory, and the clone repository’s folder name gets injected by the third one.
During the exercise phase the component fetches a subset of the available commits from its cloned repository. This list is verified agains the expected one which gets calculated by the method subList
from our fixture repository. Finally, the rules take care of the housekeeping.
If you want to have a look at the complete example code, please refer to the sample app’s sources available at the GitHub repository https://github.com/fappel/Testing-with-JUnit.
Summary
This post gave an introduction on how JUnit rules can be used for clean resource management when writing integration tests. We have gained a basic understanding of what integration testing is, understood the working principle of ExternalResource
test utility extensions, and elaborated on a detailed usage example. Of course, there is more to it than first meets the eye. Once you’re familiar with the principles shown here, you might consider to delve into additional topics like working with ClassRule
s for persistent fixtures, rule chaining, environment variables, and so on.
It would be remiss not to tell you that chapter 6, Reducing Boilerplate with JUnit Rules, of my book Testing with JUnit is available as a free reading sample at https://www.packtpub.com/packtlib/book/Application%20Development/9781782166603/6. In case you’re not tired of my scribblings yet, boldly go ahead and take the opportunity to delve deeper into the world of JUnit rules…
So just remember, folks, always stick to the rules – and don’t forget to share the knowledge ��
Resources
- [APPE15]: Appel, Testing with JUnit, Packt Publishing, 2015
- [EXRAPI]: ExternalResource, API DOC, http://junit.org/apidocs/org/junit/rules/ExternalResource.html
- [FRPR10]: Freeman, Pryce, Growing Object-Oriented Software, Guited by Tests, Addison Wesley, 2010
- [HUTH03]: Hunt, Thomas, Pragmatic Unit Testing, LLC, 2003, 2004
- [INTTES]: Wikipedia, IntegrationTesting, https://en.wikipedia.org/wiki/Integration_testing
- [MESZ07]: Meszaros, xUnit Test Patterns, Pearson Education, Inc., 2007
Reference: | Clean Integration Testing with JUnit Rules from our JCG partner Frank Appel at the Code Affine blog. |