Μια απλή εισαγωγή στο Test Driven Development με την Python

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

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

Τι είναι το TDD και γιατί είναι σημαντικό;

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

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

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

Πως να ξεκινήσεις?

Για να αρχίσουμε να γράφουμε δοκιμές στο Python θα χρησιμοποιήσουμε την unittestενότητα που συνοδεύει το Python. Για να το κάνουμε αυτό, δημιουργούμε ένα νέο αρχείο mytests.py, το οποίο θα περιέχει όλες τις δοκιμές μας.

Ας ξεκινήσουμε με το συνηθισμένο «γεια σας κόσμο»:

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world')

Παρατηρήστε ότι εισάγουμε helloworld()συνάρτηση από mycodeαρχείο. Στο αρχείο mycode.pyθα συμπεριλάβουμε αρχικά τον παρακάτω κώδικα, ο οποίος δημιουργεί τη συνάρτηση αλλά δεν επιστρέφει τίποτα σε αυτό το στάδιο:

def hello_world(): pass

Η εκτέλεση python mytests.pyθα δημιουργήσει την ακόλουθη έξοδο στη γραμμή εντολών:

F
====================================================================
FAIL: test_hello (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 7, in test_hello
self.assertEqual(hello_world(), 'hello world')
AssertionError: None != 'hello world'
--------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)

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

Για να διασφαλίσετε ότι ο κωδικός περνά, ας αλλάξουμε mycode.pyτα εξής:

def hello_world(): return 'hello world'

Εκτελώντας python mytests.pyξανά έχουμε την ακόλουθη έξοδο στη γραμμή εντολών:

.
--------------------------------------------------------------------
Ran 1 test in 0.000s
OK

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

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

Στο αρχείο mytests.pyαυτό θα ήταν μια μέθοδος test_custom_num_list:

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world') def test_custom_num_list(self): self.assertEqual(len(create_num_list(10)), 10)

Αυτό θα δοκιμάσει ότι η συνάρτηση create_num_listεπιστρέφει μια λίστα μήκους 10. Ας δημιουργήσουμε συνάρτηση create_num_listσε mycode.py:

def hello_world(): return 'hello world'
def create_num_list(length): pass

Η εκτέλεση python mytests.pyθα δημιουργήσει την ακόλουθη έξοδο στη γραμμή εντολών:

E.
====================================================================
ERROR: test_custom_num_list (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 14, in test_custom_num_list
self.assertEqual(len(create_num_list(10)), 10)
TypeError: object of type 'NoneType' has no len()
--------------------------------------------------------------------
Ran 2 tests in 0.000s
FAILED (errors=1)

Αυτή είναι η αναμενόμενη, οπότε ας προχωρήσει και λειτουργία αλλαγής create_num_listσε mytest.pyπροκειμένου να περάσει το τεστ:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]

Η εκτέλεση python mytests.pyστη γραμμή εντολών δείχνει ότι έχει περάσει και η δεύτερη δοκιμή:

..
--------------------------------------------------------------------
Ran 2 tests in 0.000s
OK

Let’s now create a custom function that would transform each value in the list like this: const * ( X ) ^ power . First let’s write the test for this, using method test_custom_func_ that would take value 3 as X, take it to the power of 3, and multiply by a constant of 2, resulting in the value 54:

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world')
def test_custom_num_list(self): self.assertEqual(len(create_num_list(10)), 10) def test_custom_func_x(self): self.assertEqual(custom_func_x(3,2,3), 54)

Let’s create the function custom_func_x in the file mycode.py:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): pass

As expected, we get a fail:

F..
====================================================================
FAIL: test_custom_func_x (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 17, in test_custom_func_x
self.assertEqual(custom_func_x(3,2,3), 54)
AssertionError: None != 54
--------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1)

Updating function custom_func_x to pass the test, we have the following:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): return const * (x) ** power

Running the tests again we get a pass:

...
--------------------------------------------------------------------
Ran 3 tests in 0.000s
OK

Finally, let’s create a new function that would incorporate custom_func_x function into the list comprehension. As usual, let’s begin by writing the test. Note that just to be certain, we include two different cases:

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world')
def test_custom_num_list(self): self.assertEqual(len(create_num_list(10)), 10)
def test_custom_func_x(self): self.assertEqual(custom_func_x(3,2,3), 54)
def test_custom_non_lin_num_list(self): self.assertEqual(custom_non_lin_num_list(5,2,3)[2], 16) self.assertEqual(custom_non_lin_num_list(5,3,2)[4], 48)

Now let’s create the function custom_non_lin_num_list in mycode.py:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): return const * (x) ** power
def custom_non_lin_num_list(length, const, power): pass

As before, we get a fail:

.E..
====================================================================
ERROR: test_custom_non_lin_num_list (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 20, in test_custom_non_lin_num_list
self.assertEqual(custom_non_lin_num_list(5,2,3)[2], 16)
TypeError: 'NoneType' object has no attribute '__getitem__'
--------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (errors=1)

In order to pass the test, let’s update the mycode.py file to the following:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): return const * (x) ** power
def custom_non_lin_num_list(length, const, power): return [custom_func_x(x, const, power) for x in range(length)]

Running the tests for the final time, we pass all of them!

....
--------------------------------------------------------------------
Ran 4 tests in 0.000s
OK

Congrats! This concludes this introduction to testing in Python. Make sure you check out the resources below for more information on testing in general.

The code is available here on GitHub.

Useful resources for further learning!

Web resources

Below are links to some of the libraries focusing on testing in Python

25.3. unittest - Unit testing framework - Python 2.7.14 documentation

The Python unit testing framework, sometimes referred to as "PyUnit," is a Python language version of JUnit, by Kent…docs.python.orgpytest: helps you write better programs - pytest documentation

Το πλαίσιο διευκολύνει τη σύνταξη μικρών δοκιμών, αλλά κλίμακες για την υποστήριξη σύνθετων λειτουργικών δοκιμών για εφαρμογές και… docs.pytest.org Καλώς ήλθατε στην υπόθεση! - Τεκμηρίωση υπόθεσης 3.45.2

Λειτουργεί δημιουργώντας τυχαία δεδομένα που ταιριάζουν με τις προδιαγραφές σας και ελέγχοντας ότι η εγγύησή σας εξακολουθεί να ισχύει σε αυτό… hypothesis.readthedocs.io unittest2 1.1.0: Ευρετήριο πακέτων Python

Οι νέες δυνατότητες στο unittest υποστηρίζονται στο Python 2.4+. pypi.python.org

Βίντεο YouTube

Εάν προτιμάτε να μην διαβάσετε, σας συνιστούμε να παρακολουθήσετε τα παρακάτω βίντεο στο YouTube.