Recipe testing
When developing new recipes, it's very important to test them to ensure that they not only make the expected changes but that they also don't make unnecessary changes.
To help you create tests that meet those standards, this guide will:
Prerequisites
This guide assumes that:
You've already set up your recipe development environment
You're familiar with creating Java refactoring recipes
Adding dependencies
Before you can go about writing tests, you'll want to add some dependencies to your project's build file so that you have all of the tools needed to create said tests:
While recipes should generally be compiled with Java 8 as the target for compatibility, tests can be compiled to target newer versions of the Java Runtime.
We recommend either using Java 17 or Kotlin so that you get access to multi-line strings in your tests.
This tutorial uses Java for tests, but which language you use is a matter of preference.
Sample recipe
With the dependencies set up, we now need a recipe that we can write tests for. For the sake of an example, let's assume we have the following recipe that ensures a class's package declaration is all lowercase:
Writing tests
Now that we have a recipe to test with, let's go over what every test class should have. At the very least, your tests should:
Implement the RewriteTest interface
Specify the recipe to test via a RecipeSpec
Define one or more source file assertions using the Fluent API provided by the interface
First, we'll provide an example of what these tests might look like. After that, we'll provide more context around a few key pieces.
Example tests
RewriteTest interface
As mentioned above, the first thing all tests need to do is implement the RewriteTest interface. This interface not only acts as the entry point to testing your recipe, but it also provides a Fluent API for expressing recipe and source file configuration.
In the above tests, we utilize two main pieces of this interface:
The
defaults
method to set up the environment needed for each test (via RecipeSpecs)The
rewriteRun
method to assert that the recipe has made the correct transformations on the code (via SourceSpecs)
RecipeSpec
Before you begin writing the core logic for your tests, you'll need to set up your environment. You can do this by utilizing the RecipeSpec class. This class serves a few main purposes:
It allows you to specify which recipe will be executed for any given test
It allows you to customize the environment in which the recipe runs (such as what parser(s) to use and whether or not you should log Java compilation errors)
It provides convenient callbacks that can be used to execute code before or after any given test
The RewriteTest.defaults()
method can be used to define common RecipeSpec
customizations that should be applied to all of the tests in the testing class. Additionally, there are overloaded versions of the RewriteTest.runRecipe()
method that allow the RecipeSpec
to be further customized for a specific test. For instance, you may want different tests to have different Spring properties or different Java versions.
SourceSpec
Once you've set up your environment, the next step is to write the tests themselves. The core component of each test is the SourceSpec class.
At a minimum, a SourceSpec
will define the type of source file (such as java
or text
) and its initial "before" contents. If a test is defined with only these pieces, then the testing infrastructure will pass a test if the specified code has not changed at all after the given recipe has run.
In a majority of cases, though, a SourceSpec
will also define an "after" state which defines what the source file contents will look like after it has been processed by the given recipe. In this case, the testing framework will pass the test if the source file has been transformed into the "after" state.
A developer can assert additional conditions on a source file by using the afterRecipe
callback that is defined on the SourceSpec
. This can be convenient when asserting conditions on the resulting semantic model that are not represented in the rendering of the source code after the recipe has transformed the source file. For instance, you may want to confirm that the path to a file has changed such as in the sample tests above. This file path is not visible in the "after" code and can only be tested via this callback.
Advanced recipe testing
Customizing source file paths and markers
Occasionally, it is desirable to modify a specific source file before it is processed by the recipe testing environment. This type of customization can be achieved by using the callback method provided as an optional parameter on the Fluent API.
As an example, let us assume a recipe has been built to manipulate properties within an application.properties
source file, but only when its path is main/resources/application.properties
. To correctly define the path for the source file, a developer can leverage the callback:
You'll need to add org.openrewrite:rewrite-properties
as a test dependency for properties
to be available in your tests.
Specifying Java versions
When writing recipes that change the version of Java (such as a migration from 11 to 17), you may want to have tests that run on a different version of Java. To do this, you can use the RecipeSpec.allSources()
method to configure a default Java version for the sources:
You can also specify the version on an individual test basis like so:
Specifying the classpath for dependencies
If your tests include code that is not part of the JDK itself, you will want to add a classpath
or classpathFromResources
to the test so that OpenRewrite can know where those dependencies are coming from.
For instance, if you want to test that your recipe removes a junit-jupiter-api
method, you would need to let the parser know that there is a dependency on that library such as in the RemoveUnneededAssertionTest.
If you want to test multiple versions of the same dependency, you'll want to use classpathFromResources
instead which you can find documentation for in the using multiple versions of a library guide.
Next steps
Now that you're familiar with writing tests, consider reading over the best practice guide for making recipes. You could also check out the guide that expands on JavaTemplates if you'd like to learn even more about creating recipes.
Last updated