junit5 icon indicating copy to clipboard operation
junit5 copied to clipboard

Introduce generic `@FileSource` for `@ParameterizedTest`

Open dotCipher opened this issue 6 years ago • 30 comments
trafficstars

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 @FileSource annotation (with similar context to @CsvFileSource)
  • [ ] Documentation on usage for new @FileSource annotation, with examples with/without converters
  • [ ] Tests for the @FileSource annotation (with/without using converters as well)

dotCipher avatar Feb 26 '19 21:02 dotCipher

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.

sbrannen avatar Feb 27 '19 12:02 sbrannen

Tentatively slated for 5.5 M1 for team discussion

sbrannen avatar Feb 27 '19 12:02 sbrannen

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.

dotCipher avatar Feb 27 '19 16:02 dotCipher

I think @sbrannen meant "converters" instead of "aggregators".

@dotCipher Have you tried using an explicit converter?

marcphilipp avatar Mar 08 '19 12:03 marcphilipp

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.

sbrannen avatar Mar 08 '19 12:03 sbrannen

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 avatar Mar 08 '19 20:03 dotCipher

@dotCipher I think that would makes sense, indeed.

marcphilipp avatar Mar 09 '19 10:03 marcphilipp

@dotCipher Could you please update this issue description and title accordingly?

marcphilipp avatar Mar 09 '19 10:03 marcphilipp

@dotCipher I think that would makes sense, indeed.

+1

sbrannen avatar Mar 09 '19 13:03 sbrannen

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!

dotCipher avatar Mar 10 '19 15:03 dotCipher

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.

dotCipher avatar Mar 10 '19 15:03 dotCipher

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.

sbrannen avatar Mar 10 '19 17:03 sbrannen

I think we should support Path and File like we do for @TempDir.

marcphilipp avatar Mar 10 '19 17:03 marcphilipp

I think we should support Path and File like 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?

sbrannen avatar Mar 11 '19 14:03 sbrannen

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?

marcphilipp avatar Mar 13 '19 08:03 marcphilipp

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?

sbrannen avatar Mar 13 '19 14:03 sbrannen

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.

dotCipher avatar Mar 13 '19 16:03 dotCipher

Who'd be responsible for closing the InputStream?

marcphilipp avatar Mar 14 '19 08:03 marcphilipp

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).

sbrannen avatar Mar 14 '19 12:03 sbrannen

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.

sbrannen avatar Mar 14 '19 12:03 sbrannen

https://github.com/verhas/yamaletdtest

may be this is something you are looking for

verhas avatar May 06 '21 18:05 verhas

Probably ok to close this out @sbrannen / @marcphilipp , since it seems like you addressed it in #1996

dotCipher avatar May 11 '21 16:05 dotCipher

@dotCipher Thanks! 👍

marcphilipp avatar May 13 '21 13:05 marcphilipp

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.

ncgisudo avatar Jan 19 '22 18:01 ncgisudo

Hi @ncgisudo,

Good catch! #1996 in fact did not address this issue.

So I'm reopening this one.

sbrannen avatar Jan 20 '22 14:01 sbrannen

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?

marcphilipp avatar Jan 28 '22 14:01 marcphilipp

Yea this was sufficient for my use cases

dotCipher avatar Jan 28 '22 21:01 dotCipher

Yea this was sufficient for my use cases

Have you tried github/verhas/yamaledt?

verhas avatar Jan 31 '22 09:01 verhas

Blog post on a related topic: https://maciejwalkowiak.com/blog/resource-as-string-junit-extension/

sbrannen avatar Aug 01 '22 12:08 sbrannen