junit5
junit5 copied to clipboard
Introduce generic `@FileSource` for `@ParameterizedTest`
Similar to the existing @CsvFileSource used for @ParameterizedTest I would be looking to get access to the raw content of a file, or collection of files in a directory (based on file filters, etc) from one or more classpath resources via a proposed annotation like @FileSource.
This would allow a user to also apply custom converters to the retrieved files to convert them to domain specific models if needed.
To elaborate, suppose you have the following files in src/test/resources:
01.json containing JSON content:
{
"name": "John Doe",
"age": 30
}
02.yml containing YAML content:
name: John Doe
age: 30
If I just want to test based on the raw strings of the file based inputs, I would do something like the following:
public class ParameterizedFileSource {
@ParameterizedTest
// or also @FileSource(directory = "/") to read all files in directory
@FileSource(files = ["01.json", "02.yml"])
public void test(List<File> files) {
assertThat(files.size()).isEqualTo(2); // etc...
}
}
However if I had some class Person for which the file represents, and some deserialization logic for it, I would like to have direct access to those objects as inputs to the tests:
public class ObjectFixtureTests {
@ParameterizedTest
@FileSource(directory = "/")
public void test(@ConvertWith(PersonConverter.class) Person person) {
assertThat(person.name()).isEqualTo("John Doe"); //etc
}
}
Deliverables
- [ ] Functionality for new
@FileSourceannotation (with similar context to@CsvFileSource) - [ ] Documentation on usage for new
@FileSourceannotation, with examples with/without converters - [ ] Tests for the
@FileSourceannotation (with/without using converters as well)
I'm not sure that the generic term "fixture" adds value to this concept.
It sounds more like a @FileSource or @PathSource that would simply supply File or Path arguments to the parameterized test method.
So, if the the scope were reduced to that I think it could possibly be a candidate for inclusion in Jupiter. Though, a project like JUnit Pioneer might be better suited.
Regarding your deserializer proposal, that's already supported via Custom Aggregators.
Tentatively slated for 5.5 M1 for team discussion
Thanks for the clarification, that all makes sense. Updated the issue title to hopefully more accurately reflect the end result, and I'll stay tuned until then.
I think @sbrannen meant "converters" instead of "aggregators".
@dotCipher Have you tried using an explicit converter?
I think @sbrannen meant "converters" instead of "aggregators".
Indeed, in this context one would need to convert from a string that represents a classpath resource location to a String/File/Path that represents the contents of that resource and then to a domain object created from the contents of the resource.
Oh yeah that might make a lot of sense actually. I haven't tried converters, but that might fit for my use cases.
Would it make sense to narrow scope of this issue just towards something like a generic @FileSource (like one abstraction higher to the format specific @CsvFileSource) to be composed with a @ParameterizedTest?
Then one could just use the converters like you guys mentioned to just map from the content of the file to the domain object, composing the three annotations for a single parameterized test.
@dotCipher I think that would makes sense, indeed.
@dotCipher Could you please update this issue description and title accordingly?
@dotCipher I think that would makes sense, indeed.
+1
Cool, updated the description and title of the issue to reflect this. Let me know if it's not in sync with what you guys were thinking. Appreciated!
I guess the only assumption I made was the actual input type of the parameterized value. I am not sure if we want to use File or not, since it would be expected that the input File would always exist, and we probably would want to minimize boilerplate and not force the user to read the File object themselves.
Maybe if we return a wrapped type of something like a ByteSource with an associated Path from where the file was read, that might be useful for files that are serialized in binary format, otherwise I would say returning the String contents of the file would work.
Either way, I think having some reference to the file's content, as well as the Path by which is was read from (to pivot tests on file names, extensions, in the cases where they aren't directly converted to a domain specific type) would be most useful.
I think we should simply use Path as the input type of the parameterized value and leave it at that.
Rationale: files can also be binary. Thus converting automatically to a string would be pointless in such use cases.
Based on my experience, it's typically better to follow the KISS principle in such situations.
I think we should support Path and File like we do for @TempDir.
I think we should support
PathandFilelike we do for@TempDir.
Are you suggesting that the ArgumentsProvider should introspect the formal method parameters for the parameterized test method to determine the type to provide?
Or are you saying the ArgumentsProvider should simply provide strings that represent the paths which can be automatically converted to a File or Path via StringToCommonJavaTypesConverter?
Hmm, good question. I was purely thinking about this from a user's perspective. Now that I think about it again, these are classpath resources so I don't think neither File nor Path would work in all cases, would they?
I think it depends on whether the files in the classpath are directly available in the file system, in a JAR, or remote.
If they are in the local file system, one should be able to use URL java.lang.Class.getResource(String) and create a File or Path from that.
Otherwise, one would have to use InputStream java.lang.Class.getResourceAsStream(String) like you did in CsvFileArgumentsProvider.
To be on the safe side, we should probably just stick with InputStream as the provided argument type and potentially introduce converters to String or List<String>.
Thoughts?
Yeah, that would definitely work for my case, and having the more generic solution via InputStream seems like it would be flexible enough to handle most cases.
Who'd be responsible for closing the InputStream?
Who'd be responsible for closing the
InputStream?
I think Jupiter should encourage (via documentation) the consumer of the stream to close it in order to handle any exceptions, but Jupiter would still be responsible for ensuring that the stream is closed after test execution -- or at least attempting to close the stream (in a try-catch block).
If we're not comfortable with creating an InputStream and providing that as an argument to parameterized test methods, we could opt for a more robust model (and naturally more involved model (in terms of maintenance, etc.)) along the lines of Spring's Resource abstraction.
https://github.com/verhas/yamaletdtest
may be this is something you are looking for
Probably ok to close this out @sbrannen / @marcphilipp , since it seems like you addressed it in #1996
@dotCipher Thanks! 👍
Admittedly I am commenting on a closed thread, but,
Was there a resolution to this?
Until the last comment in 2019 it seems like there is agreement that this is a nice idea,
Then in 2021 there is a comment saying it was addressed in #1996 (which proposed to add @YamlFileSource support) and that this issue should now be closed.
But, #1996 was closed as a duplicate of this one...
Is there something I'm missing? It seems they were both closed in support of the opposite, and there is still no @FileSource.
Hi @ncgisudo,
Good catch! #1996 in fact did not address this issue.
So I'm reopening this one.
The following is already supported today:
class ParameterizedFileSource {
@ParameterizedTest
@ValueSource(strings = {"01.json", "02.yml"})
public void test(Path file) {
assertNotNull(file);
}
@ParameterizedTest
@MethodSource("allFilesInDir")
public void test(@ConvertWith(PersonConverter.class) Person person) {
assertEquals("John Doe", person.name); //etc
}
public static Stream<?> allFilesInDir() throws IOException {
return Files.list(Paths.get("."));
}
static class PersonConverter extends TypedArgumentConverter<Path, Person> {
protected PersonConverter() {
super(Path.class, Person.class);
}
@Override
protected Person convert(Path source) throws ArgumentConversionException {
return new Person("John Doe");
}
}
static class Person {
final String name;
public Person(String name) {
this.name = name;
}
}
}
@dotCipher and others: isn't that sufficient?
Yea this was sufficient for my use cases
Yea this was sufficient for my use cases
Have you tried github/verhas/yamaledt?
Blog post on a related topic: https://maciejwalkowiak.com/blog/resource-as-string-junit-extension/