Метка: pyway

Юнит-тесты

Программисты так или иначе тестируют свои программы. В простых случаях можно запустить программу несколько раз и проверить результаты. А если вы внесли изменение? Нужно проделать эту рутинную работу еще раз и не ошибиться самому. В сложных программах это просто нереально. Естественно, этот процесс автоматизируется. Сложная программа состоит из отдельных классов, функций и модулей, каждый из которых отвечает за свой ограниченный круг функциональности. Поэтому разумно написать несколько небольших программок, которые будут подавать на вход разнообразные типичные комбинации данных и сравнивать с ожидаемым результатом. Это и будут юнит-тесты. После изменения кода запуск юнит-тестов покажет, не сломалось ли поведение программы.

В Python поставляется модуль unittest, который облегчает написание тестов:

  • Обнаружение и автоматическое исполнение тестов
  • Настройка теста и его завершение
  • Группирование тестов
  • Статистика тестирования

Чтобы создать тестовый случай, нужно создать класс, отнаследованный от unittest.TestCase. А внутри этого класса можно добавить несколько методов, начинающихся со слова test. Каждый из этих методов должен тестировать какой-то из аспектов кода. Для примера мы тестируем свойства строк Python: сложение строк (test_sum) и преобразование к нижнему регистру (test_lower):

import unittest

class StringTestCase(unittest.TestCase):
    def test_sum(self):
        self.assertEqual("" + "", "")
        self.assertEqual("foo" + "bar", "foobar")

    def test_lower(self):
        self.assertEqual("FOO".lower(), "foo")
        self.assertTrue("foo".islower())
        self.assertFalse("Bar".islower())

Самые распространенные проверки:

self.assertEqual – непосредственно проверяет, чтобы первый аргумент равнялся второму. Если это будет не так, то тест будет провален, и появится сообщение о том, что и где пошло не так.

self.assertTrue – ожидает, что аргумент будет эквивалентен правде (True), а self.assertFalse – проверяет на ложь (False).

Запуск делается либо непосредственно из самой программы:

if name == '__main__':
    unittest.main()

А можно из консоли:

python -m unittest my_test.py

Модуль unittest сам найдет все тестовые случаи и выполнит в них все тестовые функции.

Скрин результата запуска тестов

Фикстуры

Тестовые фикстуры (test fixtures) – особые условия, которые создаются для выполнения тестов. Сюда могут входить такие вещи:

  • Подготовка тестовых данных
  • Создание подключений к БД, сервисам и т.п.
  • Создание заглушек (mock) для имитации компонентов программы
  • Другие действия по поддержке рабочего окружения для проведения теста

Пример: у вас программа, которая вычисляет вычисляет число π до n-знака, и вам нужно протестировать, как будет выведен на экран миллионнный знак. Вы же не будете в тесте ждать вычисления всех предыдущих 999,999 знаков часами, а просто загрузите какие-то данные в память, чтобы создать условия, как будто мы уже на миллионом знаке.

В unittest фикстуры можно создавать на уровне модуля с тестами, отдельного класса (от unittest.TestCase) и каждого метода в классе теста. 

Метод setUp() вызывается перед каждым вызовом метода test* в классе тестового случая.

Классовый метод setUpClass() вызывается один раз перед запуском тестов в классе тестового случая.

Функция setUpModule() вызывается перед выполнением тестовых случаев в этом модуле.

У них есть пары, предназначенные для освобождения ресурсов (закрытия соединений, удаления временных файлов и т.п.):

tearDown() – после каждого метода-теста в классе.
tearDownClass() – после всех тестов в классе.
tearDownModule() – после всех классов в модуле.

📎 В примере изучим порядок вызовов этих функций:

import unittest

class StringTestCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print(' - set up class')

    def setUp(self):
        print(' - - set up method')
        self.foo = "foo"
        self.bar = "bar"

    def test_sum(self):
        self.assertEqual(self.foo + self.bar, "foobar")

    def test_lower(self):
        self.assertTrue(self.foo.islower())

    def tearDown(self):
        print(' - - tear down method')

    @classmethod
    def tearDownClass(cls):
        print(' - tear down class')

def setUpModule():
    print('set up module')

def tearDownModule():
    print('tear down module')

if name == '__main__':
    unittest.main()

Даст такую схему вызовов:

set up module
 - set up class 
 - - set up method
 - - tear down method
 - - set up method
 - - tear down method
 - tear down class
tear down module

Даже если в одной из этих или тестовых функций произошло исключение, то прочие методы tearDown*() будут все равно запущены, чтобы освобождение ресурсов произошло корректно.

Пропуск тестов

Модуль unittest поддерживает пропуск отдельных тестовых методов и целых тестовых классов. Пропускают тесты, если нет нужного ресурса для теста, тест предназначен только для отдельных платформ или версий библиотек и т.п. Способы пропустить тест:

  • @unittest.skip("причина") – всегда пропускать тест.
  • @unittest.skipIf(условие, "причина") – пропускать тест, если условие сработало (True).
  • @unittest.skipUnless(условие, "причина") – пропускать тест, если условие НЕ сработало (False).
  • self.skipTest("причина") – если нужно остановить выполнение метода, выйти из него и не учитывать его в результатах. Так же может быть вызван в методе setUp(), который вызывается перед каждым тестовым методом.

📎 Пример:

class MyTestCase(unittest.TestCase):
    @unittest.skip("всегда пропустить")
    def test_nothing(self):
        self.fail("не случится")

    @unittest.skipIf(mylib.__version__ < (1, 3),
                     "эта версия библиотеки не поддерживается")
    def test_format(self):
        # этот тест работает только для определенных версий 
        pass

    @unittest.skipUnless(sys.platform.startswith("win"), "надо Windows")
    def test_windows_support(self):
        # тест работает только на Windows
        pass

    def test_maybe_skipped(self):
        if not external_resource_available():
            self.skipTest("ресурс недоступен")
        # код дальше будет тестировать, если ресурс доступен
        pass

📎 Пример пропуска класса:

@unittest.skip("как пропустить класс")
class MySkippedTestCase(unittest.TestCase):
    def test_not_run(self):
        pass

Вы можете написать свой декоратор. Например, данный декоратор пропускает тест, если объект obj не имеет атрибут attr:

def skipUnlessHasattr(obj, attr):
    if hasattr(obj, attr):
        return lambda func: func
    return unittest.skip("{!r} не имеет {!r}".format(obj, attr))

class SkipAttrTestCase(unittest.TestCase):
    @skipUnlessHasattr(mylib, "foofunc")
    def test_with_foofunc():
        # у mylib нет атрибута foofunc, тест будет пропущен
        pass

Еще один декоратор @unittest.expectedFailure говорит системе тестирования, что следующий метод должен провалиться (один из self.assert должен не сработать). Таким образом, разработчик говорит, что он осведомлен, что данный тест пока проваливается, и в будущем к этому примут меры.

class ExpectedFailureTestCase(unittest.TestCase):
    @unittest.expectedFailure
    def test_fail(self):
        self.assertEqual(1, 0, "сломано")

В конце выполнения будут счетчики пропусков и ожидаемых провалов тестов:

OK (skipped=5, expected failures=1)

Код примеров онлайн.

Проверки

В первой части мы обсудили методы проверки assertEqual, assertTrue и assertFalse, так как они самые распространенные на практике. Вообще достаточно одного assertTrue. Действительно, одно и тоже:

assertNotIn(item, list)
assertTrue(item not in list)

Поэтому не буду особо акцентировать внимание на каждом из методов, а прикреплю общую таблицу. Помимо данных для проверки, все методы могут быть дополнены сообщением, которое будет выведено в отчет, если проверка провалилась:

self.assertEqual(2 + 2, 5, "я не учил математику")

Однако, стоит упомянуть метод self.assertRaises(SomeException), который проверяет, возбуждает ли код нужное исключение. Обычно он применяется как контекст-менеджер (с with). 

📎 Пример: деление на 0 должно бросать исключение ZeroDivisionError:

import unittest

def my_div(a, b):
    return a // b

class MyDivTestCase(unittest.TestCase):
    def test_1(self):
        self.assertEqual(my_div(10, 2), 5)

        # при делении на 0 ждем исключение:
        with self.assertRaises(ZeroDivisionError):
            my_div(7, 0)

        # или так: исключение, ф-ция, аргументы
        self.assertRaises(ZeroDivisionError, my_div, 5, 0)

unittest.main()

Если из исключения нужно извлечь данные (к примеру, код ошибки), то делают так:

with self.assertRaises(SomeException) as cm:
    do_something()

self.assertEqual(cm.exception.error_code, 3)
Картинка с таблицей проверок и что они проверяют

PyTest

Ранее мы обсуждали тестирование средствами встроенного модуля unittest. Естественно, есть и сторонние библиотеки для тестирования. Например, библиотека PyTest предоставляет более лаконичный и удобный инструментарий для написания тестов. Однако, ее нужно установить:

pip install pytest

Преимущества PyTest:

  • Краткий и красивый код
  • Только один стандартный assert
  • Подробный отчет
  • Разнообразие фикстур на всех уровнях
  • Плагин и интеграции с другими системами

Сравните этот код с кодом из предыдущих постов про unittest:

import pytest

def setup_module(module):
    #init_something()
    pass

def teardown_module(module):
    #teardown_something()
    pass

def test_upper():
    assert 'foo'.upper() == 'FOO'
    
def test_isupper():
    assert 'FOO'.isupper()
    
def test_failed_upper():
    assert 'foo'.upper() == 'FOo'

Для тестов можно применять и классы (как в unittest), так и отдельные функции.

Запускать тесты тоже просто. В окружении, где установлен pytest, появится команда py.test. Из терминала пишем:

py.test my_test_cases.py  

py.test обнаружит и выполнит тесты из этого файла.

Есть очень хорошая статья на Хабре про PyTest на русском, не вижу смысла дублировать ее сюда, а просто оставлю ссылку.

😈 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

​​Размер окна терминала

Для оформления информации в терминале часто нужно знать размеры окна терминала (количество колонок и строк). Во встроенном модуле shutil можно найти функцию get_terminal_size, которая возвращает именованный кортеж:

>>> shutil.get_terminal_size()
os.terminal_size(columns=208, lines=25)

Или

>>> cols, lines = shutil.get_terminal_size()
>>> cols, lines
(208, 25)

Или

>>> tsz = shutil.get_terminal_size()
>>> tsz.columns, tsz.lines
(208, 25)
Текст с разделителями их дефисов

Например, сделаем разделитель с заголовком, как на фото.

1. Будем форматировать по центру значение в строку с заданной длинной, а пустые места заполнить каким-то символом. Для этого нужен особый формат:

>>> '{:^10}'.format('love')
'   love   '
>>> '{:-^10}'.format('life')
'---life---'

Знак после двоеточия – заполнитель (если его нет, то пробел); а число после крышечки – желаемая ширина строки. Крышечка указывает, что форматирование будет по центру.

2. Так как число неизвестно заранее, то его тоже надо вставить с помощью format, предварительно экранировав фигурные скобки (двойная фигурная скобка в формате воспринимается как соотвествующий символ, а не как место для подстановки):

>>> '{{:-^{}}}'.format(10)
'{:-^10}'
>>> '{{:-^{}}}'.format(10).format('love')
'---love---'
>>> '{{:-^{}}}'.format(shutil.get_terminal_size().columns).format('love')
'---------------------------love----------------------------'

3. Текст, что по центру сделаем заглавным, а также каждый символ отделим пробелами, чтобы заголовок казался заметнее:

>>> ' '.join('love'.upper())
'L O V E'
>>> ' ' + ' '.join('love'.upper()) + ' '
' L O V E '

4. Соеденим все вместе в однострочник, добавив print к итоговой строке:

def sep(s): 
    print('{{:-^{}}}'.format(shutil.get_terminal_size().columns).format(' ' + ' '.join(str(s).upper()) + ' '))

Хочу уточнить, что shutil.get_terminal_size() не всегда способна определить размер терминала. Например, когда собственно и нет никакого окна терминала, а лишь есть поток вывода как при выводе в файл или в канал. У потока вывода нет таких характеристик как размер окна. При выполнении функции в среде PyCharm функция вернет размер по умолчанию (80 на 25), и разделитель будет не на всю ширину области вывода, если она шире 80 символов.

😈 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Quine на Python

Программисты тоже умеют развлекаться, так что давайте сегодня развлечемся и напишем quine (квайн). Квайн – это такая программа, которая выводит на экран свой же код, ни больше, ни меньше. Сразу договоримся, что пустая программа на Python, которая ничего не выводит, не считается квайном; это не интересно.

В Python у нас есть чудо-переменная, которая хранит путь к текущему интерпретируемому файлу, поэтому можно сделать так:

print(open(__file__).read())

Эта программа открывает свой же файл, читает и печатает его целиком. Но это жульничество, потому что в квайнах не принято читать файлы. Хорошо, а что если назвать файл print(__file__), записать в него print(__file__) и выполнить python "print(__file__)". Будет работать, но можешь вот без этих трюков, чисто кодом? Да без проблем!

Нам нужно что-то печатать, значит берем print:

>>> print('?')
?

Программа начинается с print…, значит и печатать будем тоже самое:

>>> print('print()')
print()

Не получается, потому что у нас тут уже два print да кавычки, а печатается только один. Так можно плодить print до бесконечности, но все равно не будет хватать одного в выводе. Будем решать поэтапно. Давайте заведем переменную s с кодом нашей программы.

>>> s='print()';print(s)
print()

Но код теперь начинается с s=, исправим:

>>> s='s=?;print(s)';print(s)
s=?;print(s)

Смотрите, уже похоже, осталось только на место знака вопроса воткнуть содержимое строки s из оригинального кода. Это самый важный момент. Используем format, а точнее s.format(s), который в определенном месте строки s вставит саму же строку s, таким образом, мы «разрываем рекурсию»:

>>> s='s={};print(s)';print(s.format(s))
s=s={};print(s);print(s)

Отлично! Но тут два недостатка: во-первых, не забыть добавить s.format(s) в саму строку s:

>>> s='s={};print(s.format(s))';print(s.format(s))
s=s={};print(s.format(s));print(s.format(s))

Во-вторых, нужно вернуть на место кавычки. Не зря я недавно рассказывал о флагах преобразования строк. Используем флаг {!r} в формате, чтобы вывести repr(s), который для строк содержит одинарные кавычки:

>>> s='s={!r};print(s.format(s))';print(s.format(s))
s='s={!r};print(s.format(s))';print(s.format(s))

Ура! Квайн готов и работает!

Вы можете сделать квайн короче, используя другой стиль форматирования строк через процент: {!r} заменяется на %r, s.format(s) на s%s, плюс экранируется процент внутри самой строки s%%s (%% понимается как сам знак процента, а не как место для подстановки):

>>> s='s=%r;print(s%%s)';print(s%s)
s='s=%r;print(s%%s)';print(s%s)

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Полезные декораторы

Как и обещал, приведу список полезных декораторов. Среди них как стандартные поставляемые вместе с Python, так и декораторы из других библиотек и исходные коды прочих интересных декораторов.

Начнем с самых известных.

Coffee, tee or me

Итератор, как известно, выдает значения по одному (например, методом next), и его нельзя «отмотать» назад. Это означает, что получать все подряд значения из итератора может только один потребитель. Однако, если несколько потребителей хотят читать из одного итератора, его можно разделить с помощью функции itertools.tee. Кстати, tee переводится как тройник (тройник похож на букву Т).

tee принимает исходный итератор и число – количество новых итераторов, на которые разделится исходный, а возвращает кортеж из новых итераторов. При этом извлечение значений из одного из итераторов не влияет на остальные.

📎 Пример:

from itertools import tee

def source():
    for i in range(5):
        print(f'next is {i}')
        yield i

# три потребителя
coffee, tea, me = tee(source(), 3)

# первый берет два числа
next(coffee); next(coffee)
# второй одно
next(tea)
# третий - все
for i in me:
    ...

Вывод:

next is 0
next is 1
next is 2
next is 3
next is 4

Видно, что все значения были извлечены из исходного оператора по одному разу, но каждый потребитель получил их столько раз, сколько хотел.

Предостережения:

1. Исходный итератор не следует итерировать, иначе производные итераторы лишатся некоторых значений. 

2. Механизм tee таков, что он хранит в памяти извлеченные элементы, чтобы остальные потребители могли их получить, даже если исходный итератор уже сместился. Поэтому, если элементов много или они большие, это может серьезно повлиять на расход памяти.

Полный пример кода (плюс моя реализация tee на скорую руку) – по ссылке.

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈