Skip to content

Introduce extension API for expression language evaluation in display names #1154

Description

@DcortezMeleth

Overview

I have found that it would be useful if you could use argument fields as a part of parametrized test name. Right now, when using just {0} as described in docs, it results in injecting into a test name string in form of ClassName(fieldName1=fieldValue1, fieldName2=fieldValue2, ...). But that doesn't seen readable at all especially then you have a class with more than just few fields. Now I would like to be able to use some semantics as {0.name} or {0}.name to use just name field of class used as test method argument, so I can write test name as follows:

@BeerTest
class BeerConversionServiceTest {

    @Autowired
    private BeerConversionService beerConversionService;

    private static Stream<Arguments> createBeers() {
        return Stream.of(
                Arguments.of(new Beer(...), 25L),
                Arguments.of(new Beer(...), 50L)
        );
    }

    @DisplayName("Get alcohol content of a beer in mg")
    @ParameterizedTest(name = "Alcohol content of {0.name} should be equal to {1}")
    @MethodSource("createBeers")
    void getAlcInMg(Beer beer, long mg) {
        Assertions.assertEquals(beerConversionService.getAlcInMg(beer), mg);
    }

}

This would give users possibility to keep test names simple, yet still descriptive, as they could reference objects passed as arguments and use only parts of them which identifies test cases.

I didn't look into the source code so I cannot tell which approach would be easier to implement but I would guess that {0.name} seems more reasonable.

Deliverables

Activity

  1. marcphilipp commented on Nov 11, 2017

    @marcphilipp
    Member

    I agree that this would be useful. Implementation-wise it might be a rabbit hole, though. We should probably check for getters first. If there are none, we could try to find a field traversing the class hierarchy upward.

    @junit-team/junit-lambda Thoughts?

  2. added this to the milestone on Nov 11, 2017
  3. vikaskumar89 commented on Nov 17, 2017

    @vikaskumar89
    Contributor

    hi @marcphilipp may i please work on this?

  4. marcphilipp commented on Nov 17, 2017

    @marcphilipp
    Member

    Sure!

  5. sbrannen commented on Nov 17, 2017

    @sbrannen
    Member

    I agree that this would be useful. Implementation-wise it might be a rabbit hole, though. We should probably check for getters first. If there are none, we could try to find a field traversing the class hierarchy upward.

    @junit-team/junit-lambda Thoughts?

    1. accessor method: name()
    2. getter method: getName()
    3. field named name (local and then traversing the hierarchy)
  6. DcortezMeleth commented on Nov 17, 2017

    @DcortezMeleth
    Author
    1. accessor method: name()
    2. getter method: getName()
    3. field named name (local and then traversing the hierarchy)
    

    Seems like a good idea.

  7. sormuras commented on Nov 17, 2017

    @sormuras
    Member

    How about supporting {{ mustache }} notation? We could include this library here https://github.com/spullara/mustache.java

  8. marcphilipp commented on Nov 17, 2017

    @marcphilipp
    Member

    I was also wondering about that. If we do, it needs to be an extension, though. Moreover, I'm not sure we need everything mustache has to offer. 🤔

  9. DcortezMeleth commented on Nov 17, 2017

    @DcortezMeleth
    Author

    Mustache may be a nice addition but I agree with Mark that if it's going to be used it should be provided as some kind of extension because it will be used by rather small amount of users.

    But will we need anything apart of this simple usecase? If not, additional dependecy seems a bit unnecessary.

  10. marcphilipp commented on Nov 17, 2017

    @marcphilipp
    Member

    Do we want to support things like {0.manufacturer.owner.name}?

  11. sbrannen commented on Nov 18, 2017

    @sbrannen
    Member

    Based on the discussions here, I think it's already starting to get a bit out of hand.

    As soon as we support one level of expressions, people will want more, and then they'll want additional features that the JUnit Team should not be implementing (due to scope).

    So, in order to avoid feature creep, my proposal is to go back to the original plan (from the JUnit Lambda prototype phase) to add an extension for plugging in a custom expression language.

    See also:

    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.

  12. sbrannen commented on Nov 18, 2017

    @sbrannen
    Member

    For example...

    • With Mustache, it might look like "Owner: {0.manufacturer.owner.name}".
    • With SpEL (from Spring), it might look like "Owner: #{#0.manufacturer.owner.name}" or "Owner: #{#beer.manufacturer.owner.name}".
  13. 22 remaining items

  14. changed the title [-]Add support for using argument fields in parametrized test names[/-] [+]Introduce extension API for expression language evaluation in display names[/+] on Aug 11, 2020
  15. removed this from the milestone on Jun 19, 2021
  16. stale commented on Jun 19, 2022

    @stale
  17. jean-andre-gauthier commented on Dec 20, 2024

    @jean-andre-gauthier

    Hi @marcphilipp , as discussed over slack I'm currently working on preparing a spike PR for this. Please feel free to assign me the issue

  18. marcphilipp commented on Feb 7, 2025

    @marcphilipp
    Member

    Based on #4293 (review), @sbrannen and I did a bit of brainstorming today. Based on our discussion, here's a more concrete proposal to be discussed with the entire team.

    • Introduce a ServiceLoader-based ExpressionLanguage interface in junit-jupiter-api in junit-platform-commons
    • Add @ExpressionLanguage("mustache") annotation so it can be set per class or method
    • Add junit.jupiter.expression-language.default configuration parameter so it can be set globally
    • Expose via ExtensionContext.getExpressionLanguage(String id) : Optional<ExpressionLanguage> and ExtensionContext.getDefaultExpressionLanguage() : Optional<ExpressionLanguage>
    • In junit-jupiter-params, get the default expression language, parse the template, and evaluate the expression if the new nameExpr attribute is set
    • Handle case where expression language is set but @ParameterizedTest(name = ...) isn't configured to generate the same default name as before

    API

    package org.junit.platform.commons.expression;
    
    interface ExpressionLanguage {
    	String getId(); // "mustache", "spel", etc.
        Expression parse(String template);
    }
    interface Expression {
    	default void evaluateAndAppend(ExpressionContext context, Appendable appendable) {
    		appendable.append(evaluate(context));
    	}
    	String evaluate(ExpressionContext context);
    }
    interface ExpressionContext {
    	Object get(String key);
    }

    Example

    @ParameterizedTest(nameExpr = "{{ user.name }} - {{ user.email }} ({{ address.street }})")
    @MethodSource
    void test(User user, Address address) {
        // ...
    }
  19. marcphilipp commented on Feb 28, 2025

    @marcphilipp
    Member

    Team decision: Explore the above proposal and re-evaluate after seeing a prototype in action.

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

Metadata

Metadata

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions