This skill should be used when writing or modifying tests for OpenRewrite recipes in the Liftwizard project, when the user asks to "write recipe tests", "add test coverage", "test OpenRewrite recipe", "fix failing recipe test", or discusses test structure patterns for OpenRewrite. Provides guidance on test method naming, combining test scenarios, and coverage best practices.
Guidance for writing effective tests for OpenRewrite recipes in the Liftwizard project.
Every test class must have exactly 2 test methods:
Positive test method (with @DocumentExample):
replacePatternsjava(before, after) call with one before/after pairNegative test method:
doNotReplaceInvalidPatternsjava(code) call with one code exampleEach test method should have exactly one java() call.
❌ WRONG - Multiple java() pairs:
@Test
void replacePatterns() {
this.rewriteRun(
java(before1, after1),
java(before2, after2),
java(before3, after3)
);
}
✅ CORRECT - Single java() with one full example:
@Test
void replacePatterns() {
this.rewriteRun(
java(
"""
// one example containing all test scenarios
""",
"""
// one after example showing all transformations
"""
)
);
}
Use identical method names across all test classes:
replacePatterns - Positive test (always with @DocumentExample)doNotReplaceInvalidPatterns - Negative testAvoid these patterns:
testReplaceNullSafeEquals (includes "test" prefix)replaceAssertThrowsTest (includes "test" suffix)replaceNullSafeEqualsPatterns (too specific to recipe)replaceHashMapConstructorVariations (too specific to recipe)Consistency over specificity - all test classes use the same two method names.
Combine ALL related scenarios into one code example.
Instead of separate java() calls or methods:
// WRONG - separate methods
@Test void replaceWithMessage() { ... }
@Test void replaceWithoutMessage() { ... }
// ALSO WRONG - multiple java() pairs
this.rewriteRun(
java(withMessage, withMessageExpected),
java(withoutMessage, withoutMessageExpected)
);
Combine into one example:
@Test
@DocumentExample
void replacePatterns() {
this.rewriteRun(
java(
"""
import ...;
class Test {
// All variations in one class:
void withMessage() { Verify.assertSize("msg", 1, list); }
void withoutMessage() { Verify.assertSize(1, list); }
void withZero() { Verify.assertSize(0, list); }
void withVariable() { Verify.assertSize(n, list); }
}
""",
"""
import ...;
class Test {
// All expected outputs in one class:
void withMessage() { assertThat(list).as("msg").hasSize(1); }
void withoutMessage() { assertThat(list).hasSize(1); }
void withZero() { assertThat(list).isEmpty(); }
void withVariable() { assertThat(list).hasSize(n); }
}
"""
)
);
}
What to include in the single example:
class RecipeNameTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec
.recipe(new RecipeName())
.parser(JavaParser.fromJavaVersion());
}
@Test
@DocumentExample
void replacePatterns() {
this.rewriteRun(
java(
"""
import org.example.OldApi;
import java.util.Map;
class Test<T> {
// Field declarations
private final List<String> fieldEmpty = new OldList<>();
private final List<Integer> fieldCapacity = new OldList<>(10);
void testMethod() {
// Local variables - various generic forms
List<String> diamond = new OldList<>();
List rawType = new OldList();
List<Map<String, Integer>> nested = new OldList<>();
List<? extends Number> wildcard = new OldList<>();
// Edge cases
List<String> explicit = new OldList<String>();
}
// Return statement context
List<T> factory() {
return new OldList<>();
}
}
""",
"""
import org.example.NewApi;
import java.util.Map;
class Test<T> {
// Field declarations
private final List<String> fieldEmpty = NewApi.empty();
private final List<Integer> fieldCapacity = NewApi.withCapacity(10);
void testMethod() {
// Local variables - various generic forms
List<String> diamond = NewApi.empty();
List rawType = NewApi.empty();
List<Map<String, Integer>> nested = NewApi.empty();
List<? extends Number> wildcard = NewApi.empty();
// Edge cases
List<String> explicit = NewApi.<String>empty();
}
// Return statement context
List<T> factory() {
return NewApi.empty();
}
}
"""
)
);
}
@Test
void doNotReplaceInvalidPatterns() {
this.rewriteRun(
java(
"""
import org.example.OldList;
import java.util.Collections;
class Test {
// Concrete type - should NOT transform
private final OldList<String> concreteField = new OldList<>();
void testMethod() {
OldList<String> concreteLocal = new OldList<>();
}
// FieldAccess expressions - should not crash
private static final Object EMPTY = Collections.EMPTY_SET;
}
"""
)
);
}
}
Create one integration test class per YAML recipe file to ensure all recipes work together without interference:
.recipeFromResources() to load the YAML recipePrefer classpath() when possible:
.classpath("eclipse-collections-api") to JavaTemplate builders for real Eclipse Collections types.classpath("eclipse-collections") for implementation classes if neededUse dependsOn() when necessary:
Verify class stubs in AssertJ migration tests (eclipse-collections-testutils classpath doesn't work)Process:
.typeValidationOptions(TypeValidation.none()) lineCommon Fix: Add appropriate classpath() entries to JavaTemplate builders to provide real type information
Fundamental Limitations (cannot remove TypeValidation.none()):
.typeValidationOptions(TypeValidation.none())To refactor verbose tests into the preferred pattern:
java(before, after) callreplacePatternsdoNotReplaceInvalidPatterns with one java(code) call@DocumentExample to the combined positive methodSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer