Μια επισκόπηση της ανάπτυξης καθοδηγούμενης από δοκιμές.
Η ανάπτυξη καθοδηγούμενη από tests (TDD) αναφέρεται σε ένα στυλ προγραμματισμού, όπου τα tests γράφονται για να καθοδηγήσουν την υλοποίηση του σχεδιασμού του προγράμματος σε κώδικα.
Γράφεις ένα ή περισσότερα tests (ιδίως unit tests) πριν από τον προγραμματισμό. Τα tests στοχεύουν να καλύψουν μία πτυχή της συμπεριφοράς του προγράμματος, η οποία μπορεί να εστιάζει σε μία μόνο συνάρτηση ή μέθοδο. Το γράψιμο των tests είναι ένας τρόπος να μετατρέψεις τις απαιτήσεις του προγράμματος και τη γενική αρχιτεκτονική σε έναν σχεδιασμό ειδικό για την υλοποίηση. Τρέχεις τα tests και θα πρέπει να αποτύχουν, επειδή ο κώδικας δεν έχει υλοποιηθεί ακόμη. Υλοποιείς τον κώδικα και τρέχεις τα tests ξανά. Αν τα tests περάσουν, τότε είτε η υλοποίηση της συμπεριφοράς έχει ολοκληρωθεί, είτε ίσως χρειάζεται να δημιουργηθούν κι άλλα απαραίτητα tests. Αν τα tests δεν περάσουν, τότε κάνεις debug στον κώδικα και τρέχεις τα tests ξανά. Ο κύκλος των tests και του προγραμματισμού επαναλαμβάνεται μέχρι να περάσουν όλα τα απαραίτητα tests, οπότε και η υλοποίηση αυτής της πτυχής της συμπεριφοράς του προγράμματος έχει ολοκληρωθεί... για την ώρα.
Η αναδιαμόρφωση είναι η επανεγγραφή κώδικα με σκοπό τη βελτίωση του σχεδιασμού του. Δεν είναι απλώς η επανεγγραφή κώδικα για να διορθωθούν bugs. Μερικές φορές ονομάζεται "αναδιαμόρφωση" η τροποποίηση κώδικα για να περάσει τα tests. Αν και η τροποποίηση του κώδικα μπορεί να περιλαμβάνει τη βελτίωση του σχεδιασμού ως τρόπο για να περάσουν τα tests, το να κάνεις απλώς debug δεν βελτιώνει απαραίτητα τον σχεδιασμό του κώδικα, άρα δεν είναι απαραίτητα αναδιαμόρφωση.
Το παρακάτω είναι ένα παράδειγμα debug χωρίς αναδιαμόρφωση:
# A function intended to return x added to y.
# x and y are bad parameter names, but we ignore that for now.
def add(x, y):
# used multiply operator by mistake. It fails the tests.
return x * y
# Function corrected. It passes the tests. It has been debugged, but not refactored.
def add(x, y):
return x + y
Το παρακάτω είναι ένα παράδειγμα αναδιαμόρφωσης και μετά debug:
# Function name and parameter names are modified to something more meaningful. This is refactoring.
def lot_inventory(old_cars, new_cars):
# Introduced multiply operator by mistake. It fails the tests. This is why we test.
return old_cars * new_cars
# Function corrected. It passes the tests. This is debugging.
def lot_inventory(old_cars, new_cars):
return old_cars + new_cars
Το track Python του Exercism χρησιμοποιεί τη μεθοδολογία TDD στις ασκήσεις του. Τα unit tests είναι ήδη γραμμένα. Ο μαθητής μπορεί να δει τα tests για να καταλάβει πιο αναλυτικά τι απαιτείται ώστε να περάσει μια λύση. Μπορεί να δοθεί στον μαθητή ένα stub λύσης.
Όταν ένα ή περισσότερα tests αποτυγχάνουν για μια λύση σε Python, η αντίστοιχη εργασία (ή εργασίες) δεν θα έχει πράσινο φόντο. Η πρώτη περιοχή εργασίας που αποτυγχάνει θα είναι αναπτυγμένη και η κεφαλίδα της θα μοιάζει κάπως έτσι
Task 1 Extract coordinates -
Αν κάνεις κλικ στο σύμβολο μείον, η εργασία θα συμπτυχθεί, ώστε να μπορούμε να δούμε άλλες εργασίες, αλλά για την ώρα θα μείνουμε σε αυτήν.
Από κάτω θα υπάρχει μια αναπτυγμένη περιοχή Test που θα μοιάζει κάπως έτσι
Test 1 ⌄
FAILED TisburyTreasure > get coordinate
όπου το Tisbury Treasure δείχνει την άσκηση και το get_coordinate δείχνει τη συνάρτηση ή τη μέθοδο που αποτυγχάνει.
Το Test 1 συνήθως θα είναι ένα είδος προτύπου με ένα τμήμα κώδικα για τη ρύθμιση των tests.
Δεν περιέχει πληροφορίες για το συγκεκριμένο test (ή tests) που απέτυχε.
Προς το τέλος θα λέει ότι
One or more variations of this test failed. Details can be found under each [variant#].
Αν κάνεις κλικ στο ⌄, το test θα συμπτυχθεί.
Από κάτω θα υπάρχει ένα συμπτυγμένο test που μοιάζει κάπως έτσι:
Test 2 >
FAILED TisburyTreasure > get coordinate [variation #1] (item=
("Scrimshaw Whale's Tooth", '2A'), result='2A')
Η εμφάνισή του θα διαφέρει ανάλογα με το πλάτος που έχει οριστεί για το δεξιό παράθυρο.
Αν κάνεις κλικ στο >, το test θα αναπτυχθεί.
Τα δεδομένα εισόδου και τα αναμενόμενα δεδομένα αποτελέσματος πιθανότατα θα εμφανίζονται σε ένα τμήμα κώδικα.
Τα δεδομένα μπορεί να αφορούν όλα τα tests αυτής της εργασίας.
Στο κάτω μέρος, στην ενότητα Test Failure, υπάρχει ο συγκεκριμένος λόγος για τον οποίο απέτυχε αυτό το test.
Μπορεί να μοιάζει κάπως έτσι:
AssertionError: ['2A'] != '2A'
Στη συγκεκριμένη περίπτωση, δείχνει ότι η τιμή επιστροφής ['2A'] δεν ήταν ίση με την αναμενόμενη τιμή '2A'.
Αν κοιτάξουμε τον κώδικα της get_coordinate, βλέπουμε ότι έχει υλοποιηθεί έτσι
def get_coordinate(record):
return [record[1]]
Αν αφαιρέσουμε τις αγκύλες της λίστας (π.χ. return record[1]) και τρέξουμε τα tests ξανά, τα tests της Task 1 θα περάσουν.
Αν μία ή περισσότερες εργασίες εξακολουθούν να αποτυγχάνουν, τότε η παραπάνω διαδικασία επαναλαμβάνεται με καθεμιά από αυτές μέχρι να περάσουν όλα τα tests.
Μερικές φορές, τα αναμενόμενα δεδομένα και τα δεδομένα που επιστρέφονται είναι πολύ μεγάλα για να χωρέσουν όλα στην ενότητα Test Failure.
Μπορεί να μοιάζει κάπως έτσι:
AssertionError: '("Sc[67 chars]\')\n\n(\'Brass Spyglass\', \'Abandoned Lighth[952 chars]')\n' != '("Sc[67 chars]\')\n(\'Brass Spyglass\', \'Abandoned Lighthou[928 chars]')\n'
Diff is 970 characters long. Set self.maxDiff to None to see it.
Μπορεί να υπάρχουν ακόμη αρκετά δεδομένα για να δεις ποιο είναι το πρόβλημα.
Στην παραπάνω περίπτωση, επιστρέφονται δύο αλλαγές γραμμής (π.χ. \n\n(\'Brass Spyglass) ενώ αναμένεται μόνο μία (π.χ. \n(\'Brass Spyglass).
Συγχαρητήρια! Όλα τα tests πέρασαν. Και τώρα τι; Η λύση μπορεί να δημοσιευτεί αμέσως. Ή, τώρα που ο κώδικας δουλεύει, αν θέλεις για οποιονδήποτε λόγο να τον αναδιαμορφώσεις, μπορείς να τροποποιήσεις τον κώδικα και να υποβάλεις άλλη επανάληψη. Αν πιστεύεις ότι ο κώδικας θα μπορούσε να είναι καλύτερος, αλλά δεν ξέρεις πώς, μπορείς να ζητήσεις καθοδήγηση από μέντορα για τη λύση. Αν υπάρχει διαθέσιμος μέντορας, μπορεί να επικοινωνήσει μαζί σου με ιδέες για άλλες προσεγγίσεις στη λύση. Όταν δημοσιεύεις τη λύση σου, μπορείς να επιτρέψεις σχόλια και άλλοι μαθητές μπορεί να εκμεταλλευτούν την ευκαιρία για να γράψουν σχόλια ή να κάνουν ερωτήσεις.
Αν και η "πρόωρη βελτιστοποίηση είναι η ρίζα όλων των κακών" (φράση που αποδίδεται τόσο στον Tony Hoare όσο και στον Donald Knuth), έρχεται κάποια στιγμή που, ακόμη κι αν η λύση δουλεύει, θέλεις να βελτιώσεις την απόδοσή της.
Μία από αυτές τις στιγμές μπορεί να είναι όταν η λύση περνάει κάποια tests αλλά σε άλλα υπερβαίνει το χρονικό όριο.
Μπορεί να σου φανεί χρήσιμο να ξέρεις ακριβώς πόσο χρόνο παίρνει ένα κομμάτι κώδικα.
Το module timeit μπορεί να χρησιμοποιηθεί για να μετρήσεις τον χρόνο εκτέλεσης κώδικα, ακόμη και σε πολύ μικρές διάρκειες.
Η συνάρτηση timeit μπορεί να δεχτεί μέχρι πέντε ορίσματα: timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None).
Η παράμετρος stmt ορίζει τον πραγματικό κώδικα που θα εκτελεστεί και θα μετρηθεί.
Η παράμετρος number καθορίζει πόσες φορές θα εκτελεστεί ο κώδικας του stmt.
Η παράμετρος setup ορίζει τον κώδικα που εκτελείται μόνο μία φορά για να προετοιμάσει την εκτέλεση του κώδικα του stmt.
Ο χρόνος εκτέλεσης του κώδικα του setup περιλαμβάνεται στον συνολικό χρόνο.
Όσο περισσότερες επαναλήψεις εκτελεστεί ο κώδικας του stmt, τόσο λιγότερο θα μετράει ο χρόνος του setup ανά επανάληψη.
Η παράμετρος timer επιτρέπει να περάσεις ένα διαφορετικό Timer από το προεπιλεγμένο.
Το προεπιλεγμένο όρισμα για την παράμετρο timer είναι το perf_counter, το οποίο αρκεί για τις περισσότερες περιπτώσεις.
Το προεπιλεγμένο όρισμα για την παράμετρο number είναι 1_000_000.
Η παράμετρος globals καθορίζει έναν χώρο ονομάτων στον οποίο θα εκτελεστεί ο κώδικας.
Το προεπιλεγμένο όρισμα για την παράμετρο globals είναι None.
Το παρακάτω είναι ένα παράδειγμα χρήσης του timeit για να δεις πόσο χρόνο παίρνει να προσδιοριστεί αν μια πρόταση περιέχει όλα τα αγγλικά φωνήεντα:
import timeit
# run one million times
loops = 1_000_000
# first positional argument is for stmt
# second positional argument is for setup
# third (named) argument is for number
print(timeit.timeit("""has_all_vowels('Another piggy digs up the truffles.')""",
"""
VOWELS = "AEIOU"
def has_all_vowels(sentence):
return all(letter in sentence.casefold() for letter in VOWELS)
""", number=loops) / loops)
Η εκτέλεση του κώδικα ένα εκατομμύριο φορές πήρε κατά μέσο όρο 4.965089999896008e-07 δευτερόλεπτα ανά κλήση (περίπου 497 νανοδευτερόλεπτα ανά κλήση).
Το παρακάτω παράδειγμα είναι για να δεις αν βγάζοντας την κλήση της casefold έξω από τη list comprehension εξοικονομείται χρόνος:
import timeit
loops = 1_000_000
print(timeit.timeit("""has_all_vowels('Another piggy digs up the truffles.')""",
"""
VOWELS = "AEIOU"
def has_all_vowels(sentence):
sentence = sentence.casefold()
return all(letter in sentence for letter in VOWELS)
""", number=loops) / loops)
Η εκτέλεση του κώδικα ένα εκατομμύριο φορές πήρε κατά μέσο όρο 4.923898000270128e-07 δευτερόλεπτα ανά κλήση (περίπου 492 νανοδευτερόλεπτα ανά κλήση.)
Έτσι, βγάζοντας την casefold έξω από τη list comprehension εξοικονομήθηκαν περίπου 5 νανοδευτερόλεπτα ανά κλήση, ή περίπου 5 χιλιοστά του δευτερολέπτου συνολικά για ένα εκατομμύριο κλήσεις.
Το cProfile μπορεί επίσης να χρησιμοποιηθεί για profiling κώδικα. Ωστόσο, δεν έχει τόση λεπτομέρεια, αφού φτάνει μόνο μέχρι διάρκειες της τάξης του χιλιοστού του δευτερολέπτου.