פיתוח מונחה בדיקות בשפת קוטלין

על מה הפוסט?

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

בפוסט זה נכתוב אפליקציית REST פשוטה שמחזירה תשובה סטטית ובפוסט הבא נרחיב את האפליקציה כך שתפנה לשירות חיצוני ע”י שימוש בתכנות תגובתי (Reactive Programming).

יש פרויקט מלווה בGithub. מי שרוצה להעמיק מוזמן להוריד את הפרויקט, לעבור לbranch בשם part1, למחוק את הקוד (להשאיר רק את הטסטים) ולממש בעצמו.

שפת קוטלין

קוטלין היא שפת פיתוח חדשה יחסית שפותחה ע”י JetBrains ומאפשרת קלות ונוחות פיתוח שאנו מכירים משפות דינמיות בשפה סטטית. השפה רצה על הJVM ויש לה תאימות מצוינת עם קוד ג’אווה. לשפה תחביר מאוד פשוט שמזכיר קצת Groovy והיא מאוד קלה ללמידה למפתחי ג’אווה. היא מכילה את רוב הפיצ’רים המודרניים שחסרים בג’אווה כגון Default/Named Arguments, Type Inference & Null Safety (השפה מגנה מNull ברמת הקומפלייר!) ומאפשרת כתיבת קוד פונקציונלי בצורה הרבה יותר נוחה מג’אווה. מה שזה אומר בפועל, זה שהקוד שלנו יהיה הרבה יותר קצר, קריא ומהיר לכתיבה ותחזוקה.

תכנות מונחה בדיקות (TDD)

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

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

יתרון אחרון הוא הגעה לעיצוב בעל צמידות נמוכה (decoupled). הדבר נובע מכך שקוד עם צמידות גבוהה הרבה יותר קשה ואיטי לבדיקה. תהליך הTDD יגרום לנו לטפל בצמידויות וידחוף לעיצוב מודולרי ולהבנה טובה של התלויות.

יצירת פרויקט Spring Boot עם קוטלין

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

למי שלא מכיר את Spring Boot, אסביר בקצרה שמדובר בתשתית לפיתוח Micro services שמבוססת על תשתיות קודמות של ספרינג כגון Spring MVC.  ספרינג תומך בקוטלין ומספק אתר מצוין ליצירת פרויקט חדש – https://start.spring.io. נגלוש לאתר, נבחר פרויקט Gradle עם קוטלין ונוסיף את Reactive Web.

הפרויקט שנוצר סה”כ עובד, הבעיה היחידה שנתקלתי בה היא צורך לשנות את גרסת הJVM מ1.9 ל1.8 (הדבר מתבצע מתפריט הFile->Settings מסך Kotlin Compiler תחת Target JVM version). נפתח את הפרויקט ע”י בחירת קובץ הbuild.gradle. נקבל פרויקט שמכיל קובץ Application וקובץ בדיקות. מאוד דומה למה שהיינו מקבלים אם היינו מחוללים פרויקט בג’אווה.

נתבונן בקובץ DemoApplication.kt:

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
 runApplication<DemoApplication>(*args)
}

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

נעבור לשורה 4 – המילה fun מגדירה פונקציה בקוטלין, כאשר ברירת המחדל לפונקציות בקוטלין היא פומבית (public). השורה שקולה להגדרת main בג’אווה. הפונקציה מוגדרת מחוץ למחלקה, מכיוון שבשונה מג’אווה מעמד הפונקציות שווה למעמד המחלקות (אין צורך לעטוף כל פונקציה במחלקה). שימו לב לצורה השונה של הגדרת הפרמטר- שם המשתנה קודם להצהרה על הטיפוס.

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

קצת על בדיקות

בפוסט זה נכתוב מספר סוגי בדיקות:
1. בדיקות יחידה (Unit Tests) – שימוש בכפילים (mocks) לכל תלות מחוץ לקוד הנבדק. בדיקות שרצות מהר ועוזרות לשיפור המבנה הפנימי של הקוד והגעה לDesign טוב מכיוון שהן דוחפות אותנו למדל את הקוד בצורה קלה לבדיקה, שמביאה לצמצום תלויות קשיחות בקוד. הבדיקות מחייבות אותנו לחשוב מה כל קטע קוד עושה ומדוע.
2. בדיקות אינטגרציה (Integration Tests) – בדיקות של מספר יחידות יחד או התממשקות אל ומתוך השירות שלנו (לשירות אחרים). עדיין יהיה שימוש בכפילים אך הטסט יהיה איטי יותר.
3. בדיקות קצה לקצה (End to End Tests)- בדיקת השירות שלנו מבחוץ (קופסה שחורה). אין שימוש בכפילים. הטסטים הכי איטיים והכי שבירים.

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

נושא הטסטים הוא רחב מספיק למספר פוסטים ממוקדים… בפוסט זה (ובהמשכיו) נראה בצורה פרקטית מתי להשתמש בכל טסט. מי שרוצה להרחיב מוזמן לקרוא את הפוסט המצוין – The Practical Test Pyramid.

הוספת בדיקת קצה לקצה

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

class SanityE2ETestsJavaStyle {
    @Test
    fun `isp service pass sanity - Java`() {
        `when`().
            get("/isp").
        then().
            statusCode(200)
    }
}

נשתמש בREST Assured ספרייה מומלצת לג’אווה שמספקת DSL
(Domain-specific language) שמאפשר כתיבת בדיקות לממשקי REST בצורה נוחה.

כזכור המילה fun משמשת להגדרת פונקציה בקוטלין (שורה 3), שימו לב שהשם של הפונקציה במירכאות. קוטלין מאפשרת להגדיר שם לפונקציה המכיל רווחים ע”י תחימת השם במירכאות, דבר המשפר את קריאות הבדיקות. מעל הפונקציה (שורה 2) השתמשנו באנוטציה המוכרת Test@ של Junit.

בשורה 4, המילה when (פונקציה סטטית שמסופקת ע”י REST Assured) היא במירכאות מכיוון שזוהי מילה שמורה בקוטלין. במקרה זה השימוש במירכאות מאפשר תאימות עם פונקציות מג’אווה.

הכתיבה המוכרת מג’אווה מאפשרת ממשק (API) נוח מאוד, אך ניתן לכתוב את הבדיקה בצורה יותר אלגנטית בקוטלין:

class SanityE2ETests : StringSpec({
    "isp service pass sanity" {
        given {
            on {
                get("/isp") itHas {
                    statusCode(200)
                }
            }
        }
    }
})

השתמשנו בKotlinTest ספריה שעושה ‘קסמים’ ע”י שימוש ביכולות מתקדמות של קוטלין כדי לשפר את הDSL המוגבל שאפשרי בג’אווה לDSL קריא יותר ודומה יותר למה שמוכר משפות דינמיות.

בשורה 1 אנחנו יורשים מStringSpec, הנקודתיים מחליפות את המילה extend בג’אווה. בהמשך השורה אנחנו מעבירים לבנאי של StringSpec פונקציה שבה מוגדרות הבדיקות שלנו. המחלקה StringSpec מאפשרת תחביר יותר ‘זורם’ ע”י שימוש במחרוזת במקום פונקציה להגדרת טסט.

השימוש במחרוזת מתאפשר ע”י תכונה מעניינת של קוטלין – הרחבת מחלקות קיימות ללא שימוש בירושה או ב’טריקים’ אחרים כגון Decorator. ע”י שימוש בתכונה זו נוכל להוסיף מתודות חדשות למחלקות המוכרות מג’אווה. כאן מבוצעת הרחבה מסוג invoke שמאפשרת להפעיל את האובייקט בתור פונקציה, כלומר שורה 2 היא בעצם קריאה לפונקציה שמוגדרת על כל מחרוזת (טיפוס String מג’אווה) בסקופ של StringSpec ובנותיה. קוטלין מאפשרת להשמיט את הסוגריים במקרה של פונקציה שמקבלת פונקציה כפרמטר. הפונקציה “isp service pass sanity” מקבלת את הפונקציה given בתור פרמטר, באותו אופן given מקבלת את on  ו-on מקבלת את get. זה מאפשר תחביר יותר זורם אבל יכול מאוד לבלבל בהתחלה.

כדי להגיע לDSL הנ”ל מעל Rest Assured יש צורך להגדיר מספר פונקציות נוספות (המפורטות כאן). לא נשתמש בהמשך ביכולות אלו, אז לא לדאוג אם זה לא מובן לגמרי (מי שרוצה ללמוד קצת יותר על הרחבת מחלקות בקוטלין מוזמן לקרוא כאן). המטרה של דוגמה זו היא רק לחשוף אתכם לאפשרות לייצר DSL יותר אלגנטי מעל קוד ג’אווה קיים וזאת ע”י שימוש בשפה סטטית!

יופי, יש טסט שנכשל. אפשר לכתוב קוד.

הגדרת מיפוי

 בחלק זה נגדיר Controller של ספרינג שיאפשר לאפליקציה שלנו לקבל בקשות REST:

@RestController
class IspController {
    
    @GetMapping("/isp")
    fun isp() = "OK"
}

די דומה להגדרה המוכרת מג’אווה, רק הרבה יותר תמציתי. הפונקציה בשורה 5 מחזירה מחרוזת קבועה “OK” וממופה לUrl בשורה 4. בקוטלין ניתן להשמיט הרבה פרטים כאשר מדובר בפונקציה בת שורה אחרת. הפונקציה למעשה שקולה ל:

fun isp(): String {
  return "OK"  
}

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

לצורך הרצת הטסט נעלה את השרת שלנו ע”י הרצת DemoApplication.kt (מכיוון שמדובר בטסט שבודק שרת ‘חי’) ונריץ את הטסט. עובר? יופי, אפשר להמשיך.

בדיקת אינטגרציה

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

לכן, נוסיף טסט אינטגרטיבי שיכשל-

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests : StringSpec() {
    @LocalServerPort
    var port: Int = 0

    override fun listeners() = listOf(SpringListener)

    override fun beforeTest(description: Description) {
        RestAssured.port = port
    }

    init {
        "isp controller pass integration" {
            given {
                on {
                    get("/isp") itHas {
                        statusCode(200)
                        body("country", CoreMatchers.equalTo("United States"))
                        body("isp", CoreMatchers.equalTo("GoDaddy.com, LLC"))
                    }
                }
            }
        }
    }
}

האנוטציה בשורה 1 תעלה את האפליקציה שלנו בצורה אוטומטית כחלק מהטסט בפורט רנדומלי (כמו שהיינו עושים בג’אווה). בשורה 2 אנחנו מגדירים מחלקה לטסט שיורשת מStringSpec שהכרנו קודם.

בשורה 3 נשתמש באנוטציה המוכרת מג’אווה, כדי להציב את הפורט הרנדומלי שנבחר בשדה port. השדה מוגדר בשורה 4, ע”י שימוש במילה השמורה var או val כאשר השימוש ב val שקול לשימוש בfinal בג’אווה. שימו לב לאתחול של המשתנה באפס. הדבר דרוש מכיוון שבקוטלין משתנה לא יכול להיות null ונקבל שגיאת קומפילציה עבור שדה לא מאותחל או ניסיון להשתמש במשתנה מקומי ללא אתחול! אציין שניתן להשתמש במילה השמורה lateinit כדי להורות לקוטלין שהמשתנה יאותחל בהמשך, אך זה לא עובד בPrimitives (שימו לב שInt בקוטלין הוא פרימטיב).

שורה 6 דרושה כדי להריץ את הטסט עם ספרינג. זוהי בעצם המקבילה של StringSpec לRunwith@ מJunit4 או ExtendWith@ מJunit5. נשתמש גם בטסט זה בRest Assured שבו השתמשנו בבדיקה קצה לקצה. בשורה 9 נאתחל את הספריה לשימוש בפורט הרנדומלי שנבחר ע”י ספרינג.

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

נתקן את הController כדי להעביר את הטסט:

@GetMapping("/isp")
fun ispDetails() = mapOf(
    "country" to "United States",
    "isp" to "GoDaddy.com, LLC"
)

בקוד הנ”ל אנחנו מגדירים מפה עם שני מפתחות. לא חושב שיש מה להסביר יותר מידי, סה”כ אלגנטי וברור. המפה שתיווצר תהיה מטיפוס java.util.LinkedHashMap. אם נרצה HashMap רגיל, פשוט נחליף את mapOf בhashMapOf. קל. האתחול אולי מעט מסורבל (יחסית לפייתון למשל), אבל הגישה למפה היא אלגנטית כמצופה מהשפה:

print(ispDetails()["isp"])

פשוט נכתוב את שם המפתח בסוגרים מרובעים, אין צורך בget של ג’אווה.

אחת ההחלטות החכמות בכתיבת קוטלין הייתה להתבסס על הcollections של ג’אווה מה שבעצם מאפשר תאימות מלאה עם קוד ג’אווה ישן. עוד משהו מעניין הוא שבקוטלין ברירת המחדל לMap (ולכל collection) היא immutable, כלומר לא נוכל לשנותה לאחר האתחול (קיימת גם MutableMap במידת הצורך). הדבר מתבצע ע”י הגדרה מחדש של הממשקים (interfaces) המוכרים מג’אווה בקוטלין, כך שכל ממשק הוא בעצם readonly view של המחלקה המממשת מג’אווה.

נריץ את הטסט ואחרי שנראה שהוא עובר נעלה את האפליקציה ונריץ את בדיקת הקצה לקצה שלנו. נבצע את הcommit הראשון שלנו.

סיכום

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

בפוסט הבא נראה איך קוראים לשירות חיצוני ע”י שימוש בProject Reactor וכמובן שנכתוב עוד הרבה טסטים על הדרך:)

קישורים

The Practical Test Pyramid
קוטלין (Kotlin) למפתחי ג’אווה ותיקים
The Three Laws of TDD (Featuring Kotlin) – Uncle Bob
Try Kotlin (Online code execution)