Jednostavan uvod u Test Driven Development s Pythonom

Ja sam samouki početni programer koji zna pisati jednostavne aplikacije. Ali moram priznati. Nemoguće je sjetiti se kako je sve u mojoj glavi međusobno povezano.

Ta se situacija pogoršava ako se vratim kodu koji sam napisao nakon nekoliko dana. Ispostavilo se da bi se ovaj problem mogao prevladati slijedeći metodologiju Test Driven Development (TDD).

Što je TDD i zašto je važan?

Laički rečeno, TDD preporučuje pisanje testova koji bi provjerili funkcionalnost vašeg koda prije nego što napišete stvarni kôd. Tek kad ste zadovoljni svojim testovima i značajkama koje testira, započinjete s pisanjem stvarnog koda kako biste zadovoljili uvjete nametnute testom koji bi im omogućili da prođu.

Slijedom ovog postupka osigurava se da pažljivo planirate kôd koji napišete kako biste prošli ove testove. To također sprječava mogućnost odlaganja pisanja testova na kasniji datum, jer se oni možda neće smatrati potrebnima u usporedbi s dodatnim značajkama koje bi se mogle stvoriti za to vrijeme.

Testovi vam daju i samopouzdanje kada započnete s refaktoriranjem koda, jer je veća vjerojatnost da ćete uhvatiti bugove zbog trenutnih povratnih informacija kada se testovi izvrše.

Kako započeti?

Za započinjanje pisanja testova na Pythonu koristit ćemo unittestmodul koji dolazi s Pythonom. Da bismo to učinili, kreiramo novu datoteku mytests.pykoja će sadržavati sve naše testove.

Počnimo s uobičajenim "zdravo svijetu":

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

Primijetite da uvezimo helloworld()funkciju iz mycodedatoteke. U datoteku mycode.pyćemo u početku uključiti donji kod koji stvara funkciju, ali u ovoj fazi ne vraća ništa:

def hello_world(): pass

Pokretanje python mytests.pyće generirati sljedeći izlaz u naredbenom retku:

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)

To jasno ukazuje na to da test nije uspio, što se i očekivalo. Srećom, testove smo već napisali pa znamo da će uvijek biti tu za provjeru ove funkcije, što nam daje povjerenje u uočavanju potencijalnih bugova u budućnosti.

Da bismo osigurali da kod prolazi, dopuštamo promjenu mycode.pyu sljedeće:

def hello_world(): return 'hello world'

python mytests.pyPonovnim pokretanjem u naredbenom retku dobivamo sljedeći izlaz:

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

Čestitamo! Upravo ste napisali svoj prvi test. Krenimo sada na malo teži izazov. Stvorit ćemo funkciju koja će nam omogućiti stvaranje prilagođenog razumijevanja numeričkog popisa u Pythonu.

Počnimo s pisanjem testa za funkciju koja bi stvorila popis određene duljine.

U datoteci mytests.pybi ovo bila metoda 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)

Ovo bi testiralo da funkcija create_num_listvraća popis duljine 10. Stvorimo funkciju create_num_listu mycode.py:

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

Pokretanje python mytests.pyće generirati sljedeći izlaz u naredbenom retku:

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)

To je u skladu s očekivanjima, pa idemo naprijed i promjene funkcija create_num_listu mytest.pykako bi proći test:

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

Izvršenje python mytests.pyna naredbenom retku pokazuje da je i drugi test također položen:

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