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

Introduce DisplayNameGenerator to support CamelCase, underscores, and numbers #3569

Open
FanJups opened this issue Nov 21, 2023 · 2 comments

Comments

@FanJups
Copy link

FanJups commented Nov 21, 2023

While working on Jakarta Data , we noticed that some test display names match the same pattern. Then we figured out a way to simplify those display names by creating a custom display name generator as you can see on this Issue and PR.

We wanted to implement only on our side, we faced some issues while trying to make this generator available for all Jakarta Data modules. Those issues leaded to many discussions about this feature, its added value, its scope....

Thinking about this for a long time, we asked ourselves why only Jakarta Data ? Why not all Jakarta projects ? Even more, why not make it available for everybody by adding this custom generator to JUnit directly ? Given that, this pattern is often used, it will help more developers.

That's why I am opening this issue to share this generator with the world 😊 !

To get a better idea, I share with you the code of this custom generator.

/**
 * <p>A class extending {@linkplain DisplayNameGenerator.Standard }.</p>
 *
 * <p>This extension handles method names with CamelCase, underscore and numbers.</p>
 *
 * <p>The aim is to simplify unit test display names. Instead of using this method annotation {@linkplain org.junit.jupiter.api.DisplayName },
 * we can just use this class annotation {@linkplain org.junit.jupiter.api.DisplayNameGeneration } and use that method annotation if needed.
 * </p>
 *
 * <p>This generator follows 3 rules:</p>
 *
 * <ul>
 *     <li>Each uppercase letter is turned into its lowercase value prepended by space.</li>
 *     <li>Each underscore is turned into space. Words bounded by underscores or just starting with underscore are not transformed. Usually these words words represent classes, variables....</li>
 *     <li>Each number is prepended by space.</li>
 * </ul>
 * <p>
 * Usage example:
 *
 * <pre>
 *
 *     {@code
 *
 * @DisplayNameGeneration(ReplaceCamelCaseAndUnderscoreAndNumber.class)
 * class ExampleTest {
 *     @Test
 *     //Equivalent of @DisplayName("Should return error when maxResults is negative")
 *     void shouldReturnErrorWhen_maxResults_IsNegative() {
 *       ...
 *     }
 *     @Test
 *     //Equivalent of @DisplayName("Should create limit with range")
 *     void shouldCreateLimitWithRange() {
 *       ...
 *     }
 *
 *     @Test
 *     //Equivalent of @DisplayName("Should return 5 errors")
 *     void shouldReturn5Errors() {
 *       ...
 *     }
 *
 *     @ParameterizedTest
 *     @ValueSource(strings = {"job", "player"})
 *     //Equivalent of @DisplayName("Should return the value of maxResults (String)")
 *     void shouldReturnTheValueOf_maxResults(String input) {
 *       ...
 *     }
 *
 *     @Test
 *     //Equivalent of @DisplayName("Should return the number of errors as numberOfErrors inferior or equal to 15")
 *     void shouldReturnTheNumberOfErrorsAs_numberOfErrors_InferiorOrEqualTo15() {
 *       ...
 *     }
 * }
 *
 *     }
 * </pre>
 */

public class ReplaceCamelCaseAndUnderscoreAndNumber extends DisplayNameGenerator.Standard {
    public static final DisplayNameGenerator INSTANCE = new ReplaceCamelCaseAndUnderscoreAndNumber();

    @Override
    public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
        if (hasParameters(testMethod)) {
            return replaceCamelCaseAndUnderscoreAndNumber(testMethod.getName()) + " " + DisplayNameGenerator.parameterTypesAsString(testMethod);
        }
        return replaceCamelCaseAndUnderscoreAndNumber(testMethod.getName());
    }

    private String replaceCamelCaseAndUnderscoreAndNumber(String input) {
        StringBuilder result = new StringBuilder();
        /*
         * Each method name starts with "should" then the displayed name starts with "Should"
         * */
        result.append(Character.toUpperCase(input.charAt(0)));

        /*
         * There are 2 groups of method name: with and without underscore
         * */
        if (input.contains("_")) {
            boolean insideUnderscores = false;
            for (int i = 1; i < input.length(); i++) {
                char currentChar = input.charAt(i);
                if (currentChar == '_') {
                    result.append(' ');
                    /*
                     * If the current char is an underscore and insideUnderscores is true,
                     * it means there is an opening underscore and this one is the closing one
                     * then we set insideUnderscores to false.
                     * */
                    /*
                     * If the current char is an underscore and insideUnderscores is false,
                     * it means there is not an opening underscore and this one is the opening one
                     * then we set insideUnderscores to true.
                     * */
                    insideUnderscores = !insideUnderscores;
                } else {
                    /*
                     * If the character is inside underscores, we append the character as it is.
                     * */
                    if (insideUnderscores) {
                        result.append(currentChar);
                    } else {
                        //CamelCase handling for method name containing "_"
                        if (Character.isUpperCase(currentChar)) {
                            //We already replace "_" with " ". If the previous character is "_", we will not add extra space
                            if (!(input.charAt(i - 1) == '_')) {
                                result.append(' ');
                            }
                            result.append(Character.toLowerCase(currentChar));
                        } else {
                            result.append(currentChar);
                        }
                    }
                }
            }
        } else {
            //CamelCase handling for method name not containing "_"
            for (int i = 1; i < input.length(); i++) {
                if (Character.isUpperCase(input.charAt(i))) {
                    result.append(' ');
                    result.append(Character.toLowerCase(input.charAt(i)));
                } else {
                    result.append(input.charAt(i));
                }
            }
        }

        /*Add space before all numbers
         * Nothing is done after number because each number must be followed by an uppercase letter. Thus, there will be space between these two.
         * In case of a lowercase letter following number, this will be considered as the user's choice. Thus, there will be no space between these two.
         * */
        return result.toString().replaceAll("(\\d+)", " $1");
    }

    private boolean hasParameters(Method method) {
        return method.getParameterCount() > 0;
    }
}
@sbrannen
Copy link
Member

Hi @FanJups,

Congratulations on submitting your first issue for JUnit 5! 👍

Please note, however, that this is effectively a:

In any case, I'll leave this issue open for the team to discuss the topic again.

@sbrannen sbrannen changed the title Add new custom DisplayNameGenerator ReplaceCamelCaseAndUnderscoreAndNumber Introduce DisplayNameGenerator to support CamelCase, underscores, and numbers Nov 21, 2023
@FanJups
Copy link
Author

FanJups commented Nov 21, 2023

Hi @sbrannen , it's my pleasure to contribute JUnit after using it for years now. I am looking forward to here from the team. Yes, they are similar but this one goes further.

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

No branches or pull requests

2 participants