Eine einfache Einführung in die testgetriebene Entwicklung mit Python

Ich bin ein autodidaktischer Anfänger, der einfache Apps schreiben kann. Aber ich muss ein Geständnis machen. Es ist unmöglich sich zu erinnern, wie alles in meinem Kopf miteinander verbunden ist.

Diese Situation wird noch schlimmer, wenn ich nach einigen Tagen zu dem Code zurückkehre, den ich geschrieben habe. Es stellt sich heraus, dass dieses Problem durch eine TDD-Methode (Test Driven Development) behoben werden kann.

Was ist TDD und warum ist es wichtig?

In Laienbegriffen empfiehlt TDD, Tests zu schreiben, die die Funktionalität Ihres Codes überprüfen, bevor Sie den eigentlichen Code schreiben. Erst wenn Sie mit Ihren Tests und den getesteten Funktionen zufrieden sind, beginnen Sie, den eigentlichen Code zu schreiben, um die durch den Test auferlegten Bedingungen zu erfüllen, unter denen sie bestanden werden können.

Das Befolgen dieses Vorgangs stellt sicher, dass Sie den von Ihnen geschriebenen Code sorgfältig planen, um diese Tests zu bestehen. Dies verhindert auch, dass das Schreiben von Tests auf einen späteren Zeitpunkt verschoben wird, da sie im Vergleich zu zusätzlichen Funktionen, die während dieser Zeit erstellt werden könnten, möglicherweise nicht als notwendig erachtet werden.

Tests geben Ihnen auch Sicherheit, wenn Sie mit der Umgestaltung von Code beginnen, da Sie aufgrund des sofortigen Feedbacks bei der Ausführung von Tests eher Fehler erkennen.

Wie man anfängt?

Um mit dem Schreiben von Tests in Python zu beginnen, verwenden wir das unittestmit Python gelieferte Modul. Dazu erstellen wir eine neue Datei mytests.py, die alle unsere Tests enthält.

Beginnen wir mit der üblichen „Hallo Welt“:

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

Beachten Sie, dass wir helloworld()Funktionen aus mycodeDateien importieren . In die Datei mycode.pywird zunächst nur der folgende Code eingefügt, der die Funktion erstellt, aber zu diesem Zeitpunkt nichts zurückgibt:

def hello_world(): pass

Beim Ausführen python mytests.pywird die folgende Ausgabe in der Befehlszeile generiert:

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)

Dies zeigt deutlich, dass der Test fehlgeschlagen ist, was erwartet wurde. Glücklicherweise haben wir die Tests bereits geschrieben, sodass wir wissen, dass diese Funktion immer überprüft werden kann, was uns das Vertrauen gibt, potenzielle Fehler in Zukunft zu erkennen.

Um sicherzustellen, dass der Code übergeben wird, ändern Sie mycode.pyFolgendes:

def hello_world(): return 'hello world'

Beim python mytests.pyerneuten Ausführen erhalten wir die folgende Ausgabe in der Befehlszeile:

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

Glückwunsch! Sie haben gerade Ihren ersten Test geschrieben. Kommen wir nun zu einer etwas schwierigeren Herausforderung. Wir werden eine Funktion erstellen, mit der wir ein benutzerdefiniertes numerisches Listenverständnis in Python erstellen können.

Beginnen wir mit dem Schreiben eines Tests für eine Funktion, mit der eine Liste mit einer bestimmten Länge erstellt wird.

In der Datei mytests.pywäre dies eine Methode 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)

Dies würde testen, ob die Funktion create_num_listeine Liste der Länge 10 zurückgibt. Erstellen wir eine Funktion create_num_listin mycode.py:

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

Beim Ausführen python mytests.pywird die folgende Ausgabe in der Befehlszeile generiert:

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)

Dies wird als erwartet, so geht sie voran und Änderungsfunktion create_num_listin , mytest.pyum den Test zu bestehen:

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

Die Ausführung python mytests.pyin der Befehlszeile zeigt, dass der zweite Test nun ebenfalls bestanden wurde:

..
--------------------------------------------------------------------
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

The framework makes it easy to write small tests, yet scales to support complex functional testing for applications and…docs.pytest.orgWelcome to Hypothesis! - Hypothesis 3.45.2 documentation

It works by generating random data matching your specification and checking that your guarantee still holds in that…hypothesis.readthedocs.iounittest2 1.1.0 : Python Package Index

The new features in unittest backported to Python 2.4+.pypi.python.org

YouTube videos

If you prefer not to read, I recommend watching the following videos on YouTube.