junit5 icon indicating copy to clipboard operation
junit5 copied to clipboard

`@EmptySource` not working with `ArgumentConverter` accepting its supported types

Open scordio opened this issue 5 months ago • 4 comments

Currently, EmptyArgumentsProvider directly checks the type of the first declared parameter and does not know of a potential converter declared for that parameter with its supported type(s), so it will throw a PreconditionViolationException if the declared type doesn't match the expectations.

Looking ahead to when #4219 is integrated, a globally-configured converter might even make the situation more complicated.

Steps to reproduce

Given an ArgumentConverter converting objects to their respective hash codes, like:

class ObjectToHashCodeConverter implements ArgumentConverter {

    @Override
    public Object convert(Object source, ParameterContext context) {
        return source.hashCode();
    }

}

the following test passes:

@ParameterizedTest
@FieldSource
void fieldSourceTest(@ConvertWith(ObjectToHashCodeConverter.class) int hashCode) {
    assertEquals(0, hashCode);
}

static List<?> fieldSourceTest = List.of("");

but the following test does not:

@ParameterizedTest
@EmptySource // no indication on which supported type to use
void emptySourceTest(@ConvertWith(ObjectToHashCodeConverter.class) int hashCode) {
    assertEquals(0, hashCode);
}

failing with:

org.junit.platform.commons.PreconditionViolationException: @EmptySource cannot provide an empty argument to method [void io.github.scordio.junit.converters.tests.MyTest.emptySourceTest(int)]: [int] is not a supported type.

	at [email protected]/org.junit.jupiter.params.provider.EmptyArgumentsProvider.provideArguments(EmptyArgumentsProvider.java:92)
	at [email protected]/org.junit.jupiter.params.ParameterizedInvocationContextProvider.arguments(ParameterizedInvocationContextProvider.java:79)
	at [email protected]/org.junit.jupiter.params.ParameterizedInvocationContextProvider.lambda$provideInvocationContexts$2(ParameterizedInvocationContextProvider.java:46)
	at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:273)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
	at [email protected]/org.junit.jupiter.engine.descriptor.TemplateExecutor.executeForProvider(TemplateExecutor.java:59)
	...

Similar reproducers can be composed for all other @EmptySource supported types, like Collection, Map, etc.

Context

  • Used versions (Jupiter/Vintage/Platform): 5.13.4
  • Build Tool/IDE: Maven/IntelliJ IDEA

Deliverables

  • [ ] @EmptySource provides an optional type attribute to select the desired type explicitly

scordio avatar Jul 29 '25 08:07 scordio

The more I think about it, the more I feel like a solution might require a level of complexity that doesn't justify the gain...

I mentioned as a deliverable:

@EmptySource provides the empty argument accepted by the available converter

What if the available converter accepts more than one type supported by @EmptySource, e.g., both Collection and Map? Which empty instance should @EmptySource provide?

scordio avatar Jul 31 '25 09:07 scordio

If StringToBytesConverter would just implement ArgumentConverter instead of TypedArgumentConverter, it could allow byte[] to pass through, right?

Another idea would be to add a type attribute to @EmptySource which it would use instead of inspecting the method parameter type:

@ParameterizedTest
@EmptySource(type = String.class)
void emptySourceTest(@ConvertWith(StringToBytesConverter.class) byte[] bytes) {
    assertEquals(0, bytes.length);
}

marcphilipp avatar Aug 13 '25 06:08 marcphilipp

If StringToBytesConverter would just implement ArgumentConverter instead of TypedArgumentConverter, it could allow byte[] to pass through, right?

First and foremost, I apologize for mixing up two things.

I mentioned:

Currently, EmptyArgumentsProvider [...] will throw a PreconditionViolationException if the declared type doesn't match the expectations.

but then I reported:

org.junit.jupiter.api.extension.ParameterResolutionException

which is an unfortunate coincidence of my simplified reproducer... the fact that @EmptySource supported the converter's target type was not intended 😅 and I haven't added any stack trace, which would have helped to spot my mistake...

Therefore, I updated the issue description with a more specific example and the missing stack trace.

Another idea would be to add a type attribute to @EmptySource which it would use instead of inspecting the method parameter type

Yes, that would help! I updated the deliverables accordingly.

If the change is agreed, I'll be happy to work on it.

scordio avatar Aug 14 '25 11:08 scordio

If the change is agreed, I'll be happy to work on it.

I've added it to our discussion backlog which is currently very long so I'm afraid it may be a while before we get back to you on that.

marcphilipp avatar Aug 14 '25 12:08 marcphilipp