Skip to content
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

Spike for expression language evaluation in display names #4293

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

jean-andre-gauthier
Copy link

Overview

  • Introduces a new @ExpressionLanguage annotation, that takes a Class<ExpressionLanguageAdapter> as parameter
  • Introduces a new ExpressionLanguageAdapter interface, that can be implemented to bind in a third-party expression language
  • Adds several tests in ExpressionLanguageMustacheTests, that demonstrate how @ExpressionLanguage / ExpressionLanguageAdapter can be used

I'm marking this as a draft PR, because it's not entirely clear yet how the API should look like. I'll address the items in the Definition of Done once we have more clarity on that.


I hereby agree to the terms of the JUnit Contributor License Agreement.


Definition of Done


@Override
public void append(ArgumentsContext argumentsContext, StringBuffer result) {
expressionLanguageAdapter.format(argumentsContext.arguments.get()[0], result);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct, but I'm unsure how we should name multiple arguments, so that they can be accessed by the EL?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should provide a Map<String, Object> using the method parameter names as keys. That would require compiling with javac's -parameters option. We already check for parameter.isNamePresent() in ParameterizedTestMethodContext.getParameterName(int), though, so we could populate the Map based on that and fail (?) if no parameter names are present?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the Map<String, Object>, that's also what I suggested in #1154 (comment).

In summary, I believe it would be better to design a new extension API that receives the String displayName and a Map<String,Object> expressionContextMap and then allow people to pick between Mustache, SpEL, etc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require compiling with javac's -parameters option. We already check for parameter.isNamePresent() in ParameterizedTestMethodContext.getParameterName(int), though, so we could populate the Map based on that and fail (?) if no parameter names are present?

I don't think I would throw an exception. Rather, I would document the behavior, pointing out that source code parameter names are available when compiled with -parameters and that otherwise the arguments are available via arg0, arg1, etc. (i.e., the default parameter names returned by Parameter.getName()).

Copy link
Member

@sbrannen sbrannen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've only taken a quick glance, but for starters...

This new feature must be applicable to JUnit Jupiter in general, not just for parameterized tests.

So the APIs need to reside in the junit-jupiter-api module.

Please revise your work to take that into account.

See also: #1154 (comment)

@@ -25,6 +25,7 @@ dependencies {
testImplementation(testFixtures(projects.junitJupiterEngine))
testImplementation(testFixtures(projects.junitPlatformLauncher))
testImplementation(testFixtures(projects.junitPlatformReporting))
implementation(libs.mustache)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
implementation(libs.mustache)
testImplementation(libs.mustache)

@@ -55,6 +55,7 @@ mavenSurefirePlugin = { module = "org.apache.maven.plugins:maven-surefire-plugin
memoryfilesystem = { module = "com.github.marschall:memoryfilesystem", version = "2.8.1" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
mustache = { module = "com.github.spullara.mustache.java:compiler", version = "0.9.10"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mustache = { module = "com.github.spullara.mustache.java:compiler", version = "0.9.10"}
mustache = { module = "com.github.spullara.mustache.java:compiler", version = "0.9.10" }

Comment on lines +122 to +130
try {
ExpressionLanguageAdapter adapterInstance = adapterClass.getDeclaredConstructor().newInstance();
adapterInstance.compile(segment);
return adapterInstance;
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
String message = "Failed to initialize expression language for parameterized test. "
+ "See nested exception for further details.";
throw new JUnitException(message, ex);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
try {
ExpressionLanguageAdapter adapterInstance = adapterClass.getDeclaredConstructor().newInstance();
adapterInstance.compile(segment);
return adapterInstance;
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
String message = "Failed to initialize expression language for parameterized test. "
+ "See nested exception for further details.";
throw new JUnitException(message, ex);
}
ExpressionLanguageAdapter adapterInstance = ReflectionSupport.newInstance(adapterClass);
adapterInstance.compile(segment);
return adapterInstance;


@Override
public void append(ArgumentsContext argumentsContext, StringBuffer result) {
expressionLanguageAdapter.format(argumentsContext.arguments.get()[0], result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should provide a Map<String, Object> using the method parameter names as keys. That would require compiling with javac's -parameters option. We already check for parameter.isNamePresent() in ParameterizedTestMethodContext.getParameterName(int), though, so we could populate the Map based on that and fail (?) if no parameter names are present?

Comment on lines +6 to +8
void compile(String template);

void format(Object scope, StringBuffer result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should split this into two to allow (not necessarily in this PR) registering a global default expression language that would support instantiating the ExpressionLanguageAdapter at most once and also provide a hook to clean up resources by extending AutoCloseable.

Suggested change
void compile(String template);
void format(Object scope, StringBuffer result);
Expression compile(String template);
interface Expression {
	void evaluate(Map<String, Object> context, Appendable result);
}

@marcphilipp
Copy link
Member

This new feature must be applicable to JUnit Jupiter in general, not just for parameterized tests.

@sbrannen Besides parameterized tests, what other use cases with "dynamic display names" did you have in mind? @RepeatedTest(name = ...)? Dynamic tests?

@sbrannen
Copy link
Member

sbrannen commented Feb 6, 2025

@sbrannen Besides parameterized tests, what other use cases with "dynamic display names" did you have in mind? @RepeatedTest(name = ...)? Dynamic tests?

There were discussions in #1154, #2452, and other places over the years where people expressed an interest in general-purpose expression language support.

I think at the very least that this should be usable with any @TestTemplate. Though, we may find this can be used in other scenarios as well.

And I fear that tying this to junit-jupiter-param would unnecessarily limit our options in this area.

@jean-andre-gauthier
Copy link
Author

Hi both, thanks very much for your reviews! I'll be off on holidays for the next two weeks, but will work on implementing #1154 (comment) after that (once it has been discussed with the team)

@kaipe
Copy link

kaipe commented Feb 9, 2025

@sbrannen Besides parameterized tests, what other use cases with "dynamic display names" did you have in mind? @RepeatedTest(name = ...)? Dynamic tests?

There were discussions in #1154, #2452, and other places over the years where people expressed an interest in general-purpose expression language support.

I think at the very least that this should be usable with any @TestTemplate. Though, we may find this can be used in other scenarios as well.

And I fear that tying this to junit-jupiter-param would unnecessarily limit our options in this area.

Here I like to remind about my opinion on the subject #4246 (comment) - as a way to not "limit our options in this area".

I.e. not tying it to junit-jupiter-param nor junit-jupiter-engine but offering it from junit-platform-engine and allow display-name manipulation in midair during test-execution.

It would allow JUnit Jupiter to expose an API for display-name manipulation as part of ExtensionContext, therewith allowing any kind of Jupiter extension to manipulate display-name with some expression language or any other mean available. Perhaps with a popup dialog, in which a tester could document how an exploratory test session caused a backend error?

There is a kind of implementation for it, because some level of this functionality is required by LazyParams.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Introduce extension API for expression language evaluation in display names
4 participants