Лесно въведение в тестовото развитие с Python

Аз съм самоук начинаещ разработчик, който умее да пише прости приложения. Но трябва да направя признание. Невъзможно е да си спомня как всичко е взаимосвързано в главата ми.

Тази ситуация се влошава, ако след няколко дни се върна към написания от мен код. Оказва се, че този проблем може да бъде преодолян, като се следва методологията за тестово развитие (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

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.