-
Notifications
You must be signed in to change notification settings - Fork 72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate Hamcrest to JUnit 5 #343
base: main
Are you sure you want to change the base?
Conversation
Work in progress implementation of a recipe which migrates Hamcrest test matchers to JUnit5 test assertions. Signed-off-by: matus.matok <matus.matok@pantheon.tech>
src/main/java/org/openrewrite/java/testing/junit5/MigrateFromHamcrest.java
Outdated
Show resolved
Hide resolved
Added a prototype-y implementation of translation from hamcrest's equalTo to JUnit5's assertEquals. Should be easy to add more of the simple hamcrest matchers to this implementation. \TODO the import is not being added Signed-off-by: matus.matok <matus.matok@pantheon.tech>
case "equalTo": | ||
return "assertEquals"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than statically defining the various options here, we could also look at turning the hamcrestMatcher
and targetAssertion
into @Option
arguments to the recipe, such that we can configure this repeatedly from a declarative Yaml description file. We already do something similar for Mockito, except in thise case you'd be configuring this new recipe rather than ChangeMethodName
. Such an approach pull those replacements out of code and into configuration, which is typically easier to expand on, even locally, for our users.
case 0: | ||
return method.withTemplate(template, method.getCoordinates().replace(), firstArg); | ||
case 1: | ||
return method.withTemplate(template, method.getCoordinates().replace(), firstArg, matcherArgs.get(0)); | ||
case 2 : | ||
return method.withTemplate(template, method.getCoordinates().replace(), firstArg, matcherArgs.get(0), matcherArgs.get(1)); | ||
case 3 : | ||
return method.withTemplate(template, method.getCoordinates().replace(), firstArg, matcherArgs.get(0), matcherArgs.get(1), matcherArgs.get(2)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The last argument in withTemplate is actually a varargs. So you could reduce this when opting for a Object[] args
as seen here.
Lines 75 to 80 in f25ff0b
Object[] args; | |
if (mi.getArguments().size() == 2) { | |
args = new Object[]{s.getSelect(), s.getArguments().get(0), mi.getArguments().get(1)}; | |
sb.append(", #{any()}"); | |
} else { | |
args = new Object[]{s.getSelect(), s.getArguments().get(0)}; |
//to be replaced with a static map | ||
switch (translatedAssertion) { | ||
case "assertEquals": | ||
return "assertEquals(#{any(java.lang.Object)}, #{any(java.lang.Object)})"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the recipe will fail currently with any 3/4 method arguments, as the template hardcodes the expected number of arguments to two; Might be best to add tests for various amounts of arguments and work from there to repeatedly append , #{any(java.lang.Object)}
as needed, as also seen here.
Lines 74 to 82 in f25ff0b
sb.append("assertEquals(#{any(java.lang.Object)},#{any(java.lang.Object)}"); | |
Object[] args; | |
if (mi.getArguments().size() == 2) { | |
args = new Object[]{s.getSelect(), s.getArguments().get(0), mi.getArguments().get(1)}; | |
sb.append(", #{any()}"); | |
} else { | |
args = new Object[]{s.getSelect(), s.getArguments().get(0)}; | |
} | |
sb.append(")"); |
class Biscuit { | ||
String name; | ||
Biscuit(String name) { | ||
this.name = name; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might also want to verify our use of equalTo
with Strings and other primitives, just to be sure we don't inadvertently produce errors there.
spec | ||
.parser(JavaParser.fromJavaVersion() | ||
.classpathFromResources(new InMemoryExecutionContext(), "junit-jupiter-api-5.9", "hamcrest-2.2")) | ||
.recipe(new MigrateFromHamcrest()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do end up going for declarative yaml recipes for the various matcher-to-method replacements, then we will likely want to import yaml recipes and run those instead of this direct invocation. Here's an example.
rewrite-testing-frameworks/src/test/java/org/openrewrite/java/testing/mockito/AnyToNullableTest.java
Lines 36 to 39 in f25ff0b
.recipe(Environment.builder() | |
.scanRuntimeClasspath("org.openrewrite.java.testing.mockito") | |
.build() | |
.activateRecipes("org.openrewrite.java.testing.mockito.AnyToNullable")); |
Another iteration of the prototype. Adapted it to be based on the up-to-date main. Added a proposal of how similar simple matchers could be translated to junit assertion methods. Signed-off-by: matus.matok <matus.matok@pantheon.tech>
Hello, got back to this. Noticed that the code base updated a little so I ammended my prototype. The tests dont run however, I am getting a bunch of error messages and I have not figured out how they are related to my changes, so if anyone could take a look, that would be appreciated. |
Appreciate you getting back to this! I've asked @knutwannheden to have a look as I'm caught up in other work this week; from the failures it appears your recipe might not apply or make changes. Did you already try to step through with the debugger? |
Yes I did. Seemingly, the recipe instantiates alright, but during the run phase it fails and I don't understand why. Wanted to double check it so I inserted some console prints into the recipe and they are printed as expected when the test is run. I will add some more simple matcher-to-method cases while I wait for the review. |
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) { | ||
J.MethodInvocation mi = super.visitMethodInvocation(method, executionContext); | ||
MethodMatcher matcherAssertTrue = new MethodMatcher("org.hamcrest.MatchAssert assertThat(String, boolean)"); | ||
MethodMatcher matcherAssertMatcher = new MethodMatcher("org.hamcrest.MatcherAssert assertThat(*, org.hamcrest.Matcher)"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure why this matcher is failing, but you can work around it by for now adopting:
MethodMatcher matcherAssertMatcher = new MethodMatcher("org.hamcrest.MatcherAssert assertThat(*, org.hamcrest.Matcher)"); | |
MethodMatcher matcherAssertMatcher = new MethodMatcher("org.hamcrest.MatcherAssert assertThat(..)"); |
You might need to match the three different MethodMatcher assertThat methods in a specific order with this ..
variant last, but at least that will get you going.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seemingly has something to do with matcherMethod being called on primitives, for example equalTo(5)
.
template.apply(getCursor(), method.getCoordinates().replace(), mi.getArguments().get(0), strippedMatcher); | ||
maybeAddImport("org.junit.jupiter.api.Assertions", targetAssertion); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget to return the changed template! With this return a first of your tests now passes 🎉
template.apply(getCursor(), method.getCoordinates().replace(), mi.getArguments().get(0), strippedMatcher); | |
maybeAddImport("org.junit.jupiter.api.Assertions", targetAssertion); | |
maybeAddImport("org.junit.jupiter.api.Assertions", targetAssertion); | |
return template.apply(getCursor(), method.getCoordinates().replace(), mi.getArguments().get(0), strippedMatcher); |
Sorry about the delays in getting you feedback; It's been pretty busy with the new 8.0 release coming up. I've posted two suggestions to get your first tests passing; That should help to start iterating by adding options, configuring those from Yaml, and see how far we can get with covering different Hamcrest matchers. Hope that helps! |
Added translations for matchers closeTo, containsString, empty, emptyArray, emptyIterable, emptyCollectionOf, emptyIterableOf, endsWith. Tests need to be added for each matcher. Signed-off-by: matus.matok <matus.matok@pantheon.tech>
No problem with the delays. Here is some further progress. Some tests -- those that are supposed to add import of |
Thanks a lot for your continued effort on this! I've moved your classes such that they are more in line with the AssertJ recipes that we have a in parallel. What I like about your approach is that you're better able to handle nested Matchers, which is a bit of a struggle with the alternative implementation for AssertJ. What I'm wondering with your implementation is if it would make sense to introduce an enum with fields to hold the before, after and template method strings. For instance with some rough pseudo code here enum Replacements {
EQUALTO("equalTo", "assertEquals", "#{any(java.lang.Object)}, #{any(java.lang.Object)}",
matcher -> matcher.getArguments()),
// TODO More replacements
;
String before, after, template;
Function<J.MethodInvocation, List<Expression>> arguments;
Replacements(String before, String after, String template, Function<J.MethodInvocation, List<Expression>> arguments) {
this.before = before;
this.after = after;
this.template = template;
this.arguments = arguments;
}
} Not sure if that would work out for all cases, but it could help group together the logic that's now maintained in three distinct switch statements into a structure that's perhaps easier to read and expand. Would you feel there's any merit to exploring that option? It would maybe need some special handling up front for |
The enum would probably have to hold the entire template as well, but sometimes the template is diffrent if the context is negated by |
Obviously, this PR is not yet merged, but it has functionality sufficient for some cases on our project. Therefore I have an offtopic question. Can I setup the recipe for the project from my fork or does it have to be merged into upstream? |
I think that's a perfectly valid approach; I've split those out in the AssertJ migration as well, as they're quite special. The rewrite-testing-frameworks/src/main/resources/META-INF/rewrite/hamcrest.yml Lines 41 to 50 in 266c018
There we first remove any wrapping is before applying further recipes. You can likely use that exact same recipe to simplify the conversion.
Absolutely; you can install the library to your local Maven repository through |
Hello, attempted to apply the recipe to our project, but cannot make it work. First I used the command u provided
So I went to try it in our local project, and added the plugin as follows:
Then I simply ran |
This is very strange. It should instead print |
Negative. |
You can also just inspect the contents of your |
Okay. Found it |
I still don't understand why it would publish it to your local Maven repo using the version 0.1.0-SNAPSHOT. 2.1.0-SNAPSHOT would be the expected version. |
well, neither do I. I have not touched any config files while working on this PR. Only the recipes, so I do not know. Either way, assume that 0.1.0-SNAPSHOT is the correct version. What could be other factors causing my recipe not to work in our project? (mvn config as above, prints no errors on build) |
Hmm. If you run |
Hi @matusmatokpt ! Was wondering what the current status of the PR is to you; Were you able to apply the changes locally? Are you still aiming to see this through to a merge? We've made some process since with recipes that prepare a migration such as removing |
Hi. I have been swamped with different tasks. I want to eventually finish this in my free time, but I am not sure when I ll have chance to get back to this. Maybe next week. |
All good! No rush; I'll mark the PR as a draft and move it to in progress then, just so that it's clear when you'd want to start a new round of review.
Sure! Specifically these two I thought could be interesting to reuse to simplify your migration recipes. rewrite-testing-frameworks/src/main/java/org/openrewrite/java/testing/hamcrest/RemoveIsMatcher.java Lines 25 to 28 in 244aea7
rewrite-testing-frameworks/src/main/java/org/openrewrite/java/testing/hamcrest/FlattenAllOf.java Lines 34 to 37 in 244aea7
We have similar plans still for |
Looks neat. However, the the effort to migrate When it comes to this recipe, I will try to wire it up using enums as you suggested, or maybe I will create a nested class called for example RecipeCase, which will contain the necessary fields. Will see what works the best. Either way, I can probably remove the |
Thanks for your continued plans to work on this! In general I would say try to break the recipe up into smaller reusable components that we can then wire together in a declarative yaml file; that way it's easier to implement, reason about and maintain. |
Refactored HamcrestMatcherToJUnit5 recipe, so now the whole translation is stored in one place, not scatter amongst three methods. Given the existence of RemoveIsMatcher Recipe, this recipe relies that it will never encounter is() matcher. In similar fashion as RemoveIsMatcher, RemoveNotMatcher was added, which does exactly the same as RemoveIsMatcher, but it also stores the logical context for the nested matcher (so that it knows it was negated) in execution context. Matchers instanceOf and isA were difficult to handle within the HamcrestMatcherToJUnit5 recipe, therefore these cases were moved to a newly added HamcrestInstanceOfToJUnit5 recipe. Signed-off-by: matus.matok <matus.matok@pantheon.tech>
Forgot, added now Signed-off-by: matus.matok <matus.matok@pantheon.tech>
src/main/java/org/openrewrite/java/testing/hamcrest/HamcrestMatcherToJUnit5.java
Outdated
Show resolved
Hide resolved
src/main/java/org/openrewrite/java/testing/hamcrest/HamcrestMatcherToJUnit5.java
Outdated
Show resolved
Hide resolved
src/main/java/org/openrewrite/java/testing/hamcrest/HamcrestMatcherToJUnit5.java
Outdated
Show resolved
Hide resolved
src/main/java/org/openrewrite/java/testing/hamcrest/HamcrestMatcherToJUnit5.java
Outdated
Show resolved
Hide resolved
src/test/java/org/openrewrite/java/testing/hamcrest/HamcrestInstanceOfToJUnit5Test.java
Show resolved
Hide resolved
src/test/java/org/openrewrite/java/testing/hamcrest/RemoveNotMatcherTest.java
Show resolved
Hide resolved
src/test/java/org/openrewrite/java/testing/hamcrest/RemoveNotMatcherTest.java
Outdated
Show resolved
Hide resolved
src/test/java/org/openrewrite/java/testing/hamcrest/RemoveNotMatcherTest.java
Outdated
Show resolved
Hide resolved
src/test/java/org/openrewrite/java/testing/hamcrest/RemoveNotMatcherTest.java
Outdated
Show resolved
Hide resolved
src/test/java/org/openrewrite/java/testing/hamcrest/RemoveNotMatcherTest.java
Outdated
Show resolved
Hide resolved
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This is a draft pull request for #212