בדיקת פרמטריזציה עם JUnit 5.7: צלילה עמוקה לתוך @EnumSource

מבחנים מפורטים מאפשרים למפתחים לבדוק את הקוד שלהם באופן יעיל עם טווח של ערכי קלט. בתחום של מבחני JUnit, משתמשים ותיקים נאבקו זמן רב במורכבויות של השקעת מבחנים אלה. אבל עם יציאת גירסה 5.7 של JUnit, תקופה חדשה של מבחנים מפורטים נכנסת, המציעה תמיכה ראשונה ויכולות משופרות למפתחים. בואו נחקור את האפשרויות המרגשות שJUnit 5.7 מביא לשולחן למבחנים מפורטים!

דוגמאות פרמטריזציה מתוך התיעוד של JUnit 5.7

בואו נראה כמה דוגמאות מהתיעוד:

Java

 

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
    assertTrue(StringUtils.isPalindrome(candidate));
}

@ParameterizedTest
@CsvSource({
    "apple,         1",
    "banana,        2",
    "'lemon, lime', 0xF1",
    "strawberry,    700_000"
})
void testWithCsvSource(String fruit, int rank) {
    assertNotNull(fruit);
    assertNotEquals(0, rank);
}

@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
    assertEquals(5, str.length());
    assertTrue(num >=1 && num <=2);
    assertEquals(2, list.size());
}

static Stream<Arguments> stringIntAndListProvider() {
    return Stream.of(
        arguments("apple", 1, Arrays.asList("a", "b")),
        arguments("lemon", 2, Arrays.asList("x", "y"))
    );
}

האנוטציה @ParameterizedTest חייבת להלוות אחת ממספר אנוטציות מקור המתארות מהיכן לקחת את הפרמטרים. מקור הפרמטרים לעיתים קרובות נקרא "ספק הנתונים".

I will not dive into their detailed description here: the JUnit user guide does it better than I could, but allow me to share several observations:

  • ה@ValueSource מוגבל לספק ערך פרמטר יחיד בלבד. במילים אחרות, שיטת הבדיקה לא יכולה להתקבל יותר מארגומנט אחד, והסוגים שניתן להשתמש בהם מוגבלים גם כן.
  • העברת יותר מארגומנט אחד מתוארת באופן מסוים על ידי @CsvSource, שמפרש כל שורה לרשומה שאז מועברת כארגומנטים שדה-אחר-שדה. זה יכול להפוך לקשה לקריאה עם מחרוזות ארוכות ו/או רבים של ארגומנטים. גם הסוגים שניתן להשתמש בהם מוגבלים — יותר על כך מאוחר יותר.
  • כל המקורות שמגדירים ערכים אמיתיים באנוטציות מוגבלים לערכים קבועים בזמן ההידור (אנוטציות Java, לא JUnit).
  • @MethodSource ו-@ArgumentsSource מספקים זרם/אוסף של (לא-מסוגרים) n-צמדים שאז מועברים כארגומנטים של השיטה. ישנם סוגים שונים של סוגים ממשיים המוכללים לייצוג רצף של n-צמדים, אך אף אחד מהם לא מבטיח שהם יתאימו לרשימת הארגומנטים של השיטה. סוג זה של מקור דורש שיטות נוספות או כיתות, אך הוא לא מטיל הגבלה על המקום והדרך בהם לקבל את נתוני הבדיקה.

כפי שאתה רואה, סוגי המקור זמינים מהפשוטים (קלים לשימוש, אך מוגבלים ביכולת) ועד הגמישים ביותר שדורשים יותר קוד כדי להתחיל לעבוד.

  • הערה בצד — זה בדרך כלל סימן לעיצוב טוב: קצת קוד נדרש לתפקוד בסיסי, והוספת תוספת מורכבות מוצדקת כאשר משתמשים בה לאפשר מקרה שימוש יותר מתחייב.

מה שלא נראה שמתאים למשקל ההיפותטי פשוט-גמיש, הוא @EnumSource. תסתכל על דוגמה לא טריוויאלית של ארבע קבוצות פרמטרים עם 2 ערכים כל אחת.

  • הערה — בעוד ש-@EnumSource מעביר את ערך המינהוג כפרמטר יחיד של שיטת הבדיקה, באופן מושגי, הבדיקה ממופחתת על שדות המינהוג, שלא מטילים הגבלה על מספר הפרמטרים.
Java

 

    enum Direction {
        UP(0, '^'),
        RIGHT(90, '>'),
        DOWN(180, 'v'),
        LEFT(270, '<');

        private final int degrees;
        private final char ch;

        Direction(int degrees, char ch) {
            this.degrees = degrees;
            this.ch = ch;
        }
    }

    @ParameterizedTest
    @EnumSource
    void direction(Direction dir) {
        assertEquals(0, dir.degrees % 90);
        assertFalse(Character.isWhitespace(dir.ch));
        
        int orientation = player.getOrientation();
        player.turn(dir);
        assertEquals((orientation + dir.degrees) % 360, player.getOrientation());
    }

תחשבו על זה: רשימה קבועה של ערכים מגבילה בצורה חמורה את הגמישות (ללא מידע חיצוני או נוצר), בעוד שכמות הקוד הנוסף הדרוש להכרזת enum הופך את זה לאלטרנטיבה די מילולית בהשוואה ל, נאמר, @CsvSource.

אבל זו רק ראשונה התשה. נראה איזה שיפור יכול להיות כאשר מנצלים את הכוח האמיתי של מבני התקנה בג'אבה.

  • הערה בצד: המאמר הזה לא טורח להתייחס לאימות של מבני תקנה שהם חלק מקוד הייצור שלכם. אלה, כמובן, היו צריכים להיות מוכרזים ללא קשר לאופן בו אתם בוחרים לאמת אותם. במקום זאת, הוא מתמקד במתי ואיך לבטא את נתוני הבדיקה בצורת מבני תקנה.

מתי להשתמש בזה

יש מצבים שבהם מבני תקנה מצליחים יותר מהאלטרנטיבות:

מספר פרמטרים לכל בדיקה

כשכל מה שצריכים הוא פרמטר יחיד, אתם כנראה לא רוצים להסתבך יותר מ@ValueSource. אבל ברגע שצריכים יותר מאחד – למשל, קלטים ותוצאות מצופות – צריכים להישען על @CsvSource, @MethodSource/@ArgumentsSource או @EnumSource.

בדרך מסוימת, enum מאפשר לכם ל"הבריח" מספר לא מוגבל של שדות נתונים.

אז כשצריכים להוסיף יותר פרמטרים לשיטת הבדיקה בעתיד, רק הוספים יותר שדות במבנה התקנה הקיים, ומשאירים את חתימות שיטות הבד

עבור מקורות אחרים, יש להשתמש בArgumentsAccessorים או ArgumentsAggregatorים עבור הגמישות שהאנומרלים מעניקים מיידית.

בטחון טיפוס

עבור מפתחים Java, זה צריך להיות גדול.

פרמטרים שנקראים מ-CSV (קבצים או מילוליים), @MethodSource או @ArgumentsSource, הם לא מספקים ערעור זמן הקמת מחדל שמספר הפרמטרים וסוגיהם, יתאימו לחתימה.

ברור, JUnit יתלונן בזמן ריצה אך תשכחו מכל סיוע קוד מה-IDE שלכם.

כמו קודם, זה מסתכם כשאתם משתמשים באותם פרמטרים למבחנים מרובים. השימוש בגישה בטוחה טיפוסית יהיה זכות ענקית כשתרחיבו את קבוצת הפרמטרים בעתיד.

טיפוסים מUSTOM

זה בעיקר יתרון על מקורות מילוליים, כגון אלו שקוראים נתונים מ-CSV – הערכים המקודדים בטקסט צריכים להיות מומרים לטיפוסים Java.

אם יש לכם מחלקה מUSTOM להפעלה מתוך רשומת ה-CSV, אפשר לעשות זאת באמצעות ArgumentsAggregator. עם זאת, ההצהרה על הנתונים שלכם עדיין אינה בטוחה טיפוסית – כל סטייה בין החתימה של השיטה לבין הנתונים המוגדרים תתגלה בזמן הריצה כש"מאספים" את הארגומנטים. ולא לדבר על כך שהגדרת מחלקת האגרגטור מוסיפה יותר את קוד התמיכה הנחוץ לפרמטריזציה שלכם לעבוד. ואנו תמיד העדיפו @CsvSource על @EnumSource כדי להימנע מהקוד הנוסף.

ניתן לתיעוד

לחילופי השיטות האחרות, מקור המנוי (enum) מכיל סימנים של Java גם עבור קבוצות הפרמטרים (מופעים של enum) וגם עבור כל הפרמטרים שהם מכילים (שדות enum). הם מספקים מקום פשוט וברור לצירוף תיעוד בצורה יותר טבעית – JavaDoc.

זה לא אומר שהתיעוד לא יכול להימקם במקום אחר, אבל הוא יהיה – לפי ההגדרה – מרוחק יותר ממה שהוא מתעד ולכן קשה יותר למצוא וקל יותר להתיישן.

אבל יש עוד!

עכשיו: Enums. הם. כיתות.

נראה שהרבה מפיתוחי השורש עדיין לא הבינו כמה חזקים המנויים (enum) של Java באמת הם.

בשפות תכנות אחרות, הם באמת רק קבועים מוכבדים. אבל ב-Java, הם ישומים קטנים ונוחים של תבנית עיצוב Flyweight עם (חלק מ) היתרונות של כיתות מלאות.

למה זה דבר טוב?

התנהגות קשורה לתיקי ניסוי

כמו עם כל כיתה אחרת, ניתן להוסיף שיטות למנויים (enum).

זה הופך לשימושי אם פרמטרי ניסוי של enum משומשים בין ניסויים – אותה נתונים, רק מוכנים מעט שונה. כדי לעבוד יעיל עם הפרמטרים ללא הצורך בהעתקה גדולה, כמה קוד עזר צריך להיות משותף בין הניסויים.

זה לא משהו שכיתת עזר וכמה שיטות סטטיות לא י"פתרו".

  • הערת שוליים: שימו לב שתכנון כזה סובל מתשוקה לתכונות. שיטות בדיקה – או אפילו יותר גרוע, שיטות עזר של כיתות – יהיו צריכות למשוך את הנתונים מאובייקטי המנהלים כדי לבצע פעולות על אותם נתונים.

בעוד שזו הדרך (היחידה) בתכנות הרצפי, בעולם האובייקטים, אנחנו יכולים לעשות טוב יותר.

הצהרת שיטות "עזר" בתוך ההצהרה של המנהלים, נזיז את הקוד למקום שבו נמצאים הנתונים. או, להציב בלשון תכנון אובייקטים, שיטות העזר יהפכו ל"התנהגות" של תקעים בדיקה שמיושמים כמנהלים. זה לא רק יעשה את הקוד יותר עקבי (יקרא שיטות הגיוניות על מופעים במקום שיטות סטטיות העוברות נתונים), אלא גם יקל על שימוש מחדש בפרמטרים של המנהלים בין מקרי בדיקה שונים.

ירושה

המנהלים יכולים לממש ממשקים עם (שיטות ברירת מחדל). כאשר משתמשים בהם באופן הגיוני, זה יכול להיות מנוצל לחלוקת התנהגות בין מספר ספקי נתונים – מספר מנהלים.

דוגמה שקלה להגיע לראש מחשבה היא מנהלים נפרדים לבדיקות חיוביות ושליליות. אם הם מייצגים סוג דומה של תקע בדיקה, יש סיכוי גדול שיש להם חלק מהתנהגות לשיתוף.

השיחה חינם

בואו נמחיש זאת במערכת בדיקה של מתמר היפותטי של קבצי קוד מקור, שא

כדי שיהיה ביטחון אמיתי במה שכלי כה מקיף עושה, יהיה צורך בקבוצת קבצי קלט נרחבת המבטאת היבטים שונים של השפה, וקבצים תואמים להשוואת תוצאת ההמרה. מלבד זאת, יש צורך לוודא אילו אזהרות/שגיאות ניתנות למשתמש עבור קלטים בעייתיים.

זה מתאים באופן טבעי למבחנים עם פרמטרים בשל המספר הרב של דגימות שיש לאמת, אך זה לא מתאים לאף אחד ממקורות הפרמטרים הפשוטים של JUnit, מכיוון שהנתונים הם מעט מורכבים.

ראה למטה:

Java

 

    enum Conversion {
        CLEAN("imports-correct.2.py", "imports-correct.3.py", Set.of()),
        WARNINGS("problematic.2.py", "problematic.3.py", Set.of(
                "Using module 'xyz' that is deprecated"
        )),
        SYNTAX_ERROR("syntax-error.py", new RuntimeException("Syntax error on line 17"));
        // רבים, רבים אחרים ...

        @Nonnull
        final String inFile;
        @CheckForNull
        final String expectedOutput;
        @CheckForNull
        final Exception expectedException;
        @Nonnull
        final Set expectedWarnings;

        Conversion(@Nonnull String inFile, @Nonnull String expectedOutput, @NotNull Set expectedWarnings) {
            this(inFile, expectedOutput, null, expectedWarnings);
        }

        Conversion(@Nonnull String inFile, @Nonnull Exception expectedException) {
            this(inFile, null, expectedException, Set.of());
        }

        Conversion(@Nonnull String inFile, String expectedOutput, Exception expectedException, @Nonnull Set expectedWarnings) {
            this.inFile = inFile;
            this.expectedOutput = expectedOutput;
            this.expectedException = expectedException;
            this.expectedWarnings = expectedWarnings;
        }

        public File getV2File() { ... }

        public File getV3File() { ... }
    }

    @ParameterizedTest
    @EnumSource
    void upgrade(Conversion con) {

        try {
            File actual = convert(con.getV2File());
            if (con.expectedException != null) {
                fail("No exception thrown when one was expected", con.expectedException);
            }
            assertEquals(con.expectedWarnings, getLoggedWarnings());
            new FileAssert(actual).isEqualTo(con.getV3File());
        } catch (Exception ex) {
            assertTypeAndMessageEquals(con.expectedException, ex);
        }
    }

השימוש ב-enums אינו מגביל אותנו בכמה מורכבים הנתונים יכולים להיות. כפי שאתה יכול לראות, אנו יכולים להגדיר מספר קונסטרקטורים נוחים ב-enums, כך שהכרזת סטים חדשים של פרמטרים היא נקייה וברורה. זה מונע את השימוש ברשימות ארוכות של ארגומנטים שלעתים קרובות מסתיימות עם הרבה ערכים "ריקים" (nulls, מחרוזות ריקות, או אוספים) שמשאירים תוהים מה מייצג ארגומנט #7 — אתה יודע, אחד מה-nulls.

שים לב כיצד enums מאפשרים שימוש בסוגים מורכבים (Set, RuntimeException) ללא מגבלות או המרות קסומות. העברת נתונים כאלה היא גם בטוחה לחלוטין מבחינת סוג.

עכשיו, אני יודע מה אתה חושב. זה נורא מילולי. ובכן, עד לנקודה מסוימת. באופן ריאלי, אתה הולך שיהיו לך הרבה יותר דגימות נתונים לאימות, כך שכמות קוד ה-boilerplate תהיה פחות משמעותית בהשוואה.

כמו כן, ראה כיצד ניתן לכתוב מבחנים קשורים באמצעות אותם enums ושיטות העזר שלהם:

Java

 

    @ParameterizedTest
    @EnumSource
    // שכל שידרוג של קבצים שכבר שודרגו תמיד עובר, לא עושה שינויים, ולא מוציא אזהרות.
    void upgradeFromV3toV3AlwaysPasses(Conversion con) throws Exception {
        File actual = convert(con.getV3File());
        assertEquals(Set.of(), getLoggedWarnings());
        new FileAssert(actual).isEqualTo(con.getV3File());
    }

    @ParameterizedTest
    @EnumSource
    // שידרוג לאחור של קבצים שנוצרו על ידי תהליך שידרוג מצופה להיות תמיד מוצלח ללא אזהרות.
    void downgrade(Conversion con) throws Exception {
        File actual = convert(con.getV3File());
        assertEquals(Set.of(), getLoggedWarnings());
        new FileAssert(actual).isEqualTo(con.getV2File());
    }

דיבור נוסף אחרי כל הזמן

באופן קונספטואלי, @EnumSourceמעודד אותך ליצור תיאור מורכב, קל לקריאה על ידי מכונה של מצבי בחינה בודדים, ומסתיר את הקו בין מספקי נתונים למבני בחינה.

דבר נוסף נפלא בנוגע להשתמש בכל קבוצת נתונים כסמל ג'אווה (אלמנט של enum) הוא שהם יכולים להיות מושתמים באופן יחידני; לחלטין מחוץ למספקי נתונים/בדיקות מפורשות. מכיוון שיש להם שם הגיוני והם מוכלים בעצמם (במונחים של נתונים והתנהגות), הם תורמים לבדיקות קריאות ונחמדות.

Java

 

@Test
void warnWhenNoEventsReported() throws Exception {
    FixtureXmls.Invalid events = FixtureXmls.Invalid.NO_EVENTS_REPORTED;
    
    // read() היא שיטה עזר שמשותפת על ידי כל FixtureXmls
    try (InputStream is = events.read()) {
        EventList el = consume(is);
        assertEquals(Set.of(...), el.getWarnings());
    }
}

עכשיו, @EnumSource לא הולך להיות אחד ממקורות הארגומנטים הנפוצים ביותר שתשתמשו בהם, וזה דבר טוב, מכיוון ששימוש יתר בו לא יביא לשום תועלת. אבל בנסיבות הנכונות, זה נוח לדעת כיצד להשתמש בכל מה שהם מציעים.

Source:
https://dzone.com/articles/junit5-parameterized-tests-with-enumsource