Egyszerű bevezetés a tesztvezérelt fejlesztéshez a Pythonnal

Autodidakta kezdő fejlesztő vagyok, aki képes egyszerű alkalmazásokat írni. De be kell vallanom egy vallomást. Lehetetlen emlékezni arra, hogy minden összefügg egymással a fejemben.

Ez a helyzet még rosszabbá válik, ha néhány nap múlva visszatérek az általam írt kódra. Kiderült, hogy ezt a problémát meg lehet oldani egy tesztvezérelt fejlesztés (TDD) módszertanának követésével.

Mi a TDD és miért fontos?

A laikusok szerint a TDD olyan tesztek írását javasolja, amelyek a tényleges kód megírása előtt ellenőrzik a kód működését. Csak akkor, ha elégedett a tesztjeivel és az általa tesztelt funkciókkal, akkor kezdje el írni a tényleges kódot annak érdekében, hogy megfeleljen a teszt által előírt feltételeknek, amelyek lehetővé teszik számukra a teljesítést.

Ennek a folyamatnak a betartása biztosítja, hogy gondosan megtervezze az Ön által írt kódot a tesztek teljesítéséhez. Ez megakadályozza a tesztek írásának későbbi elhalasztásának lehetőségét is, mivel előfordulhat, hogy ezeket nem tartják szükségesnek az ezalatt létrehozható további szolgáltatásokhoz képest.

A tesztek magabiztosságot is adnak, amikor elkezdenek refaktorozni, mivel a tesztek végrehajtásakor azonnali visszajelzés miatt nagyobb valószínűséggel fog hibákat elkapni.

Hogy kezdjed?

A tesztek Pythonban való megírásának megkezdéséhez a Pythonhoz mellékelt unittestmodult használjuk . Ehhez létrehozunk egy új fájlt mytests.py, amely tartalmazza az összes tesztünket.

Kezdjük a szokásos „helló világgal”:

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

Figyelje meg, hogy a helloworld()függvényt mycodefájlból importáljuk . A fájlban mycode.pykezdetben csak az alábbi kódot fogjuk tartalmazni, amely létrehozza a függvényt, de ebben a szakaszban nem ad vissza semmit:

def hello_world(): pass

A futás python mytests.pya következő kimenetet generálja a parancssorban:

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)

Ez egyértelműen jelzi, hogy a teszt nem sikerült, amire számítani lehetett. Szerencsére már megírtuk a teszteket, így tudjuk, hogy mindig ott lesz, hogy ellenőrizze ezt a funkciót, ami magabiztosságot ad a jövőben potenciális hibák észlelésében.

A kód átadásának biztosítása érdekében váltson mycode.pya következőkre:

def hello_world(): return 'hello world'

python mytests.pyÚjra futva a következő kimenetet kapjuk a parancssorba:

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

Gratula! Most írta meg az első tesztjét. Térjünk át egy kissé nehezebb kihívásra. Létrehozunk egy olyan függvényt, amely lehetővé teszi számunkra, hogy egyéni numerikus listát készítsünk a Pythonban.

Kezdjük azzal, hogy írunk egy tesztet egy függvényhez, amely létrehoz egy meghatározott hosszúságú listát.

A fájlban mytests.pyez egy módszer 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)

Ez teszteli, hogy a függvény create_num_listvisszaad egy hosszú 10. Hozzunk létre funkciója create_num_lista mycode.py:

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

A futás python mytests.pya következő kimenetet generálja a parancssorban:

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)

Ez a várakozásnak megfelelően, akkor menjünk előre, és megváltozik a funkciója create_num_listaz mytest.py, hogy adja át a vizsgálat:

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

python mytests.pyA parancssoron történő végrehajtás megmutatja, hogy a második teszt is sikeresen teljesült:

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