Простое введение в разработку через тестирование с помощью 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.