Δοκιμή βάσει ανάπτυξης: τι είναι και τι δεν είναι.

Η δοκιμαστική ανάπτυξη έχει γίνει δημοφιλής τα τελευταία χρόνια. Πολλοί προγραμματιστές έχουν δοκιμάσει αυτήν την τεχνική, απέτυχαν και κατέληξαν στο συμπέρασμα ότι το TDD δεν αξίζει τον κόπο που απαιτεί.

Μερικοί προγραμματιστές πιστεύουν ότι, θεωρητικά, είναι μια καλή πρακτική, αλλά ότι δεν υπάρχει ποτέ αρκετός χρόνος για να χρησιμοποιήσετε πραγματικά το TDD. Και άλλοι πιστεύουν ότι είναι βασικά χάσιμο χρόνου.

Εάν αισθάνεστε έτσι, νομίζω ότι ίσως να μην καταλαβαίνετε τι είναι το TDD. (Εντάξει, η προηγούμενη πρόταση ήταν να τραβήξει την προσοχή σας). Υπάρχει ένα πολύ καλό βιβλίο για το TDD, Test Driven Development: By Example, του Kent Beck, αν θέλετε να το δείτε και να μάθετε περισσότερα.

Σε αυτό το άρθρο θα διαβάσω τις βασικές αρχές του Test Driven Development, αντιμετωπίζοντας κοινές παρανοήσεις σχετικά με την τεχνική TDD. Αυτό το άρθρο είναι επίσης το πρώτο από πολλά άρθρα που πρόκειται να δημοσιεύσω, όλα σχετικά με το Test Driven Development.

Γιατί να χρησιμοποιήσετε το TDD;

Υπάρχουν μελέτες, έγγραφα και συζητήσεις σχετικά με το πόσο αποτελεσματικό είναι το TDD. Παρόλο που είναι σίγουρα χρήσιμο να έχουμε κάποιους αριθμούς, δεν νομίζω ότι απαντούν στο ερώτημα γιατί πρέπει να χρησιμοποιήσουμε το TDD.

Ας πούμε ότι είστε προγραμματιστής ιστού. Μόλις ολοκληρώσατε ένα μικρό χαρακτηριστικό. Θεωρείτε αρκετό να δοκιμάσετε αυτήν τη λειτουργία απλώς αλληλεπιδρώντας χειροκίνητα με το πρόγραμμα περιήγησης; Δεν νομίζω ότι αρκεί να βασίζομαι μόνο σε δοκιμές που γίνονται από προγραμματιστές με μη αυτόματο τρόπο. Δυστυχώς, αυτό σημαίνει ότι μέρος του κώδικα δεν είναι αρκετά καλό.

Αλλά η παραπάνω σκέψη αφορά τη δοκιμή και όχι το ίδιο το TDD Γιατί λοιπόν TDD; Η σύντομη απάντηση είναι «επειδή είναι ο απλούστερος τρόπος για να επιτευχθεί τόσο καλής ποιότητας κωδικός όσο και καλής κάλυψης δοκιμών».

Η μεγαλύτερη απάντηση προέρχεται από αυτό που πραγματικά είναι το TDD… Ας ξεκινήσουμε με τους κανόνες.

Οι κανόνες του παιχνιδιού

Ο θείος Μπομπ περιγράφει το TDD με τρεις κανόνες:

- Δεν επιτρέπεται να γράψετε κανέναν κωδικό παραγωγής, εκτός εάν πρόκειται να πετύχετε μια δοκιμαστική μονάδα που αποτύχει. και οι αποτυχίες συλλογής είναι αποτυχίες. - Δεν επιτρέπεται να γράψετε περισσότερο κωδικό παραγωγής από ότι αρκεί για να περάσετε τη δοκιμή μιας αποτυχημένης μονάδας.

Μου αρέσει επίσης μια μικρότερη έκδοση, την οποία βρήκα εδώ:

- Γράψτε μόνο αρκετή δοκιμή μονάδας για να αποτύχετε. - Γράψτε μόνο αρκετό κωδικό παραγωγής για να κάνετε την αποτυχημένη δοκιμή μονάδας.

Αυτοί οι κανόνες είναι απλοί, αλλά τα άτομα που πλησιάζουν το TDD παραβιάζουν συχνά ένα ή περισσότερα από αυτά. Σας προκαλώ: μπορείτε να γράψετε ένα μικρό έργο ακολουθώντας αυστηρά αυτούς τους κανόνες; Με μικρό έργο εννοώ κάτι πραγματικό, όχι μόνο ένα παράδειγμα που απαιτεί όπως 50 γραμμές κώδικα.

Αυτοί οι κανόνες καθορίζουν τους μηχανισμούς του TDD, αλλά σίγουρα δεν είναι όλα όσα πρέπει να γνωρίζετε. Στην πραγματικότητα, η διαδικασία χρήσης του TDD περιγράφεται συχνά ως ένας κύκλος κόκκινου / πράσινου / αντιδραστήρα. Ας δούμε τι είναι.

Κύκλος Red Green Refactor

Κόκκινη φάση

Στην κόκκινη φάση, πρέπει να γράψετε ένα τεστ για μια συμπεριφορά που πρόκειται να εφαρμόσετε. Ναι, έγραψα συμπεριφορά . Η λέξη «test» στην δοκιμαστική ανάπτυξη είναι παραπλανητική. Θα έπρεπε να το ονομάσουμε «Συμπεριφορική ανάπτυξη». Ναι, ξέρω, ορισμένοι υποστηρίζουν ότι το BDD είναι διαφορετικό από το TDD, αλλά δεν ξέρω αν συμφωνώ. Έτσι, στον απλοποιημένο ορισμό μου, BDD = TDD.

Εδώ έρχεται μια κοινή λανθασμένη αντίληψη: «Πρώτα γράφω μια τάξη και μια μέθοδο (αλλά καμία εφαρμογή), μετά γράφω μια δοκιμή για να δοκιμάσω αυτήν τη μέθοδο κλάσης» Στην πραγματικότητα δεν λειτουργεί με αυτόν τον τρόπο.

Ας κάνουμε ένα βήμα πίσω. Γιατί ο πρώτος κανόνας του TDD απαιτεί να συντάξετε ένα τεστ πριν να γράψετε οποιοδήποτε κομμάτι κώδικα παραγωγής; Είμαστε μανιακοί άνθρωποι TDD;

Κάθε φάση του κύκλου RGR αντιπροσωπεύει μια φάση στον κύκλο ζωής του κώδικα και πώς μπορεί να σχετίζεται με αυτόν.

Στην κόκκινη φάση, ενεργείτε σαν να είστε απαιτητικός χρήστης που θέλει να χρησιμοποιήσει τον κώδικα που πρόκειται να γραφτεί με τον απλούστερο δυνατό τρόπο. Πρέπει να γράψετε ένα τεστ που χρησιμοποιεί ένα κομμάτι κώδικα σαν να είχε ήδη εφαρμοστεί. Ξεχάστε την εφαρμογή! Εάν, σε αυτήν τη φάση, σκέφτεστε πώς θα γράψετε τον κωδικό παραγωγής, το κάνετε λάθος!

Είναι σε αυτήν τη φάση όπου εστιάζετε στη σύνταξη μιας καθαρής διεπαφής για μελλοντικούς χρήστες. Αυτή είναι η φάση όπου σχεδιάζετε πώς ο κώδικάς σας θα χρησιμοποιηθεί από τους πελάτες.

Αυτός ο πρώτος κανόνας είναι ο πιο σημαντικός και αυτός είναι ο κανόνας που κάνει το TDD διαφορετικό από τις κανονικές δοκιμές. Γράφετε ένα τεστ, ώστε να μπορείτε στη συνέχεια να γράψετε έναν κωδικό παραγωγής. Δεν γράφετε δοκιμή για να ελέγξετε τον κωδικό σας.

Ας δούμε ένα παράδειγμα.

// LeapYear.spec.jsdescribe('Leap year calculator', () => { it('should consider 1996 as leap', () => { expect(LeapYear.isLeap(1996)).toBe(true); });});

Ο παραπάνω κώδικας είναι ένα παράδειγμα του τρόπου εμφάνισης μιας δοκιμής σε JavaScript, χρησιμοποιώντας το πλαίσιο δοκιμών Jasmine. Δεν χρειάζεται να γνωρίζετε την Jasmine - αρκεί να καταλάβετε ότι it(...)είναι μια δοκιμασία και expect(...).toBe(...)είναι ένας τρόπος να κάνετε την Jasmine να ελέγξει αν κάτι είναι το αναμενόμενο.

Στην παραπάνω δοκιμή, έχω ελέγξει ότι η συνάρτηση LeapYear.isLeap(...)επιστρέφει trueγια το έτος 1996. Μπορεί να πιστεύετε ότι το 1996 είναι ένας μαγικός αριθμός και, επομένως, είναι μια κακή πρακτική. Δεν είναι. Στον κωδικό δοκιμής, οι μαγικοί αριθμοί είναι καλοί, ενώ στον κώδικα παραγωγής πρέπει να αποφεύγονται.

Αυτό το τεστ έχει πραγματικά κάποιες επιπτώσεις:

  • Το όνομα του υπολογιστή άλμα έτους είναι LeapYear
  • isLeap(...)είναι μια στατική μέθοδος του LeapYear
  • isLeap(...)παίρνει έναν αριθμό (και όχι έναν πίνακα, για παράδειγμα) ως όρισμα και επιστρέφει trueή false.

Είναι ένα τεστ, αλλά στην πραγματικότητα έχει πολλές επιπτώσεις! Χρειαζόμαστε μια μέθοδο για να πούμε εάν ένα έτος είναι έτος άλματος ή χρειαζόμαστε μια μέθοδο που επιστρέφει μια λίστα με τα έτη άλματος μεταξύ ημερομηνίας έναρξης και λήξης; Έχει σημασία το όνομα των στοιχείων; Αυτά είναι τα είδη των ερωτήσεων που πρέπει να έχετε κατά νου ενώ γράφετε δοκιμές στην κόκκινη φάση.

Σε αυτήν τη φάση, πρέπει να λάβετε αποφάσεις σχετικά με τον τρόπο χρήσης του κώδικα. Το βασίζετε σε αυτό που πραγματικά χρειάζεστε αυτή τη στιγμή και όχι σε αυτό που νομίζετε ότι μπορεί να χρειαστεί.

Εδώ έρχεται ένα άλλο λάθος: μην γράφετε πολλές λειτουργίες / τάξεις που νομίζετε ότι μπορεί να χρειαστείτε. Επικεντρωθείτε στη λειτουργία που εφαρμόζετε και σε αυτό που πραγματικά χρειάζεται. Το να γράφεις κάτι που δεν απαιτεί το χαρακτηριστικό είναι υπερ-μηχανική.

Τι γίνεται με την αφαίρεση; Θα το δούμε αργότερα, στη φάση του αντιδραστήρα.

Πράσινη φάση

Αυτή είναι συνήθως η ευκολότερη φάση, γιατί σε αυτή τη φάση γράφετε (παραγωγή) κώδικα. Εάν είστε προγραμματιστής, το κάνετε συνεχώς.

Εδώ έρχεται ένα άλλο μεγάλο λάθος: αντί να γράφετε αρκετό κώδικα για να περάσετε το κόκκινο τεστ, γράφετε όλους τους αλγορίθμους. Ενώ κάνετε αυτό, πιθανότατα σκεφτείτε ποια είναι η πιο αποτελεσματική εφαρμογή. Με τιποτα!

Σε αυτήν τη φάση, πρέπει να ενεργήσετε σαν προγραμματιστής που έχει μια απλή εργασία: γράψτε μια απλή λύση που κάνει το τεστ επιτυχίας (και κάνει το ανησυχητικό κόκκινο στην αναφορά δοκιμής να γίνει φιλικό πράσινο). Σε αυτήν τη φάση, επιτρέπεται να παραβιάζετε τις βέλτιστες πρακτικές και ακόμη και τον διπλό κώδικα. Η επανάληψη κώδικα θα αφαιρεθεί στη φάση του αντιδραστήρα

Γιατί όμως έχουμε αυτόν τον κανόνα; Γιατί δεν μπορώ να γράψω όλο τον κώδικα που είναι ήδη στο μυαλό μου; Για δύο λόγους:

  • Μια απλή εργασία είναι λιγότερο επιρρεπής σε σφάλματα και θέλετε να ελαχιστοποιήσετε σφάλματα.
  • Σίγουρα δεν θέλετε να συνδυάσετε κώδικα που είναι υπό δοκιμή με κώδικα που δεν είναι. Μπορείτε να γράψετε κώδικα που δεν είναι υπό δοκιμή (γνωστός και ως παλαιού τύπου), αλλά το χειρότερο πράγμα που μπορείτε να κάνετε είναι η ανάμειξη δοκιμασμένου και μη δοκιμασμένου κώδικα.

Τι γίνεται με τον καθαρό κώδικα; Τι γίνεται με την απόδοση; Τι γίνεται αν η σύνταξη κώδικα με κάνει να ανακαλύψω ένα πρόβλημα; Τι γίνεται με αμφιβολίες;

Η παράσταση είναι μια μακρά ιστορία και είναι εκτός του πεδίου αυτού του άρθρου. Ας πούμε απλώς ότι ο συντονισμός απόδοσης σε αυτήν τη φάση είναι, τις περισσότερες φορές, πρόωρη βελτιστοποίηση.

Η δοκιμαστική τεχνική ανάπτυξης παρέχει δύο άλλα πράγματα: μια λίστα υποχρεώσεων και τη φάση του αντιδραστήρα.

The refactor phase is used to clean up the code. The to-do list is used to write down the steps required to complete the feature you are implementing. It also contains doubts or problems you discover during the process. A possible to-do list for the leap year calculator could be:

Feature: Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400.
- divisible by 4- but not by 100- years divisible by 400 are leap anyway
What about leap years in Julian calendar? And years before Julian calendar?

The to-do list is live: it changes while you are coding and, ideally, at the end of the feature implementation it will be blank.

Refactor phase

In the refactor phase, you are allowed to change the code, while keeping all tests green, so that it becomes better. What “better” means is up to you. But there is something mandatory: you have to remove code duplication. Kent Becks suggests in his book that removing code duplication is all you need to do.

In this phase you play the part of a picky programmer who wants to fix/refactor the code to bring it to a professional level. In the red phase, you’re showing off your skills to your users. But in the refactor phase, you’re showing off your skills to the programmers who will read your implementation.

Removing code duplication often results in abstraction. A typical example is when you move two pieces of similar code into a helper class that works for both the functions/classes where the code has been removed.

For example the following code:

class Hello { greet() { return new Promise((resolve) => { setTimeout(()=>resolve('Hello'), 100); }); }}class Random { toss() { return new Promise((resolve) => { setTimeout(()=>resolve(Math.random()), 200); }); }}new Hello().greet().then(result => console.log(result));new Random().toss().then(result => console.log(result));

could be refactored into:

class Hello { greet() { return PromiseHelper.timeout(100).then(() => 'hello'); }}class Random { toss() { return PromiseHelper.timeout(200).then(() => Math.random()); }}class PromiseHelper { static timeout(delay) { return new Promise(resolve => setTimeout(resolve, delay)); }}const logResult = result => console.log(result);new Hello().greet().then(logResult);new Random().toss().then(logResult);

As you can see, in order to remove thenew Promise and setTimeout code duplication, I created a PromiseHelper.timeout(delay) method, which serves both Hello and Random classes.

Just keep in mind that you cannot move to another test unless you’ve removed all the code duplication.

Final considerations

In this section I will try to answer to some common questions and misconceptions about Test Drive Development.

  • T.D.D. requires much more time than “normal” programming!

What actually requires a lot of time is learning/mastering TDD as well as understanding how to set up and use a testing environment. When you are familiar with the testing tools and the TDD technique, it actually doesn’t require more time. On the contrary, it helps keep a project as simple as possible and thus saves time.

  • How many test do I have to write?

The minimum amount that lets you write all the production code. The minimum amount, because every test slows down refactoring (when you change production code, you have to fix all the failing tests). On the other hand, refactoring is much simpler and safer on code under tests.

  • With Test Driven Development I don’t need to spend time on analysis and on designing the architecture.

This cannot be more false. If what you are going to implement is not well-designed, at a certain point you will think “Ouch! I didn’t consider…”. And this means that you will have to delete production and test code. It is true that TDD helps with the “Just enough, just in time” recommendation of agile techniques, but it is definitely not a substitution for the analysis/design phase.

  • Should test coverage be 100%?

No. As I said earlier, don’t mix up tested and untested code. But you can avoid using TDD on some parts of a project. For example I don’t test views (although a lot of frameworks make UI testing easy) because they are likely to change often. I also ensure that there is very a little logic inside views.

  • I am able to write code with very a few bugs, I don’t need testing.

You may able to to that, but is the same consideration valid for all your team members? They will eventually modify your code and break it. It would be nice if you wrote tests so that a bug can be spotted immediately and not in production.

  • TDD works well on examples, but in a real application a lot of the code is not testable.

I wrote a whole Tetris (as well as progressive web apps at work) using TDD. If you test first, code is clearly testable. It is more a matter of understanding how to mock dependencies and how to write simple but effective tests.

  • Tests should not be written by the developers who write the code, they should be written by others, possibly QA people.

Εάν μιλάτε για δοκιμή της αίτησής σας, ναι, είναι καλή ιδέα να ζητήσετε από άλλους ανθρώπους να δοκιμάσουν τι έκανε η ομάδα σας. Εάν μιλάτε για τη σύνταξη κώδικα παραγωγής, τότε αυτή είναι η λανθασμένη προσέγγιση.

Τι έπεται?

Αυτό το άρθρο αφορούσε τη φιλοσοφία και τις κοινές παρανοήσεις του TDD. Σκοπεύω να γράψω άλλα άρθρα στο TDD όπου θα δείτε πολύ κώδικα και λιγότερες λέξεις. Εάν ενδιαφέρεστε για το πώς να αναπτύξετε το Tetris χρησιμοποιώντας TDD, μείνετε συντονισμένοι!