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

Create extension point for ArgumentConverter #853

Open
nipafx opened this issue May 16, 2017 · 16 comments · May be fixed by #4219
Open

Create extension point for ArgumentConverter #853

nipafx opened this issue May 16, 2017 · 16 comments · May be fixed by #4219

Comments

@nipafx
Copy link
Contributor

nipafx commented May 16, 2017

As it stands argument converters need to be applied (with @ConvertWith) either to a custom annotation (at least it looks like that; couldn't make it work) or to the argument. When the former is not an option (for example because you can't make it work 😉), the latter quickly becomes repetitive.

An extension point that allows registering argument converters as the class and method level (much like parameter resolvers) would remedy this problem.

Beyond ease of use, this extension point would also enable creating more wholesome extensions which, applied to a test class, can register parameter resolvers, argument converters, post instance extensions, and more all in one annotation. It would also make it possible to do something like @ConvertByCalling("fromString") (either on class, method, or parameter), which would try to call a static method fromString(String) on the target type.

If you think this is a valuable feature, I'd like to take a shot at implementing it.

@marcphilipp
Copy link
Member

Please take a look at JavaTimeConversionPattern and JavaTimeArgumentConverter for an example of a custom annotation. It should really not be that hard to make it work. 😉

I'm not sure it makes sense to register ArgumentsSources on the class level since it would require all methods to have take the same parameter types.

However, I think registering additional implicit argument converters makes sense. Can you please provide an API proposal before jumping into the actual coding?

@marcphilipp marcphilipp added this to the 5.1 Backlog milestone May 18, 2017
@nipafx
Copy link
Contributor Author

nipafx commented May 21, 2017

Thanks for the hint, that worked fine. I was so stuck in the extension model thinking that I applied the custom annotation to the method, not the argument. But my point still stands, then: I have to apply that annotation to every single parameter that wants to use that converter.

I'm not sure it makes sense to register ArgumentsSources on the class level since it would require all methods to have take the same parameter types.

Unless I'm missing something that would only be the case if the ArgumentConverter API was left unchanged. I wanted to change it, though (see below).

However, I think registering additional implicit argument converters makes sense. Can you please provide an API proposal before jumping into the actual coding?

Sure but I'm going to focus on my initial idea (extension point for arbitrary converters) as opposed to your suggestion to look into additional implicit argument converters. I think the former can be a superset of the latter.

First of all, as soon as converters can be registered in several places, the question of priority and collisions arises. In the end this can be designed any way you want but I think the following order makes sense:

  • If @ConvertWith is (meta-)present on the parameter, use that converter. This should be unsurprising to any tester as the annotation is right there.
  • Otherwise look into converter extensions that were registered on the method or class level and give them an API that allows a similar technique to parameter resolvers (call supports, enforce unique jurisdiction).
  • Otherwise use default extensions that Jupiter provides.

As a consequence the API for converters would be twofold

  • the current ArgumentConverter can be used with @ConvertWith
  • a new ArgumentConverterExtension (or whatever) has the same convert method (maybe inherited?) but also a matching supports and can be applied with @ExtendWith

In both cases I would consider adding the ExtensionContext (I was surprised that at that point we can not be sure it's a TestExtensionContext) to the argument list to give extension more insight into their surroundings. Alternatively (and moving the needle into the other direction) supports might only get to see the parameter's type and must decide based on that.

Regarding the implementation, it looks like ParameterizedTestParameterResolver:57 is the place to inject the new "look for extensions" behavior.

@bobtiernay-okta
Copy link

bobtiernay-okta commented Nov 30, 2019

A great addition would be to support the registration of implicit (a.k.a default) converters project wide (similar to extensions) to cut down on test boilerplate. Some examples of common beneficial customizations include Jackson JsonNode, array types, List, Map, as well as custom objects provided through String-based factories not declared on the implementing class.

@sbrannen
Copy link
Member

Tentatively slated for 5.6 M2 solely for the purpose of team discussion

@marcphilipp
Copy link
Member

Team decision: Waiting for additional interest from the community.

@stale
Copy link

stale bot commented May 13, 2021

This issue has been automatically marked as stale because it has not had recent activity. Given the limited bandwidth of the team, it will be automatically closed if no further activity occurs. Thank you for your contribution.

@stale stale bot added the status: stale label May 13, 2021
@stale
Copy link

stale bot commented Jun 3, 2021

This issue has been automatically closed due to inactivity. If you have a good use case for this feature, please feel free to reopen the issue.

@bjmi
Copy link
Contributor

bjmi commented Apr 4, 2024

I'm also interested in a solution to register certain ArgumentConverters project wide.
What occurs very often is

  • Class to Instance conversion via default constructor e.g.
@ValueSource(classes = { ProductRequest.class, ArticleRequest.class, StockRequest.class, PriceRequest.class })
@ParameterizedTest
void sendAndReceive(@ToInstance Request<Payload> request) { ... }
  • Parsing dates with custom DateTimeFormatters
@CsvSource({ "2021-11-22 11:31",
             "2024-12-24 00:00" ...)
@ParameterizedTest
void deliveryDay(@Timing Instant deliveryDay) { ... }
  • Support of built-in Kotlin types e.g.
@ValueSource(classes = [ Int::class, Long::class, Double::class ])
@ParameterizedTest
fun withNumbers(type: KClass<out Number>) { ... }

It would be great if repetitive argument converters could be used implicitly without polluting the code with annotations.

@scordio
Copy link
Contributor

scordio commented Nov 30, 2024

@marcphilipp while reworking #3410, I wondered if the extension point mentioned in this issue could be a better way forward for what I reported at #3141, especially considering the reasoning at #3141 (comment).

If an argument converter extension point were available, I could globally register a custom argument converter for BCP 47 values without changing JUnit's internals or exposing a new configuration property.

The only disadvantage is that each JUnit user affected by #3141 would need to implement their argument converter, but I consider this straightforward.

What would be your preference? Should I invest in #3410, or would it be better to develop a proposal for this issue? @nipafx was willing to contribute some years ago, but I'm also happy to look into it if he is no longer available.

@marcphilipp
Copy link
Member

@scordio Thanks for bringing this up! I agree that there's an overlap here and it might be better to work on a more general solution than one specific for #3141. Have you already thought about what the extension interface would look like?

@scordio
Copy link
Contributor

scordio commented Dec 12, 2024

Instead of introducing a new interface, I was thinking how to adapt ArgumentConverter so that existing converters could be reusable for this purpose.

What if there was a new default method in ArgumentConverter? Something like:

default boolean canConvert(Object source, ParameterContext context) {
  return false;
}

The method might be overridden down the hierarchy for better user experience:

Image

For example, I imagine TypedArgumentConverter could be prefilled using the existing sourceType and targetType fields.

Then, canConvert should be invoked during the parameters resolving phase to identify the proper converter, maybe in ParameterizedTestParameterResolver as @nipafx suggested, or in ParameterizedTestMethodContext.Converter where the DefaultArgumentConverter instance is used – I need to study the flow a bit better 🙂

Would such a direction make sense?

@marcphilipp
Copy link
Member

marcphilipp commented Dec 12, 2024

Instead of introducing a new interface, I was thinking how to adapt ArgumentConverter so that existing converters could be reusable for this purpose.

What if there was a new default method in ArgumentConverter? Something like:

default boolean canConvert(Object source, ParameterContext context) {
  return false;
}

Wouldn't returning false disable all existing ArgumentConverters in user or third-party code?

We could introduce the method in a subinterface, though, e.g. ArgumentConverterService. Implementations would be registered via ServiceLoader. We would then also introduce a TypedArgumentConverterService<S, T> base class. WDYT?

@scordio
Copy link
Contributor

scordio commented Dec 12, 2024

Wouldn't returning false disable all existing ArgumentConverters in user or third-party code?

My rough idea was to return false to avoid impacting the existing converters with unexpected behavior. Like, if you want your existing converter to be compatible with the global conversion, you need to have an explicit change.

As I don't have it clear yet where to wire the lookup via ServiceLoader, you might just be right 😄

What I didn't mention in my previous comment is that canConvert would be invoked only for converters loaded by the ServiceLoader and wouldn't be evaluated for the ones wired via @ConvertWith, as the user already expresses the desire to use the converter at that place via the annotation.

I am still wondering if my direction is "too smart" and if it would be better to have the ArgumentConverterService subinterface, as you proposed. Specifically, out of the hierarchy from above, I have the impression neither AnnotationBasedArgumentConverter nor DefaultArgumentConverter should be relevant for registration (the latter even being an internal type).

Should I sketch a solution based on the ArgumentConverterService?

@marcphilipp
Copy link
Member

Should I sketch a solution based on the ArgumentConverterService?

I think that would be a good next step. 👍

@marcphilipp
Copy link
Member

We talked about this issue in our recent team call. Since we introduced ConversionSupport in junit-platform-commons in 5.11 to make the machinery to convert from String to other objects available to third-party extensions and test engines, we should also consider this issue in that broader context. Instead of introducing a ArgumentConverterService in junit-jupiter-params, we should consider introducing a more generic ConversionService in junit-platform-commons. The exact API would require some thinking, though, as it shouldn't just be applicable to parameters and we can't use types such as ParameterContext or AnnotatedElementContext from junit-jupiter-api there.

Probably something along these lines:

public interface ConversionService {

	boolean canConvert(Object source, Class<?> targetType, ConversionContext context);

    Object convert(Object source, Class<?> targetType, ConversionContext context);

	interface ConversionContext {

		Optional<AnnotatedElement> getAnnotatedElement();

		ClassLoader getClassLoader();

	}
}

In addition, we'd introduce a new method in ConversionSupport (and deprecate the old one?):

public static <T> T convert(Object source, Class<T> targetType, ClassLoader classLoader) {
	// ...
}

It would first check if there's a ServiceLoader-registered ConversionService that can convert the object, and, if not and source is a String, fall back to the existing logic.

@scordio A draft PR for a solution based on the ArgumentConverterService would still be welcome, if you've already started on that. If nothing else, it would allow us to get a better grasp on the relative complexity of the more generic solution I've proposed above.

@scordio
Copy link
Contributor

scordio commented Dec 17, 2024

Thanks for the update, @marcphilipp! I got sidetracked a bit but I'll raise something by the end of the week.

@scordio scordio linked a pull request Dec 23, 2024 that will close this issue
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment