
Я проверил рецепт OpenRewrite: ошибки, которые я сделал, и как их исправить
27 июня 2025 г.В течение последних двух недель я пнул шины Openrewrite. Сначала я создалРецепт для перемещения исходных файлов KotlinСогласно официальным рекомендациям с установленным именем пакета. Затем я улучшилРецепт для автоматического вычисления корняПолем В обеих версиях я тщательно проверил рецепт. Однако мой подход к тестированию был неправильным. В этом посте я хочу описать свои ошибки и то, как я их исправил.
Наивный подход
Я первоначально подошел к тестированию рецепта очень наивно, если не сказать больше. Как объяснено вПервый постЯ использовал API с низким уровнем Openrewrite. Вот что я написал:
// Given
val parser = KotlinParser.builder().build() //1
val cu = parser.parse(
InMemoryExecutionContext(), //2
sourceCode
).findFirst() //3
.orElseThrow { IllegalStateException("Failed to parse Kotlin file") } //3
val originalPath = Paths.get(originalPath)
val modifiedCu = (cu as K.CompilationUnit).withSourcePath(originalPath) //4
// When
val recipe = FlattenStructure(configuredRootPackage)
val result = recipe.visitor.visit(modifiedCu, InMemoryExecutionContext()) //5
// Then
val expectedPath = Paths.get(expectedPath)
assertEquals(expectedPath, (result as SourceFile).sourcePath) //6
- Построить анализатор котлина
- Установить контекст выполнения; Мне пришлось выбрать, и в памяти была самой простой.
- Cowerplate для получения единого компиляционного блока из потока
- Бросить в
K.CompilationUnit
Потому что мы знаем лучше - Явно позвоните посетителю, чтобы посетить
- Утверждайте рецепт переместил файл
Вышеуказанное работает, но требует глубокого понимания того, как работает Openrewrite. У меня не было такого понимания, но это было достаточно хорошо. Это вернулось, чтобы укусить меня, когда я улучшил рецепт, чтобы вычислить корень.
Как объяснено в последнем посте, я перешел от обычного рецепта на сканирующий рецепт. Мне пришлось предоставить как минимум два исходных файла, чтобы проверить новые возможности. Я придумал следующее:
// When
val recipe = FlattenStructure()
val context = InMemoryExecutionContext()
val acc = AtomicReference//String?(null)
recipe.getScanner(acc).visit(modifiedCu1, context) //1
recipe.getScanner(acc).visit(modifiedCu2, context) //1
val result1 = recipe.getVisitor(acc).visit(modifiedCu1, context) //2
val result2 = recipe.getVisitor(acc).visit(modifiedCu2, context) //2
- Получите сканер и посетите исходные файлы, чтобы вычислить корень
- Посетите посетителя и посетите исходные файлы, чтобы переместить файл
Это сработало, но я признаю, что это было счастливое предположение. Более вовлеченные рецепты потребуют более глубокого знания о том, как работает OpenRewrite, с большими потенциальными ошибками. К счастью, Openrewrite предоставляет средства для поддержания кода тестирования на правильном уровне абстракции.
Номинальный подход
Номинальный подход включает в себя пару классов из коробки; Это требует новой зависимости. Я не делал этого раньше, так что сейчас хорошее время: давайте представим <abbr title = "Билл материала"> Bom </abbr>, чтобы выравнивать все зависимости Openrewrite:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-recipe-bom</artifactId>
<version>3.9.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Теперь можно добавить зависимость без версии, так как Maven решает ее от вышеуказанного HOD.
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-test</artifactId>
<scope>test</scope>
</dependency>
Это приносит пару новых занятий в проект:
В документации говорится, что ваш тестовый класс должен наследовать отRewriteTest
, который обеспечиваетrewriteRun
Полем Последний управляет рецептом, без необходимости знать о его внутренней работе,например, вышеуказанный контекст исполнения в памяти.
Это уровень абстракции, который мы хотим.Assertions
предлагает статические методы для утверждения. Openrewrite также советует использовать Assertion4j, который я полностью поддерживаю. Тем не менее, я не сделал этого, чтобы сравнить проще.
Мы можем переписать предыдущий фрагмент:
rewriteRun( //1
kotlin(sourceCode1) { spec -> //2-3-4
spec.path("src/main/kotlin/ch/frankel/blog/foo/Foo.kt") //5
spec.afterRecipe { //6
assertEquals( //7
Paths.get("src/main/kotlin/foo/Foo.kt"),
it.sourcePath
)
}
},
kotlin(sourceCode2) { spec -> //2-3-4
spec.path("src/main/kotlin/org/frankel/blog/bar/Bar.kt") //5
spec.afterRecipe { //6
assertEquals( //7
Paths.get("src/main/kotlin/bar/Bar.kt"),
it.sourcePath
)
}
},
)
- Запустите рецепт
kotlin
преобразовать строку вSourceSpecs
- Я использую котлин, но
java
То же самое для регулярных проектов Java - Разрешить настройку спецификации источника
- Настройте путь
- Крючок после запуска рецепта
- Утверждают, что рецепт обновил путь в соответствии с ожиданиями
Возможно, вы заметили, что переписанный код не указывает, какой рецепт он тестирует. Это ответственностьRewriteTest.defaults()
метод
class FlattenStructureComputeRootPackageTest : RewriteTest {
override fun defaults(spec: RecipeSpec) {
spec.recipe(FlattenStructure())
}
// Rest of the class
}
Не забывайте циклы
Если вы следовали приведенным выше инструкциям, есть высокая вероятность, что ваш тест не пройдет с этим сообщением об ошибке:
java.lang.AssertionError: Expected recipe to complete in 0 cycle, but took 1 cycle. This usually indicates the recipe is making changes after it should have stabilized.
Нам нужно обратиться к документации, чтобы понять это загадочное сообщение:
Рецепты в трубопроводе выполнения могут привести к изменениям, которые, в свою очередь, приводят к дальнейшей работе еще один рецепт. В результате трубопровод может выполнять несколько проходов (или циклов) по всем рецептам в трубопроводе, пока ни в проме, ни в промежутке не будут внесены никаких изменений, либо не будет достигнуто максимальное количество проходов (по умолчанию 3). Это позволяет рецептам реагировать на изменения, внесенные другими рецептами, которые выполняются после них в трубопроводе.
-Циклы исполнения
Поскольку рецепт не полагается ни на один другой, и от него не зависит другой рецепт, мы можем установить цикл на 1.
override fun defaults(spec: RecipeSpec) {
spec.recipe(FlattenStructure())
.cycles(1) //1
.expectedCyclesThatMakeChanges(1) //2
}
- Установите, сколько циклов должен работать рецепт
- Установить на 0, если рецепт не ожидается внести изменения
Критика
Мне нравится то, что приносят классы тестирования OpenWrite, но у меня есть две критики.
Прежде всего, почему Openrewrite утверждает количество циклов по умолчанию? Это укусило меня сзади без веской причины. Мне пришлось покопаться в документации и понять, как работает OpenRewrite, хотя API тестирования должен защищать пользователей от его внутренней работы. Я также не могу не задаться вопросом о значениях по умолчанию.
public class RecipeSpec {
@Nullable
Integer cycles;
int getCycles() {
return cycles == null ? 2 : cycles; //1
}
int getExpectedCyclesThatMakeChanges(int cycles) {
return expectedCyclesThatMakeChanges == null ? cycles - 1 : //2
expectedCyclesThatMakeChanges;
}
// Rest of the class body
}
- Почему два цикла по умолчанию? Разве в большинстве случаев не должно быть достаточно?
- Почему
cycles - 1
по умолчанию?
Моя вторая критика заключается в том, как предоставленные классы тестирования заставляют вас структурировать ваши тесты. Мне нравится структурировать их на три части:
- Дано: Опишите начальное состояние
- Когда: выполнить проверенный код
- Затем: утверждать, что окончательное состояние соответствует тому, что я ожидаю.
С абстракциями Openrewrite структура сильно отличается от вышеперечисленного.
Заключение
В этом посте я мигрировал свойдля этого случаяТестовый код, чтобы полагаться на предоставленные классы Openrewrite. Несмотря на то, что они не освобождаются от критики, они предлагают солидный слой абстракции и делают тесты более поддерживаемыми.
Полный исходный код для этого поста можно найти на GitHub:
https://github.com/ajavageek/flatten-kotlin-recipe?embedable=true
Идти дальше:
- Тестирование рецептов
- Циклы исполнения
- Как разрешить ожидаемое несоответствие цикла рецептов
Первоначально опубликовано вJava Geek22 июня 2025 года
Оригинал