Python. Тестирование. unittest

Введение

Написание тестов - тема, которую поначалу многие игнорируют, такой вывод я делаю в первую очередь по себе, и по некоторым людям, от которых я слышал что-либо касательно тестирования. Мне было немного лень садиться за изучение библиотек для тестирования, хотя я к любому виду программирования отношусь с большим трепетом, но именно тестирование хотелось постоянно откладывать.

Совершенно очевидно, что тестирование программ это важно и что не напрасно для этого существует столько инструментов. Ответьте на вопрос: что доказывает факт работы программы? Успешное тестирование. Потому что другого способа доказать это попросту не существует.

И на самом деле, написание тестов к маленьким программам, которые вы пишите во время начала своего обучения, не имеет как таковой ценности для этих программ, ведь если программа принимает на вход строку X, а на выходе отдает строку X, с добавленным перед ней словом 'Hello', то скорее всего она не нуждается в тестировании. Но как и в случае с прочими темами, учиться тестированию следует с малого, поэтому для постепенного погружения в тему мы начнем писать тесты именно для тех программ, которые в тестировании на самом деле и не нуждаются.

unittest

Начнем мы знакомство с тестированием с библиотеки unittest. Эта библиотека поставляется со стандартным набором библиотек, поэтому устанавливать ничего не придется.
Документация

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

$ python3 -m venv venv
$ source venv/bin/activate

Добавим в проект git и сразу оформим файл .gitignore, создадим репозиторий и соединим его с проектом.

$ git init
$ git add .
$ git commit -m "v1.0"
$ git branch -M main
$ git remote add origin (ссылка на репозиторий)
$ git push -u origin main

Создадим папку

$ mkdir tests

И в папке tests создадим файл __init__.py

__init__.py нужен, чтобы папка tests воспринималась как пакет

В итоге содержание будет таким

$ ls
tests  venv

Файл .gitignore не показывается по команде ls, для этого есть команда

$ ls -a
.  ..  .git  .gitignore .idea  tests  venv

В папке tests создадим папку examples

Перейдем в папку examples

$ cd examples/
/examples$ mkdir unittest
/examples$ ls
unittest

И создадим папку unittest

С подготовкой закончено.

Первые тесты

Теперь перейдем в папку unittest и напишем первую программу для тестирования.

resume.py
import time


def candidate(item):
    """Информация о кандидате"""
    new_candidate = {'Кандидат': item}
    time.sleep(2)
    return new_candidate


def resume():
    """Сценарий сбора информации о кандидате"""
    print('Давайте познакомимся')
    time.sleep(1)
    full_name = input('Как вас зовут? ').title()
    age = input(f'Приятно познакомиться, {full_name}, сколько вам лет?: ')
    city = input('И последни вопрос, откуда вы?: ')
    time.sleep(1)
    print('Спасибо за беседу, передал информацию о вашей кандидатуре руководству')

    info = f'имя - {full_name}, возраст - {age}, город - {city}'

    return info


def main():
    print('\n', candidate(resume()), sep='')


if __name__ == '__main__':
    main()
RUN
Давайте познакомимся
Как вас зовут? Илья
Приятно познакомиться, Илья, сколько вам лет?: 24
И последни вопрос, откуда вы?: Москва
Спасибо за беседу, передал информацию о вашей кандидатуре руководству

{'Кандидат': 'имя - Илья, возраст - 24, город - Москва'}

Process finished with exit code 0

Например такую. Программа resume.py будет содержать две функции, resume() - будет имитировать общение с человеком, candidate() - возвращать собранную информацию. В самой программе нет ничего необычного, метод .sleep() модуля time нужен для небольших пауз, это никак не сказывается на функционале, просто, как по мне, добавляет немного натуральности процессу. В resume() мы возвращаем строку из собранных данных, в candidate() мы передаем эту строку в качестве ключа для словаря и возвращаем этот словарь. Оборачиваем программу в функцию main() и запускаем ее внутри конструкции if __name__ == '__main__'.
Как видно из запуска resume.py работает так как и описано, и эта программа как раз относится к тем, которые не имеет большого смысла тестировать, потому что тут все совсем очевидно и не понятно, что тут может пойти не так. Тем не менее давайте ее проверим.

tests/test_resume.py
import unittest
from examples.unittest import resume


class ResumeTests(unittest.TestCase):
    """Тесты для программы resume.py"""

    def test_candidate_resume(self):
        """Тест функции candidate(). Возвращаемые данные верны?"""
        formatted_info_about_candidate = resume.candidate('имя - Илья, возраст - 25, город - Москва')
        self.assertEqual(formatted_info_about_candidate, {'Кандидат': 'имя - Илья, возраст - 25, город - Москва'})


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

Модуль unittest предполагает строгое соблюдение нескольких условий
1. Все тесты должны называться со слова test_. После test_ обычно пишется название функции (в названии допускаются уточнения логики функции), которую мы тестируем. Если функция будет не соответствовать этому условию, то при запуске тестирования unittest попросту не увидит этот тест.
2. Все тесты хранятся в классе, который наследуется от класса TestCase. Собственно в связи с этим и связано первое правило, логика TestCase предполагает наличие слова test_. При этом наличие слова Tests или Test в названии класса, унаследованного от TestCase, условие не обязательное.

Теперь что касается самого теста. Тест на данный момент в классе ResumeTests только один - test_candidate_resume. В данном тесте мы тестируем функцию candidate(), внутрь этой функции мы передаем какие-то данные, и сохраняем работу функции в переменную formatted_info_about_candidate. Эта переменная будет объектом для сравнения результата.

Второй строчкой происходит это самое сравнение. Метод assertEqual принимает два значения и проверяет их на условие равенства (A == B). Таким образом, в качестве A передаем результат работы функции candidate(), а в качестве B результат, который мы предполагаем увидеть.

Запустить программу можно с помощью Run (ctrl + shift + f10), но чаще используется метод запуска через терминал. Этот способ наиболее практичен, поскольку так нам не надо каждый раз открывать программу с тестом для проверки программы. Так что будем использовать именно метод запуска через терминал.

Terminal
$ python -m unittest
.
----------------------------------------------------------------------
Ran 1 test in 2.001s

OK

Запустить тесты можно командой python -m unittes. Первая строчка результата содержит точку, эта точка означает, что один тест прошел успешно. Ниже информация о времени исполнения теста и еще ниже слово OK, которе является свидетельством успеха теста.

Мы протестировали функцию candidate(). Но что делать с функцией resume()? Если у вас возникла мысль, что проверять можно только функции, которые принимают какие-нибудь параметры, то это, конечно, не так. Метод assertEqual всего один из ряда методов для проверки условий, хотя проверить функцию resume() безусловно можно и с помощью assertEqual.

Давайте проверим, например, являются ли, возвращаемые функцией resume(), данные типом str.

tests/test_resume.py
import unittest
from examples.unittest import resume


class ResumeTests(unittest.TestCase):
    """Тесты для программы resume.py"""

    def test_candidate_resume(self):
        """Тест функции candidate(). Возвращаемые данные верны?"""
        formatted_info_about_candidate = resume.candidate('имя - Илья, возраст - 25, город - Москва')
        self.assertEqual(formatted_info_about_candidate, {'Кандидат': 'имя - Илья, возраст - 25, город - Москва'})

    def test_resume(self):
        """Тест функции resume(). Тип возвращаемых данных верен?"""
        type_of_return = resume.resume()
        self.assertIsInstance(type_of_return, str)


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

Добавим тест test_resume(), в котором будем проверять тип, возвращаемых функцией resume(), данных. Для этого воспользуемся методом assertIsInstance, который как раз это проверят (isinstance(a,b)).

Теперь запустим тест и посмотрим на результат.

Terminal
$ python -m unittest
.Давайте познакомимся
Как вас зовут? Борис
Приятно познакомиться, Борис, сколько вам лет?: 31
И последни вопрос, откуда вы?: Владимир
Спасибо за беседу, передал информацию о вашей кандидатуре руководству
.
----------------------------------------------------------------------
Ran 2 tests in 26.611s

OK

Как видно, при запуске теста для программы, в которую не переданы данные по умолчанию, но которая предполагает обработку данных, эти данные все равно будут собраны. Выполнение программы попросту запустится внутри теста и также будут запрошены данные от пользователя.
Оба теста возвращают OK, таким образом мы успешно проверили две взаимосвязанные программы по двум разным условиям, при этом обе проверки пишутся в две строки.

Но нам ведь необязательно останавливаться на двух проверках, количество проверок не должно ровняться количеству проверяемых функций. Давайте добавим еще тестов.

tests/test_resume.py
import unittest
from examples.unittest import resume


class ResumeTests(unittest.TestCase):
    """Тесты для программы resume.py"""

    def test_candidate_resume(self):
        """Тест функции candidate(). Возвращаемые данные верны?"""
        print('Функция test_candidate_resume()')
        formatted_info_about_candidate = resume.candidate('имя - Илья, возраст - 25, город - Москва')
        self.assertEqual(formatted_info_about_candidate, {'Кандидат': 'имя - Илья, возраст - 25, город - Москва'})

    def test_resume(self):
        """Тест функции resume(). Тип возвращаемых данных верен?"""
        print('\n', '\n', 'Функция test_resume()')
        type_of_return = resume.resume()
        self.assertIsInstance(type_of_return, str)

    def test_resume_nodata(self):
        """Тест функции main(). Случай, когда данные не переданы."""
        print('\n', '\n', 'Функция test_resume_nodata()')
        nodata_send = resume.resume()
        self.assertIsNotNone(nodata_send)

    def test_main(self):
        """Тест функции main(). Функция содержит данные?"""
        print('\n', '\n', 'Функция test_main()')
        main_result = resume.main()
        self.assertTrue(main_result)


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

Добавим тест test_resume_nodata(), в котором рассмотрим ситуацию, когда ответов на вопросы бот не последует. Метод assertIsNotNone проверяет, содержит ли переданный аргумент хоть какие-нибудь данные. И тест test_main() где будем использовать метод assertTrue, метод также принимает один аргумент и положительным результатом проверки будет ситуация, когда аргумент возвращает True. Напомню, что в случае данных True будет возвращено при их наличии.

И также добавим в каждый тест print() с названием теста и пробелами для наглядности.

Terminal
$ python -m unittest
Функция test_candidate_resume()
.
 
 Функция test_main()
Давайте познакомимся
Как вас зовут? Андрей
Приятно познакомиться, Андрей, сколько вам лет?: 33
И последни вопрос, откуда вы?: Орел
Спасибо за беседу, передал информацию о вашей кандидатуре руководству

{'Кандидат': 'имя - Андрей, возраст - 33, город - Орел'}
F
 
 Функция test_resume()
Давайте познакомимся
Как вас зовут? Вова
Приятно познакомиться, Вова, сколько вам лет?: 56
И последни вопрос, откуда вы?: Минск
Спасибо за беседу, передал информацию о вашей кандидатуре руководству
.
 
 Функция test_resume_nodata()
Давайте познакомимся
Как вас зовут? 
Приятно познакомиться, , сколько вам лет?: 
И последни вопрос, откуда вы?: 
Спасибо за беседу, передал информацию о вашей кандидатуре руководству
.
======================================================================
FAIL: test_main (test_resume.ResumeTests)
Тест функции main(). Функция содержит данные?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tsarkoilya/kavo/TestsKAVO/tests/test_resume.py", line 30, in test_main
    self.assertTrue(main_result)
AssertionError: None is not true

----------------------------------------------------------------------
Ran 4 tests in 34.768s

FAILED (failures=1)

Все проверки, кроме test_main(), прошли успешно. И действительно функция main() только печатает результат, но ничего не возвращает.

Результат ошибочного теста ожидаемо богаче на информацию. Тут мы можем почитать информацию об ошибке и комментарий к проверке, в данном случае: None is not true.

На примере resume.py мы познакомились с вами с синтаксисом тестов, с несколькими методами проверки и познакомились с интерфейсом работы тестов.

Подробнее о TestCase

Для знакомства с некоторыми остальными возможностями unittest давайте напишем еще одну программу.

resume_evolve.py
class Resume:
    """Класс для беседы и сбора информации о кандидате"""
    list_of_questions = [
        'Как вас зовут? ',
        'Сколько вам лет? ',
        'Откуда вы? ',
        'Какое у вас образование? '
    ]

    def __init__(self, candidate):
        """Инициализация каждого кандидата"""
        self.candidate = candidate

    def preparation(self, list_of_questions):
        """
        Сценарий беседы с кандидатом.
        Кандидат может согласиться начать диалог или отказаться.
        В случае согласия начать беседу кандидату задаются вопросы
        из списка list_of_questions.
        """
        print('Здравствуйте, вы откликнулись на вакансию нашей компании', '\n', '\n',
              'Желаете немного побеседовать?', sep='')
        option = input('Введите \'ДА\' или \'НЕТ\' ')
        if option.upper() == 'НЕТ':
            print('Понял вас, всего доброго!')
        if option.upper() == 'ДА':
            answers = []
            print('Хорошо, тогда ответьте на несколько вопросов')

            for question in list_of_questions:
                answer = input(question)
                answers.append(question + ' - ' + answer)

            new_candidate = {self.candidate: answers}

            return new_candidate

        else:
            print('Неизвестная команда')


def main():
    ex_1 = Resume('Кандидат 1')
    print(ex_1.preparation(list_of_questions=Resume.list_of_questions))


if __name__ == '__main__':
    main()

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

Напишем первый тест.

tests/test_resume_evolve.py
import unittest
from examples.unittest import resume_evolve


class ResumeEvolve(unittest.TestCase):
    """Тесты класса Resume"""

    def test_resume(self):
        """Тест класса Resume. Возвращаемый тип данных не является словарем?"""
        ex = resume_evolve.Resume('Кандидат 1')
        self.assertNotIsInstance(ex, dict)


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

Тестом test_resume будем проверять, что экземпляр класса не словарь методом assertNotIsInstance.

Terminal
$ python -m unittest test_resume_evolve.py 
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

И, конечно, это так. Тип экземпляра класса - сам этот класс, экземпляром которого он является. Но обратить внимание тут я хотел на другое, в директории tests у нас находятся два файла с тестами и запустив проверку так, как мы делали выше мы начнем исполнение обоих файлов, для того чтобы этого избежать достаточно через пробел написать название файла, который мы хотим запустить.

Добавим еще один тест.

tests/test_resume_evolve.py
import unittest
from examples.unittest import resume_evolve


class ResumeEvolve(unittest.TestCase):
    """Тесты класса Resume"""

    def test_resume(self):
        """Тест класса Resume. Возвращаемый тип данных не является словарем?"""
        print('\n', 'Тест test_preparation()')
        ex = resume_evolve.Resume('Кандидат 1')
        self.assertNotIsInstance(ex, dict)

    def test_preparation_with_data(self):
        """Тест функции preparation(). Возвращаемый тип данных не является словарем?"""
        print('\n', 'Тест test_preparation_with_data()')
        ex = resume_evolve.Resume('Кандидат 1').preparation(list_of_questions=resume_evolve.Resume.list_of_questions)
        self.assertNotIsInstance(ex, dict)


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

Будем делать точно такую же проверку, но конкретно для функции preparation().

Terminal
$ python -m unittest test_resume_evolve.py 

 Тест test_preparation_with_data()
Здравствуйте, вы откликнулись на вакансию нашей компании

Желаете немного побеседовать?
Введите 'ДА' или 'НЕТ' да
Хорошо, тогда ответьте на несколько вопросов
Как вас зовут? Илья
Сколько вам лет? 24
Откуда вы? Москва
Какое у вас образование? Высшее
F
 Тест test_preparation()
.
======================================================================
FAIL: test_preparation_with_data (test_resume_evolve.ResumeEvolve)
Тест функции preparation(). Возвращаемый тип данных не является словарем?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tsarkoilya/kavo/TestsKAVO/tests/test_resume_evolve.py", line 18, in test_preparation_with_data
    self.assertNotIsInstance(ex, dict)
AssertionError: {'Кандидат 1': ['Как вас зовут?  - Илья', 'Сколько вам лет?  - 24', 'Откуда вы?  - Москва', 'Какое у вас образование?  - Высшее']} is an instance of <class 'dict'>

----------------------------------------------------------------------
Ran 2 tests in 8.292s

FAILED (failures=1)

Тут же при исполнении теста мы можем посмотреть на работу программы, как видно результат проверки функции preparation() - ошибка. Пусть функция preparation() и собирает информацию об экземпляре класса, но конкретно эта функция возвращает словарь, о чем также написано в информации об ошибке.

Но при тестировании функции preparation() возникает еще одна ситуация, которую хотелось бы учесть при проверке. Кандидат может написать что-нибудь отличное от ДА, возникает вопрос: есть ли возможность в unittest учесть такой сценарий и как-то его обработать?

tests/test_resume_evolve.py
import unittest
from examples.unittest import resume_evolve


class ResumeEvolve(unittest.TestCase):
    """Тесты класса Resume"""

    def test_resume(self):
        """Тест класса Resume. Возвращаемый тип данных не является словарем?"""
        print('\n', 'Тест test_preparation()')
        ex = resume_evolve.Resume('Кандидат 1')
        self.assertNotIsInstance(ex, dict)

    def test_preparation_with_data(self):
        """Тест функции preparation(). Возвращаемый тип данных не является словарем?"""
        print('\n', 'Тест test_preparation_with_data()')
        ex = resume_evolve.Resume('Кандидат 1').preparation(list_of_questions=resume_evolve.Resume.list_of_questions)
        if ex:
            self.assertNotIsInstance(ex, dict)
        else:
            self.fail('Кандидат не написал ДА')


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

Для этих целей unittest имеет метод fail. В качестве аргумента fail принимает сообщение, которое мы увидим при срабатывании этого метода. Такой подход добавит точности, ведь когда условие для начала беседы не соблюдены, то и ошибка говорящая, что результат является словарем не будет соответствовать действительности. Таким образом в случае, когда переменная ex содержит результат мы обрабатываем его методом assertNotIsInstance, в противном случае просто сообщаем, что клиент не захотел отвечать на вопросы.

Terminal
$ python -m unittest test_resume_evolve.py 

 Тест test_preparation_with_data()
Здравствуйте, вы откликнулись на вакансию нашей компании

Желаете немного побеседовать?
Введите 'ДА' или 'НЕТ' нет
Понял вас, всего доброго!
Неизвестная команда
F
 Тест test_preparation()
.
======================================================================
FAIL: test_preparation_with_data (test_resume_evolve.ResumeEvolve)
Тест функции preparation(). Возвращаемый тип данных не является словарем?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tsarkoilya/kavo/TestsKAVO/tests/test_resume_evolve.py", line 21, in test_preparation_with_data
    self.fail('Кандидат не написал ДА')
AssertionError: Кандидат не написал ДА

----------------------------------------------------------------------
Ran 2 tests in 1.778s

FAILED (failures=1)

Проверка работает.

Но если мы не хотим получать ошибку, а просто пропустить тест в случа, если не было введено ДА, то можем воспользоваться еще одним методом - skipTest.

tests/test_resume_evolve.py
...
ex = resume_evolve.Resume('Кандидат 1').preparation(list_of_questions=resume_evolve.Resume.list_of_questions)
if ex:
    self.assertNotIsInstance(ex, dict)
else:
    self.skipTest('Кандидат не написал ДА')
...
Terminal
...
Желаете немного побеседовать?
Введите 'ДА' или 'НЕТ' нет
Понял вас, всего доброго!
Неизвестная команда
s
 Тест test_preparation()
.
----------------------------------------------------------------------
Ran 2 tests in 2.118s

OK (skipped=1)

При использовании skipTest мы увидим s, а в итоговом отчете информацию о количестве пропущенных текстов.

Еще один момент, который мы можем улучшить это описание теста. Сейчас мы с помощью print() сами пишем какой тест в данный момент отрабатывает, но unittest предоставляет свои методы для этого.

tests/test_resume_evolve.py
import unittest
from examples.unittest import resume_evolve


class ResumeEvolve(unittest.TestCase):
    """Тесты класса Resume"""

    def test_resume(self):
        """Тест класса Resume. Возвращаемый тип данных не является словарем?"""
        print('\n', self.shortDescription(), '\n', self.id(), sep='')
        ex = resume_evolve.Resume('Кандидат 1')
        self.assertNotIsInstance(ex, dict)

    def test_preparation_with_data(self):
        """Тест функции preparation(). Возвращаемый тип данных не является словарем?"""
        print('\n', self.shortDescription(), '\n', self.id(), sep='')
        ex = resume_evolve.Resume('Кандидат 1').preparation(list_of_questions=resume_evolve.Resume.list_of_questions)
        if ex:
            self.assertNotIsInstance(ex, dict)
        else:
            self.fail('Кандидат не написал ДА')


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

Первый метод shortDescription(), который будет выводить первую строку docstring’а теста, это то, что мы пишем в тройных кавычках под объявлением метода. Метод id() будет возвращать имя теста, это как раз то, что ранее мы писали вручную.

Запустим тестирование теперь.

Terminal
$ python -m unittest test_resume_evolve.py 

Тест функции preparation(). Возвращаемый тип данных не является словарем?
test_resume_evolve.ResumeEvolve.test_preparation_with_data
Здравствуйте, вы откликнулись на вакансию нашей компании

Желаете немного побеседовать?
Введите 'ДА' или 'НЕТ' нет
Понял вас, всего доброго!
Неизвестная команда
F
Тест класса Resume. Возвращаемый тип данных не является словарем?
test_resume_evolve.ResumeEvolve.test_resume
.
======================================================================
FAIL: test_preparation_with_data (test_resume_evolve.ResumeEvolve)
Тест функции preparation(). Возвращаемый тип данных не является словарем?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tsarkoilya/kavo/TestsKAVO/tests/test_resume_evolve.py", line 21, in test_preparation_with_data
    self.fail('Кандидат не написал ДА')
AssertionError: Кандидат не написал ДА

----------------------------------------------------------------------
Ran 2 tests in 4.058s

FAILED (failures=1)

Обратите внимание на текст перед тестами, первая строка описание из docstring'а, вторая имя теста вида: название файла.название класса. название теста. Достаточно удобно и понятно, и, конечно, любые дополнительные комментарии модно добавить к этому описанию.

Еще два особых метода, о которых хотелось бы упомянуть - setUp() и tearDown() и их аналоги уровня класса setUpClass() и tearDownClass().
Метод setUp() вызывается перед запуском каждого теста, метод setUpClass() - перед запуском тестов класса.
Методы tearDown() и tearDownClass() вызываются при завершении теста.

tests/test_resume_evolve.py
import unittest
from examples.unittest import resume_evolve


class ResumeEvolve(unittest.TestCase):
    """Тесты класса Resume"""

    @classmethod
    def setUpClass(cls):
        """Метод, который срабатывает перед запуском всех тестов класса."""
        print('Запускаем тесты класса ResumeEvolve')

    @classmethod
    def tearDownClass(cls):
        """Метод, который срабатывает после отработки всех тестов класса."""
        print('\n', '\n', 'Закончили тесты класса ResumeEvolve', sep='')

    def setUp(self):
        """Метод, который срабатывает перед запуском каждого теста."""
        print('\n', self.shortDescription(), '\n', self.id(), sep='')

    def tearDown(self):
        """Метод, который срабатывает после отработки каждого теста."""
        print(f'Тест: {self.id()} закончен')

    def test_resume(self):
        """Тест класса Resume. Возвращаемый тип данных не является словарем?"""
        ex = resume_evolve.Resume('Кандидат 1')
        self.assertNotIsInstance(ex, dict)

    def test_preparation_with_data(self):
        """Тест функции preparation(). Возвращаемый тип данных не является словарем?"""
        ex = resume_evolve.Resume('Кандидат 1').preparation(list_of_questions=resume_evolve.Resume.list_of_questions)
        if ex:
            self.assertNotIsInstance(ex, dict)
        else:
            self.skipTest('Кандидат не написал ДА')


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

Методы setUpClass() и tearDownClass() являются методами класса, поэтому для их использования необходимы соответствующие декораторы. В setup метода давайте выводит сообщение, что тесты класса начата, а в teardown - что закончены.
Методы setUp() и tearDown() работаю для каждого теста, поэтому в них мы можем вынести дублирующийся код для описания.

Terminal
$ python -m unittest test_resume_evolve.py 
Запускаем тесты класса ResumeEvolve

Тест функции preparation(). Возвращаемый тип данных не является словарем?
test_resume_evolve.ResumeEvolve.test_preparation_with_data
Здравствуйте, вы откликнулись на вакансию нашей компании

Желаете немного побеседовать?
Введите 'ДА' или 'НЕТ' нет
Понял вас, всего доброго!
Неизвестная команда
Тест: test_resume_evolve.ResumeEvolve.test_preparation_with_data закончен
s
Тест класса Resume. Возвращаемый тип данных не является словарем?
test_resume_evolve.ResumeEvolve.test_resume
Тест: test_resume_evolve.ResumeEvolve.test_resume закончен
.

Закончили тесты класса ResumeEvolve

----------------------------------------------------------------------
Ran 2 tests in 3.285s

OK (skipped=1)

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

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

asserts

Мы познакомились с вами с некоторыми наиболее частыми assert'ами, в официальной документации все assert'ы разбиты на несколько таблиц, первая из них - наиболее частые, ознакомится с таблицами вы можете в этой документации, мы же рассмотрим их на примерах.

tests/all_asserts.py
import unittest


class AllMostCommonlyUsedAssertsExamples(unittest.TestCase):
    """В данном класс рассмотрим все наиболее часто используемые assert методы"""

    def test_assert_equal(self):
        """Метод assertEqual(x, y) (X == Y)"""
        self.assertEqual(10, 10)

    def test_assert_not_equal(self):
        """Метод assertNotEqual(x, y) (X != Y)"""
        self.assertNotEqual([1, 2, 3], [2, 3, 4])

    def test_assert_true(self):
        """Метод assertTrue(x) (bool(x) is True)"""
        self.assertTrue({'Russia': 'Moscow'})

    def test_assert_false(self):
        """Метод assertFalse(x) (bool(x) is False)"""
        self.assertFalse({})

    def test_assert_none(self):
        """Метод assertIsNone(x) (x is None)"""

        def test(a=10, b=5):
            result = a * b

        self.assertIsNone(test())

    def test_assert_is_not_none(self):
        """Метод assertIsNotNone(x) (x is not None)"""
        self.assertIsNotNone('Это не None')

    def test_assert_in(self):
        """Метод assertIn(x, y) (x in y)"""
        example = 'python'
        self.assertIn(example, ['C#', 'python', 'C++'])

    def test_assert_not_in(self):
        """Метод assertNotIn(x, y) (x not in y)"""
        example = 'python'
        self.assertNotIn(example, ['C#', 'PHP', 'C++'])

    def test_assert_is(self):
        """Метод assertIs(x, y) (x is y)"""
        ex = (1,)
        self.assertIs(type(ex), tuple)

    def test_assert_is_not(self):
        """Метод assertIsNot(x, y) (x is not y)"""
        ex = (1, 2, 3)
        self.assertIsNot(type(ex), set)

    def test_assert_is_instance(self):
        """Метод assertIsInstance(x, y) (isinstance(x, y))"""

        class Test:
            pass

        ex = Test()

        self.assertIsInstance(ex, Test)

    def test_assert_not_is_instance(self):
        """Метод assertNotIsInstance(x, y) (not isinstance(x, y))"""
        self.assertNotIsInstance(type(2), type('2'))


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

В классе AllMostCommonlyUsedAssertsExamples приведены примеры всех наиболее часто используемых assert'ов. Каких-то дополнительных комментариев об этих методах дать не получится, их работа максимально понятна и очевидна.

Terminal
$ python -m unittest all_asserts.py 
............
----------------------------------------------------------------------
Ran 12 tests in 0.001s

OK

Ранее я не упоминал об этом, но все тесты имею один необязательный параметр - msg. В этом параметре мы можем написать сообщение, которое мы увидим в случае ошибочного теста.

tests/all_asserts.py
...
def test_assert_none(self):
    """Метод assertIsNone(x) (x is None)"""

    def test(a=10, b=5):
        result = a * b
        return result

    self.assertIsNone(test(), 'Функция test возвращает результат отличный от None')
...
Terminal
$ python -m unittest all_asserts.py 
.......F....
======================================================================
FAIL: test_assert_none (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertIsNone(x) (x is None)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tsarkoilya/kavo/TestsKAVO/tests/all_asserts.py", line 30, in test_assert_none
    self.assertIsNone(test(), 'Функция test возвращает результат отличный от None')
AssertionError: 50 is not None : Функция test возвращает результат отличный от None

----------------------------------------------------------------------
Ran 12 tests in 0.001s

FAILED (failures=1)

Например давайте сделаем, чтоб функция test теста test_assert_none возвращала результат, и добавим параметр msg в метод assertIsNone. Сообщение об ошибке мы видим вместо сообщения об ошибке, которое генерируется по умолчанию, если параметр msg не задан.

Следующая группа проверок, которая вынесена в отдельную таблицу - проверка создания исключений, предупреждений и сообщений журнала. Для подобных проверок unittest предоставляет 6 методов, будем разбирать по 2.

tests/all_asserts.py
...

class ExceptionsWarningslogsAssertsExamples(unittest.TestCase):
    """Проверка создания исключений, предупреждений и сообщений журнала"""

    def setUp(self):
        print('\n', self.shortDescription(), sep='')

    def test_assert_raises(self):
        """Метод assertRaises (Exception, Fragment of Code)"""

        with self.assertRaises(IndexError) as exc:
            lst = [1, 2, 3]
            print(lst[5])

        print(exc)
        print(exc.exception)

        # def test():
        #     lst = [1, 2, 3]
        #     return lst[5]
        #
        # self.assertRaises(IndexError, test)

    def test_assert_raises_regex(self):
        """Метод assertRaisesRegex (Exception, Error as Str, Fragment of Code)"""

        error = 'list index out of range'
        with self.assertRaisesRegex(IndexError, error) as exc:
            lst = [1, 2, 3]
            print(lst[5])

        print(exc)
        print(exc.exception)

        # def test():
        #     lst = [1, 2, 3]
        #     return lst[5]
        #
        # error = 'list index out of range'
        # self.assertRaisesRegex(IndexError, error, test)


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

Первыми двумя методами будут - assertRaises и assertRaisesRegex. Оба метода проверяют результат какого-нибудь фрагмента кода на соответствие ошибки. Разница в том, что assertRaisesRegex ожидает получить в качестве аргумента еще и текст ошибки, для этого метод assertRaisesRegex поддерживает использование регулярных выражений для описания текста ошибки.

Рассмотрим каждый тест отдельно.

test_assert_raises. Фрагмент кода сверху и фрагмент кода в комментарии отрабатывают одинаково и оба возвращают ОК. Но во втором случае тест описан классическим способом, к которому мы уже привыкли, то есть сверху мы написали функцию, которая будет возвращать ошибку, а ниже в методе assertRaises мы первым параметром передали предполагаемый тип ошибки, а вторым проверяемый фрагмент кода.
Но сами разработчики модуля unittest рекомендует для этих методов использовать менеджер контекста with, поскольку таким образом мы делаем ту же самую проверку, что и в нижнем примере, но при этом мы сразу сохраняем исключение в переменную. С этой переменной мы далее при необходимости можем удобно взаимодействовать, например, можем распечатать текст исключения ошибки. И сама проверка при использовании with получается лаконичнее.

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

Terminal
$ python -m unittest all_asserts.ExceptionsWarningslogsAssertsExamples

Метод assertRaises (Exception, Fragment of Code)
<unittest.case._AssertRaisesContext object at 0x7f9933023dc0>
list index out of range
.
Метод assertRaisesRegex (Exception, Error as Str, Fragment of Code)
<unittest.case._AssertRaisesContext object at 0x7f9933023be0>
list index out of range
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Теперь запустим тесты. Для запуска только одного класса из файла используйте запись - название_файла.название_класса. Как видно тесты отработали успешно и в каждом мы видим вывод двух print(), первый, куда мы передали exc, распечатывает описание объекта, а второй, куда мы передали exc.exception - описание ошибки из этого объекта.

Предупреждения (Warnings), почти то же самое что и ошибки, но предупреждения в отличие от ошибок не прерывают выполнение программы, весь список предупреждений я приводил на схеме в блоке по ООП в python в разделе, где речь шла о try/except. Текст предупреждений мы можем писать самостоятельно, используя доступные типы предупреждений. Посмотрим как это выглядит.

tests/all_asserts.py
...
class ExceptionsWarningslogsAssertsExamples(unittest.TestCase):
    """Проверка создания исключений, предупреждений и сообщений журнала"""

    def setUp(self):
        print('\n', self.shortDescription(), sep='')

    ...    

    def test_assert_warns(self):
        """Метод assertWarns (Warning, Fragment of Code)"""

        with self.assertWarns(SyntaxWarning) as wrng:
            def syntax_warning():
                version_python = "3.8"
                result = version_python is "3.8"
                warnings.warn('Для сравнения лучше использовать == ', SyntaxWarning)
                return result

            syntax_warning()

        print(wrng)
        print(wrng.warnings)
        print(wrng.warning)
        print(wrng.lineno)
        print(wrng.filename)

        # def syntax_warning():
        #     version_python = "3.8"
        #     result = version_python is "3.8"
        #     warnings.warn('Для сравнения лучше использовать == ', SyntaxWarning)
        #     return result
        #
        # self.assertWarns(SyntaxWarning, syntax_warning)

    def test_assert_warns_regex(self):
        """Метод assertWarnsRegex (Warning, Warning as Str, Fragment of Code)"""

        with self.assertWarnsRegex(SyntaxWarning, 'Для сравнения лучше использовать == ') as wrng:
            def syntax_warning():
                version_python = "3.8"
                result = version_python is "3.8"
                warnings.warn('Для сравнения лучше использовать == ', SyntaxWarning)
                return result

            syntax_warning()

        print(wrng)
        print(wrng.warnings)
        print(wrng.warning)
        print(wrng.lineno)
        print(wrng.filename)

        # def syntax_warning():
        #     version_python = "3.8"
        #     result = version_python is "3.8"
        #     warnings.warn('Для сравнения лучше использовать == ', SyntaxWarning)
        #     return result
        #
        # warning = 'Для сравнения лучше использовать == '
        # self.assertWarnsRegex(SyntaxWarning, warning, syntax_warning)


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

Методы assertWarns и assertWarnsRegex. Логика точно такая же как в случае assertRaises и assertRaisesRegex, но работаем мы уже с предупреждениями. Также есть два варианта вызова, стандартно (тесты в комментариях) и через with.

Посмотрим на test_assert_warns. В менеджере with вызываем метод assertWarns и передаем в него SyntaxWarning, таким образом код внутри контекста будет проверяться на вызов данного предупреждения. Напишем функцию syntax_warning() где для сравнения будем использовать is, вместо ==, конечно, использование == предпочтительнее, но и с использованием is программа успешно отработает и вернет True. Но все-таки нужно стремиться писать код правильно, поэтому добавим в программу вызов предупреждения типа SyntaxWarning с текстом, говорящим, что предпочтительнее здесь использовать ==.
Пример максимально простой, но думаю хорошо демонстрирующий работу данного метода проверки.

С test_assert_warns_regex ситуация такая же, но для работы необходим дополнительный параметр с текстом предупреждения.

У переменной хранящей предупреждение есть 4 вызываемых параметра, посмотрим на них при запуске тестов.

Terminal
$ python -m unittest all_asserts.ExceptionsWarningslogsAssertsExamples

...
Метод assertWarns (Warning, Fragment of Code)
<unittest.case._AssertWarnsContext object at 0x7fa000ea0130>
[<warnings.WarningMessage object at 0x7fa000ea0250>]
Для сравнения лучше использовать == 
119
/home/tsarkoilya/kavo/TestsKAVO/tests/all_asserts.py
.
Метод assertWarnsRegex (Warning, Warning as Str, Fragment of Code)
<unittest.case._AssertWarnsContext object at 0x7fa000e43f70>
[<warnings.WarningMessage object at 0x7fa000ea0220>]
Для сравнения лучше использовать == 
145
/home/tsarkoilya/kavo/TestsKAVO/tests/all_asserts.py
.
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

Отработку тестов test_assert_raises и test_assert_raises_regex рассмотренных выше уберем из результата. Методы успешно отработали. Посмотри на 4 вызываемых параметра.
.warnings объект хранящий сообщения
.warning само сообщения предупреждения
.lineno строка, в которой вызывается предупреждение
.filename название файла.

Осталось еще 2 метода из этой категории.

tests/all_asserts.py
...
class ExceptionsWarningslogsAssertsExamples(unittest.TestCase):
    """Проверка создания исключений, предупреждений и сообщений журнала"""

    def setUp(self):
        print('\n', self.shortDescription(), sep='')

    ...

    def test_assert_logs(self):
        """Метод assertLogs (is logging.Logger obj)"""
        with self.assertLogs('Journal', level=None) as lg:
            logging.getLogger('Journal').info('Example Message')
            logging.getLogger('Journal.warnings').warning('Example Warning')
            logging.getLogger('Journal.errors').error('Example Error')

        print(lg.output)
        print(lg.records)

        # ex = logging.Logger('Journal')
        # ex.log(20, 'Example message fo level INFO')
        # self.assertLogs(ex)

    def test_assert_no_logs(self):
        """Метод assertNoLogs (logging.Logger obj is False)"""

        with self.assertNoLogs('Journal', level='INFO') as lg:
            logging.getLogger('Journal').info('Example Message')

        print(lg.output)
        print(lg.records)

        # ex = logging.Logger('Journal')
        # self.assertNoLogs(ex)


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

Методы assertLogs и assertNoLogs. Используются для проверки наличия записей в определенных уровнях журнала логов. Метод assertLogs проверяет, что записи на выбранных уровнях, либо в журнале существуют, метод assertNoLogs используется для обратной проверки.

Рассмотрим примеры, чтобы лучше понять происходящее.

test_assert_logs. В этом тесте используем метод assertLogs, первый параметр имя журнала, второй - уровень, наличие сообщений на котором мы будем проверять. Передадим None, таким образом мы будем искать записи на любом уровне. Уровни этого журнала записываются либо строковым значением, либо числовым. Так, например, уровень INFO соответствует значению 20, а уровень WARNING значение 30.
Внутри test_assert_logs мы добавляем записи в журнал на уровни INFO, WARNING и ERROR, таким образом журнал содержит 3 записи и метод assertLogs успешно отрабатывает.
В закомментированном варианте ситуация аналогичная.

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

Посмотрим на результат.

Terminal
$ python -m unittest all_asserts.ExceptionsWarningslogsAssertsExamples

...
Метод assertLogs (is logging.Logger obj)
['INFO:Journal:Example Message', 'WARNING:Journal.warnings:Example Warning', 'ERROR:Journal.errors:Example Error']
[<LogRecord: Journal, 20, /home/tsarkoilya/kavo/TestsKAVO/tests/all_asserts.py, 169, "Example Message">, <LogRecord: Journal.warnings, 30, /home/tsarkoilya/kavo/TestsKAVO/tests/all_asserts.py, 170, "Example Warning">, <LogRecord: Journal.errors, 40, /home/tsarkoilya/kavo/TestsKAVO/tests/all_asserts.py, 171, "Example Error">]
.
Метод assertNoLogs (logging.Logger obj is False)
F
======================================================================
FAIL: test_assert_no_logs (all_asserts.ExceptionsWarningslogsAssertsExamples)
Метод assertNoLogs (logging.Logger obj is False)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tsarkoilya/kavo/TestsKAVO/tests/all_asserts.py", line 183, in test_assert_no_logs
    with self.assertNoLogs('Journal', level='INFO') as lg:
  File "/usr/lib/python3.10/unittest/_log.py", line 75, in __exit__
    self._raiseFailure(
AssertionError: Unexpected logs found: ['INFO:Journal:Example Message']

----------------------------------------------------------------------
Ran 6 tests in 0.001s

FAILED (failures=1)

Тест test_assert_logs отрабатывает успешно. И мы видим результат двух параметров.
.output возвращает содержимое журнала, те самые 3 записи, которые мы добавили вручную.
.records возвращает историю добавления записей в журнал, первый параметр заголовок записи, второй уровень в численном эквиваленте, третий - текст записи.

Тест test_assert_no_logs ожидаемо отрабатывает с ошибкой, в которой написано, что записи найдены.

tests/all_asserts.py
...
def test_assert_no_logs(self):
    """Метод assertNoLogs (logging.Logger obj is False)"""

    with self.assertNoLogs('Journal', level='ERROR') as lg:
        logging.getLogger('Journal').info('Example Message')

    # ex = logging.Logger('Journal')
    # self.assertNoLogs(ex)
...
Terminal
...
Метод assertNoLogs (logging.Logger obj is False)
.
----------------------------------------------------------------------
Ran 1 tests in 0.002s

OK

А вот если мы заменим уровень INFO на, например, ERROR, то тест отработает успешно, потому что на этот уровень записи мы не добавляли. .output и .records в таком случае придется убрать, потому что lg содержит пустой Logger().

Теперь перейдем к следующей категории.

tests/all_asserts.py
...
class MoreSpecificChecksAssertsExamples(unittest.TestCase):
    """Методы, используемые для более специфических проверок"""

    def setUp(self):
        print('\n', self.shortDescription(), sep='')

    def test_assert_almost_equal(self):
        """Метод assertAlmostEqual(X, Y, знаков после запятой) (X == Y)"""

        self.assertAlmostEqual(10.001, 10.002, 2)

    def test_assert_not_almost_equal(self):
        """Метод assertNotAlmostEqual(X, Y, знаков после запятой) (X != Y)"""

        self.assertNotAlmostEqual(10.001, 10.002, 3)

    def test_assert_greater(self):
        """Метод assertGreater(X, Y) (X > Y)"""

        self.assertGreater([1, 2, 3], [1, 2])

    def test_assert_greater_equal(self):
        """Метод assertGreaterEqual(X, Y) (X >= Y)"""

        self.assertGreaterEqual('первый', 'второй')

    def test_assert_less(self):
        """Метод assertLess(X, Y) (X < Y)"""

        self.assertLess('три', 'четыре')

    def test_assert_less_equal(self):
        """Метод assertLessEqual(X, Y) (X <= Y)"""

        self.assertLessEqual('', '')

    def test_assert_regex(self):
        """Метод assertRegex(Text, Liter) (Liter in Text)"""

        self.assertRegex('один два три', ' ')

    def test_assert_not_regex(self):
        """Метод assertNotRegex(Text, Liter) (Liter not in Text)"""

        self.assertNotRegex('один два три', 'одиндва')

    def test_assert_count_equal(self):
        """Метод assertCountEqual(X, Y) (Элементы X == Y, порядок значений не важен)"""

        self.assertCountEqual(['один', 2, 3], [3, 'один', 2])


if __name__ == '__main__':
    unittest.main()
Terminal
$ python -m unittest all_asserts.MoreSpecificChecksAssertsExamples

Метод assertAlmostEqual(X, Y, знаков после запятой) (X == Y)
.
Метод assertCountEqual(X, Y) (Элементы X == Y, порядок значений не важен)
.
Метод assertGreater(X, Y) (X > Y)
.
Метод assertGreaterEqual(X, Y) (X >= Y)
.
Метод assertLess(X, Y) (X < Y)
.
Метод assertLessEqual(X, Y) (X <= Y)
.
Метод assertNotAlmostEqual(X, Y, знаков после запятой) (X != Y)
.
Метод assertNotRegex(Text, Liter) (Liter not in Text)
.
Метод assertRegex(Text, Liter) (Liter in Text)
.
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

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

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

tests/all_asserts.py
...
class TypeSpecificAssertsExamples(unittest.TestCase):
    """Список методов, зависящих от типа, автоматически используемых в assertEqual()"""

    def setUp(self):
        print('\n', self.shortDescription(), sep='')

    def test_assert_multiline_equal(self):
        """Метод assertMultiLineEqual(str(X), str(Y)) (str(X) == str(Y))"""

        self.assertMultiLineEqual('пример', 'пример')

    def test_assert_list_equal(self):
        """Метод assertListEqual(list(X), list(Y)) (list(X) == list(Y))"""

        self.assertListEqual(['пример', 'пример'], ['пример', 'пример'])

    def test_assert_tuple_equal(self):
        """Метод assertTupleEqual(tuple(X), tuple(Y)) (tuple(X) == tuple(Y))"""

        self.assertTupleEqual(('пример', 'пример'), ('пример', 'пример'))

    def test_assert_set_equal(self):
        """Метод assertSetEqual(set(X), set(Y)) (set(X) == set(Y))"""

        self.assertSetEqual({'пример', 'пример'}, {'пример', 'пример'})

    def test_assert_dict_equal(self):
        """Метод assertDictEqual(dict(X), dict(Y)) (dict(X) == dict(Y))"""

        self.assertDictEqual({'пример': 'пример'}, {'пример': 'пример'})

    def test_assert_sequence_equal(self):
        """Метод assertSequenceEqual(X, Y, тип=None) (X == Y, можно выбрать тип)"""

        self.assertMultiLineEqual('строка', 'строка')


if __name__ == '__main__':
    unittest.main()
Terminal
$ python -m unittest all_asserts.TypeSpecificAssertsExamples

Метод assertDictEqual(dict(X), dict(Y)) (dict(X) == dict(Y))
.
Метод assertListEqual(list(X), list(Y)) (list(X) == list(Y))
.
Метод assertMultiLineEqual(str(X), str(Y)) (str(X) == str(Y))
.
Метод assertSequenceEqual(X, Y, тип=None) (X == Y, можно выбрать тип)
.
Метод assertSetEqual(set(X), set(Y)) (set(X) == set(Y))
.
Метод assertTupleEqual(tuple(X), tuple(Y)) (tuple(X) == tuple(Y))
.
----------------------------------------------------------------------
Ran 6 tests in 0.000s

OK

С этой категорией все тоже достаточно очевидно.

Таким образом мы посмотрели на все существующие методы сравнения.

TestSuite

В файле all_asserts.py получилось 33 теста, допустим мы хотим запускать только часть из них. TestSuite как раз используется для группировки тестов и запуска этой группы. Для знакомства создадим новый файл test_suite.py.

tests/test_suite.py
import unittest
import all_asserts

exTestSuite = unittest.TestSuite()
exTestSuite.addTest(unittest.makeSuite(all_asserts.AllMostCommonlyUsedAssertsExamples))

if __name__ == '__main__':
    unittest.TextTestRunner().run(exTestSuite)

Синтаксис состоит всего из нескольких методов. Сначала создадим экземпляр TestSuite, а после добавим в него методом makeSuite один из классов файла all_asserts.py.

Запускать файл будем с помощью класса TextTestRunner и его метода run. Этот класс умеет запускать как TestSuite, так и TestCase. TextTestRunner имеет некоторые параметры, которые мы скоро увидим.

Terminal
$ python test_suite.py 
............
----------------------------------------------------------------------
Ran 12 tests in 0.000s

OK

Если мы хотим увидеть какое-нибудь описание, то мы должны изменить значение параметра verbosity класса TextTestRunner.

tests/test_suite.py
import unittest
import all_asserts

exTestSuite = unittest.TestSuite()
exTestSuite.addTest(unittest.makeSuite(all_asserts.AllMostCommonlyUsedAssertsExamples))

if __name__ == '__main__':
    unittest.TextTestRunner(verbosity=2).run(exTestSuite)
Terminal
$ python test_suite.py 
test_assert_equal (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertEqual(x, y) (X == Y) ... ok
test_assert_false (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertFalse(x) (bool(x) is False) ... ok
test_assert_in (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertIn(x, y) (x in y) ... ok
test_assert_is (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertIs(x, y) (x is y) ... ok
test_assert_is_instance (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertIsInstance(x, y) (isinstance(x, y)) ... ok
test_assert_is_not (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertIsNot(x, y) (x is not y) ... ok
test_assert_is_not_none (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertIsNotNone(x) (x is not None) ... ok
test_assert_none (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertIsNone(x) (x is None) ... ok
test_assert_not_equal (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertNotEqual(x, y) (X != Y) ... ok
test_assert_not_in (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertNotIn(x, y) (x not in y) ... ok
test_assert_not_is_instance (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertNotIsInstance(x, y) (not isinstance(x, y)) ... ok
test_assert_true (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertTrue(x) (bool(x) is True) ... ok

----------------------------------------------------------------------
Ran 12 tests in 0.001s

OK

По умолчанию параметр verbosity равен 1, для того чтобы увидеть описания тестов, необходимо заменить значение verbosity на любое значение отличное от 1.

tests/test_suite.py
import unittest
import all_asserts

exTestSuite = unittest.TestSuite()
exTestSuite.addTest(unittest.makeSuite(all_asserts.AllMostCommonlyUsedAssertsExamples))
exTestSuite.addTests(unittest.makeSuite(all_asserts.MoreSpecificChecksAssertsExamples))

print(f'Всего тестов {exTestSuite.countTestCases()}')

if __name__ == '__main__':
    unittest.TextTestRunner().run(exTestSuite)
Terminal
$ python test_suite.py 
Всего тестов 21
............
Метод assertAlmostEqual(X, Y, знаков после запятой) (X == Y)
.
Метод assertCountEqual(X, Y) (Элементы X == Y, порядок значений не важен)
.
Метод assertGreater(X, Y) (X > Y)
.
Метод assertGreaterEqual(X, Y) (X >= Y)
.
Метод assertLess(X, Y) (X < Y)
.
Метод assertLessEqual(X, Y) (X <= Y)
.
Метод assertNotAlmostEqual(X, Y, знаков после запятой) (X != Y)
.
Метод assertNotRegex(Text, Liter) (Liter not in Text)
.
Метод assertRegex(Text, Liter) (Liter in Text)
.
----------------------------------------------------------------------
Ran 21 tests in 0.001s

OK

Метод countTestCases() выводит количество тестов.

Добавим к exTestSuite тесты класса MoreSpecificChecksAssertsExamples методом addTests(). Метод addTests() применяется к итерируемым объектам. Но на данный момент разница может быть непонятна, потому что мы использовали метод addTest() так же как и addTests() для целого класса. Но как нам поступить если мы хотим использовать выборочные тесты из этого класса.

tests/test_suite.py
import unittest
import all_asserts

exTestSuite = unittest.TestSuite()
exTestSuite.addTest(all_asserts.AllMostCommonlyUsedAssertsExamples('test_assert_true'))

print(f'Всего тестов {exTestSuite.countTestCases()}')

if __name__ == '__main__':
    unittest.TextTestRunner().run(exTestSuite)
Terminal
$ python test_suite.py 
Всего тестов 1
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Для проверки единственного метода класса используется чистый addTest(), куда передается класс, а в самом классе в качестве атрибута передается имя теста в строковом виде.

TestLoader

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

Для знакомства создадим новый файл test_loader.py.

tests/test_loader.py
import unittest
import all_asserts

exTestLoader = unittest.TestLoader()
tests = exTestLoader.loadTestsFromTestCase(all_asserts.AllMostCommonlyUsedAssertsExamples)
# loadTestsFromTestCase(). Обрабатывает все тесты класса

# tests = exTestLoader.loadTestsFromModule(all_asserts)
# loadTestsFromModule(). Обрабатывает все тесты файла

# tests = exTestLoader.loadTestsFromName("all_asserts.AllMostCommonlyUsedAssertsExamples.test_assert_in")
# loadTestsFromName(). Обрабатывает все тесты файла, класса или отдельного теста. Уровень определяется точечной нотацией

tests_names = exTestLoader.getTestCaseNames(all_asserts.ExceptionsWarningslogsAssertsExamples)
print(tests_names)

if __name__ == '__main__':
    unittest.TextTestRunner().run(tests)

TestLoader предоставляет несколько возможностей для выбора тестов, все они представлены на примере, мы можем обработать все тесты файла, все тесты класса, либо любой отдельно взятый тест.

Метод getTestCaseNames сформирует список имен тестов класса.

Terminal
$ python test_loader.py 
['test_assert_logs', 'test_assert_no_logs', 'test_assert_raises', 'test_assert_raises_regex', 'test_assert_warns', 'test_assert_warns_regex']
............
----------------------------------------------------------------------
Ran 12 tests in 0.000s

OK

При запуске получаем список имен тестов класса ExceptionsWarningslogsAssertsExamples и запускаем все тесты класса AllMostCommonlyUsedAssertsExamples.

TestResultr

TestResult используется для компиляции информации о пройденных тестах.

tests/test_result.py
import unittest
import all_asserts

exTestLoader = unittest.TestLoader()
tests = exTestLoader.loadTestsFromTestCase(all_asserts.AllMostCommonlyUsedAssertsExamples)

exTestResult = unittest.TestResult()

if __name__ == '__main__':
    exTestResult = unittest.TextTestRunner().run(tests)

print('неожиданные исключения ', exTestResult.errors)
print('явно сигнализированные с использованием assert* методов исключения ', exTestResult.failures)
print('тесты содержащие причину пропуска ', exTestResult.skipped)
print('ожидаемые ошибки ', exTestResult.expectedFailures)
print('тесты, которые ожидались завершиться ошибкой, но завершились успешно ', exTestResult.unexpectedSuccesses)
print('всего тестов ', exTestResult.testsRun)
print('Все тесты прошли успешно?', exTestResult.wasSuccessful())
Terminal
$ python test_result.py 
F...........
======================================================================
FAIL: test_assert_equal (all_asserts.AllMostCommonlyUsedAssertsExamples)
Метод assertEqual(x, y) (X == Y)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tsarkoilya/kavo/TestsKAVO/tests/all_asserts.py", line 11, in test_assert_equal
    self.assertEqual(10, 12)
AssertionError: 10 != 12

----------------------------------------------------------------------
Ran 12 tests in 0.001s

FAILED (failures=1)
неожиданные исключения  []
явно сигнализированные с использованием assert* методов исключения  [(<all_asserts.AllMostCommonlyUsedAssertsExamples testMethod=test_assert_equal>, 'Traceback (most recent call last):\n  File "/home/tsarkoilya/kavo/TestsKAVO/tests/all_asserts.py", line 11, in test_assert_equal\n    self.assertEqual(10, 12)\nAssertionError: 10 != 12\n')]
тесты содержащие причину пропуска  []
ожидаемые ошибки  []
тесты, которые ожидались завершиться ошибкой, но завершились успешно  []
всего тестов  12
Все тесты прошли успешно? False

TestResult имеет различные статистические методы, которые возвращают список попавших под каждую категорию ошибок, я намеренно добавил в тесты одну ошибку, чтобы посмотреть как это выглядит. Конечно, поскольку это список, то к нему можно применить функцию len(), для вывода числового значения.

IsolatedAsyncioTestCase

unittest поддерживает асинхронное выполнение тестов, для этого вместо TestCase необходимо наследоваться от IsolatedAsyncioTestCase.

tests/iso_lated_asyncio_test_case.py
import unittest
import asyncio


async def async_summa(x, y):
    await asyncio.sleep(1)
    print('Спим 1...')
    return x + y


async def async_func():
    await asyncio.sleep(0.5)
    print('Спим 0.5..')


class Test(unittest.IsolatedAsyncioTestCase):

    def setUp(self):
        """Метод, который срабатывает перед запуском каждого теста."""
        print('\n', self.shortDescription(), sep='')

    async def asyncSetUp(self):
        print('Метод asyncSetUp()')
        await asyncio.gather(async_func(), async_summa(5, 5))

    async def asyncTearDown(self):
        print('Метод asyncTearDown()')
        await asyncio.gather(async_func())

    def tearDown(self):
        """Метод, который срабатывает после отработки каждого теста."""
        print(f'Тест: {self.id()} закончен')

    async def test_assert_equal(self):
        """Метод assertEqual(x, y) (X == Y)"""
        self.assertEqual(await async_summa(5, 5), 10)


if __name__ == "__main__":
    unittest.main()

При наследовании IsolatedAsyncioTestCase нам доступны корутины. Также IsolatedAsyncioTestCase имеет два метода asyncSetUp и asyncTearDown. asyncSetUp вызывается после обычного SetUp, а asyncTearDown перед обычным TearDown. В данном примере напишем два корутина async_summa() и async_func(). Обе эти функции мы будем вызывать перед выполнением тестов в методе asyncSetUp, после этого async_summa() мы еще раз вызовем в test_assert_equal(), а в asyncTearDown() мы еще раз вызовем async_func(). Таким образом async_func() и async_summa() должны отработать программу по 2 раза, async_func() засыпает на пол секунды по два раза, async_summa() на одну по два раза, все складываем и получаем 3 секунды на сон. Теперь запустим программу.

Terminal
$ python -m unittest iso_lated_asyncio_test_case.py

Метод assertEqual(x, y) (X == Y)
Метод asyncSetUp()
Спим 0.5..
Спим 1...
Спим 1...
Метод asyncTearDown()
Спим 0.5..
Тест: iso_lated_asyncio_test_case.Test.test_assert_equal закончен
.
----------------------------------------------------------------------
Ran 1 test in 2.512s

OK

Отработала программа за 2.5 секунды, потому что при срабатывании asyncSetUp() корутины async_summa() и async_func() вызываются и срабатывают асинхронно, таким образом мы убедились что асинхронность при тестировании отлично работает.

Что дальше?

Мы разобрали все основные возможности unittest, но рекомендую самостоятельно еще раз пробежаться по документации. На unittest тестирование не заканчивается, следующий материал будет посвящен библиотеке pytest.

Тестируйте свой код. Это не сложно и полезно.

Для отправки комментария необходимо авторизоваться



Комментарии

Здесь пока ничего нет...