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

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

enter image description here

План

  1. Введение в тестирование
    1. Что такое тестирование и баг
    2. Распространенные причины появления ошибок
    3. Виды тестирования
    4. Кто должен тестировать?
    5. Когда начинать тестировать?
    6. TDD
  2. Тестирование в Python
    1. Assertion
    2. unittest
    3. pytest tox, nose2,
    4. docktest
    5. mock
  3. Литература (Что почитать)

Введение в тестирование

Тестирование программного обеспечения - процесс исследования, испытания программного продукта, имеющий своей целью проверку соответствия между реальным поведением программы и её ожидаемым поведением.

Тестирование - сравнение фактического результата с ожидаемым

Actual Result == Expected Result

Цели тестирования

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

Программная ошибка он же баг

Actual Result != Expected Result

Баг (bug)– это ошибка в программе, вызывающая ее неправильную и (или) непредсказуемую работу, также багом называют отличие между фактическим и ожидаемым результатом. Из-за дефектов, допущенных еще во время написания кода, программа может не выполнять заложенных функций, работать не так, как обозначено в спецификации, или выполнять действия, которые не предусмотрены. Такие случаи называются сбоями программы (failure).

Распространенные причины появления ошибок(подробнее тут):

  1. Проблемы в коммуникациях между членами команды
  2. Изменение требований
  3. Временные рамки
  4. Ошибки программистов
    • опечатки и невнимательность;
    • непонимание логики участка кода;
    • незнание тонкостей языка разработки;
    • ложные тесты;
    • отсутствие обработки ошибок;
    • копирование чужих ошибок
  5. Баги в инструментах для разработки программного обеспечения
  6. Аппаратные проблемы(битая память, баг в процессорах Intel Skylake)
  7. Ошибки тестировщиков
  8. Некачественный контроль версий кода
  9. Архитектура программного обеспечения
  10. Недостаток финансирования

enter image description here

Последствия

  1. Ошибки в программном обеспечении медицинского аппарата лучевой терапии Therac-25 привели к превышению доз облучения нескольких людей. С июня 1985 года по январь 1987 года этот аппарат стал причиной шести передозировок радиации, некоторые пациенты получили дозы в десятки тысяч рад. Как минимум двое умерли непосредственно от передозировок
  2. Ракета Ariane 5: ущерб в 8,5 млрд долларов
    4 июня 1996 года случился неудачный запуск ракеты-носителя Ariane 5, которая была разработана Европейским космическим агентством. Ракета разрушилась на 39-й секунде полета из-за неверной работы бортового программного обеспечения. Эта история запомнилась, как одна из самых дорогостоящих компьютерных ошибок.
  3. Финансовая организация Knight Capital Group потеряла 440 миллионов долларов за 45 минут из-за ошибки в программе высокочастотного трейдинга.

Худшие компьютерные баги в истории
Самые дорогие и судьбоносные ошибки в ИТ-индустрии

Виды тестирования

Функциональное тестирование — это тестирование ПО в целях проверки реализуемости функциональных требований, то есть способности ПО в определённых условиях решать задачи, нужные пользователям. Функциональные требования определяют, что именно делает ПО, какие задачи оно решает.

Функциональные требования включают в себя:

  • Функциональная пригодность (англ. suitability).
  • Точность (англ. accuracy).
  • Способность к взаимодействию (англ. interoperability).
  • Соответствие стандартам и правилам (англ. compliance).
  • Защищённость (англ. security).

Не функциональное тестирование ПО — в первую очередь проверка на соответствие не функциональным требованиям:

  • Удобство (В основном производиться оценка удобства для пользователей)
  • Маштабируемость (проверяется как вертикальная так и горизонтальная маштабируемость тестируемого приложения)
  • Производительность (Способность работы приложения при различных нагрузках)
  • Безопасность (Защита пользовательских данных, защита данных приложения, стойкость на взлом)
  • Портируемость (Совместимость и переносимость приложения для и под различные окружения, платформы и т.д.)
  • Надежность (Поведение системы при различных непредвиденных ситуациях, способность обработки нестандартных действий пользователя)

И еще немного видов тестирования

test_table
Тестирование. Фундаментальная теория

Кто должен тестировать? (все, кто может)
test_table

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

  • Тестировщик - это тот, кто пишет и/или выполняет тестирование программного обеспечения с намерением продемонстрировать, что программа не работает.
  • Программист – это тот, чьи тестирования предназначены, чтобы показать, что программа действительно работает.
    (Борис Бейзер «Software Testing Techniques»)

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

TDD (test-driven development)
Разработка через тестирование — техника разработки программного обеспечения, которая основывается на повторении очень коротких циклов разработки: сначала пишется тест, покрывающий желаемое изменение, затем пишется код, который позволит пройти тест, и под конец проводится рефакторинг нового кода к соответствующим стандартам.
Картинки по запросу "tdd"

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

Что такое Assertion
Assertions (утверждения) — это инструкции, которые «утверждают» определенный кейс в программе. В Python они выступают булевыми выражениями, которые проверяют, является ли условие истинным или ложным.
Для проверки используется оператор assert

assert condition

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

Если же нужно добавить сообщение для вывода при ложном условии, то синтаксис будет таким.

assert condition, message
>>> a = None
>>> b = 2
>>> assert  a != b
>>> assert  a is not None and b is not None, 'a and b can not be empty'

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: a and can not be empty

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

Инструменты для тестирование

  • unittest
  • pytest
  • doctest
  • tox
  • nose

unittest

unittest - это framework для тестирования, входящий в стандартную библиотеку языка Python. Его архитектура выполнена в стиле xUnit. xUnit представляет собой семейство framework’ов для тестирования в разных языках программирования, в Java – это JUnit (а Unittest это порт JUnit), C# – NUnit и т.д.

В использовании unittest присутствуют несколько концепций:

  • test case -это наименьшая единица тестирования. Он проверяет конкретный ответ для конкретного набора входных данных.Для реализации этой сущности используется класс TestCase

  • test suite - это коллекция тестов, которая может в себя включать как отдельные test case’ы так и целые коллекции (т.е. можно создавать коллекции коллекций). Коллекции используются с целью объединения тестов для совместного запуска.

  • test runner - это компонент, который организует выполнение тестов и предоставляет результат пользователю.Test runner может иметь графический интерфейс, текстовый интерфейс или возвращать какое-то заранее заданное значение, которое будет описывать результат прохождения тестов.

  • test fixture - это фиксированное состояние объектов используемых в качестве исходного при выполнении тестов.

Цель использования fixture - если у вас сложный test case, то подготовка нужного состояния легко может занимать много ресурсов (например, вы считаете функцию с определенной точностью и каждый следующий знак точности в расчетах занимает день). Используя fixture (на сленге - фикстуры) предварительную подготовку состояния пропускаем и сразу приступаем к тестированию.

Test fixture может выступать, например, в виде:
– состояние базы данных
– набор переменных среды
– набор файлов с необходимым содержанием.

import unittest

class StringMethodTest(unittest.TestCase):

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])

Для того, чтобы метод класса выполнялся как тест, необходимо, чтобы он начинался со слова test. Несмотря на то, что методы framework’а unittest написаны не в соответствии с PEP 8 (ввиду того, что идейно он наследник xUnit), необходимо следовать правилам стиля для Python везде, где это возможно. Поэтому имена тестов будем начинать с префикса test_.

Методы, используемые при запуске тестов

  • setUp()
    Метод вызывается перед запуском теста. Как правило, используется для подготовки окружения для теста.

  • tearDown()
    Метод вызывается после завершения работы теста. Используется для закрытия файла, удаления данных, и т.д.

Методы setUp() и tearDown() вызываются для всех тестов в рамках класса, в котором они переопределены. По умолчанию, эти методы ничего не делают.

unittest TestCase setUp/tearDown work

import unittest

class WidgetTest(unittest.TestCase):
    def setUp(self):
        self.widget = Widget('The widget')

    def test_default_widget_size(self):
        self.assertEqual(self.widget.size(), (50,50),
                         'incorrect default size')

    def test_widget_resize(self):
        self.widget.resize(100,150)
        self.assertEqual(self.widget.size(), (100,150),
                         'wrong size after resize')

TestCase класс предоставляет набор assert-методов для проверки и генерации ошибок:

assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)
Это далеко не весь список, остальные методы описаны в документации.

Где Писать Тест

Начать написание теста можно с создания файла test.py, в котором будет содержаться ваш первый тест-кейс. Для тестирования у файла должна быть возможность импортировать ваше приложение, поэтому положите test.py в папку над пакетом.
По мере добавления новых тестов, ваш файл становится все более громоздким и сложным для поддержки, поэтому советуем создать папку tests/ и разделить тесты на несколько файлов. Названия всех файлов должны начинаться с test_, чтобы исполнители тестов понимали, что файлы Python содержат тесты, которые нужно выполнить. На больших проектах тесты делят на несколько директорий в зависимости от их назначения или использования.

Запуск тестов

  1. Command-Line Interface (CLI)
    Что-бы найти и запустить все тесты , можно просто вызвать модуль unittest, при этом будет запущен Test Discovery для поиска.
  python -m unittest 
  
..F
======================================================================
FAIL: test_split_2 (test_view.StringMethodTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/artem/PycharmProjects/pythonProject/test_view.py", line 15, in test_split_2
    self.assertEqual(s.split(), ['hello', 'wodrld'])
AssertionError: Lists differ: ['hello', 'world'] != ['hello', 'wodrld']

First differing element 1:
'world'
'wodrld'

- ['hello', 'world']
+ ['hello', 'wodrld']
?              +


----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

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

   python -m unittest tests
   python -m unittest test_something.py
   python -m unittest test_module1 test_module2
   python -m unittest test_module.TestClass
   python -m unittest test_module.TestClass.test_method
  1. Graphical User Interface (GUI)
    В PyCharm есть удобный способ запуска тестов:
    enter image description here

enter image description here

Пропуск тестов
Для пропуска тестов в unittest есть декораторы skip и skipIf, skipUnless.

class CalcBasicTests(unittest.TestCase):
	# Тест будет пропущен
    @unittest.skip("Temporaly skip test_add")    
    def test_add(self):
        self.assertEqual(calc.add(1, 2), 3)
    
    #  Тест будет пропущен, если условие (condition) истинно  
    @unittest.skipIf(condition, reason)   
    def test_sub(self):
        self.assertEqual(calc.sub(4, 2), 2)
        
   #  Тест будет пропущен если, условие (condition) не истинно. 
   @unittest.skipUnless(condition, reason)     
    def test_mul(self):
        self.assertEqual(calc.mul(2, 5), 10)
        
    def test_div(self):
        self.assertEqual(calc.div(8, 4), 2)

pytest

pip install pytest

pytest довольно мощный инструмент для тестирования, и многие разработчики
оставляют свой выбор именно на нем. pytest по “духу” ближе к языку Python нежели
unittest. Как было сказано выше, unittest в своей базе – xUnit, что накладывает
определенные обязательства при разработке тестов (создание классов-наследников
от unittest.TestCase, выполнение определенной процедуры запуска тестов и т.п.). При
разработке на pytest ничего этого делать не нужно, вы просто пишете функции, которые
должны начинаться с “test_” и используете assert’ы, встроенные в Python (unittest
использует свои).pytest также поддерживает выполнение тест-кейсов unittest.
Есть в нем и другие полезные функции:

  • Поддержка встроенных выражений assert вместо использования специальных self.assert*() методов;
  • Поддержка фильтрации тест-кейсов;
  • Возможность повторного запуска с последнего проваленного теста;
  • Экосистема из сотен плагинов, расширяющих функциональность.

Пример тест-кейса TestSum для pytest будет выглядеть следующим образом:

def test_sum():
    assert sum([1, 2, 3]) == 6, "Should be 6"

def test_sum_tuple():
    assert sum((1, 2, 2)) == 6, "Should be 6"

tox

pip install tox

tox — библиотека, автоматизирующая тестирование в нескольких виртуальных средах.
Tox настраивается через файл конфигурации в каталоге проекта. В нем содержится следующее:

  • Что установить
  • Какие версии Python использовать
  • Что сделать перед запуском тестов
  • Как запускать тесты
  • Что делать после запуска тестов

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

tox-quickstart

Инструмент настройки Tox задаст вам вопросы и создаст файл, похожий на следующий, в tox.ini:

[tox]
envlist = py27, py36

[testenv]
deps = requests
commands = python -m unittest

Запуск tox из командной строки:

tox

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

nose 2

pip install nose2

Цель nose2 - расширить unittest, чтобы сделать тестирование более приятным и понятным. Со временем, после написания сотни, а то и тысячи тестов для приложения, становится все сложнее понимать и использовать данные вывода unittest.

Nose совместим со всеми тестами, написанными с unittest фреймворком, и может заменить его тестовый исполнитель. Разработка nose, как приложения с открытым исходным кодом, стала тормозиться, и был создан nose2. Если вы начинаете с нуля, рекомендуется использовать именно nose2.

# in test_fancy.py
from nose2.tools import params

@params("Sir Bedevere", "Miss Islington", "Duck")
def test_is_knight(value):
    assert value.startswith('Sir')
nose2 -v --pretty-assert
test_fancy.test_is_knight:1
'Sir Bedevere' ... ok
test_fancy.test_is_knight:2
'Miss Islington' ... FAIL
test_fancy.test_is_knight:3
'Duck' ... FAIL

======================================================================
FAIL: test_fancy.test_is_knight:2
'Miss Islington'
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/mnt/ebs/home/sirosen/tmp/test_fancy.py", line 6, in test_is_knight
    assert value.startswith('Sir')
AssertionError

>>> assert value.startswith('Sir')

values:
    value = 'Miss Islington'
    value.startswith = <built-in method startswith of str object at 0x7f3c3172f430>
======================================================================
FAIL: test_fancy.test_is_knight:3
'Duck'
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/mnt/ebs/home/sirosen/tmp/test_fancy.py", line 6, in test_is_knight
    assert value.startswith('Sir')
AssertionError

>>> assert value.startswith('Sir')

values:
    value = 'Duck'
    value.startswith = <built-in method startswith of str object at 0x7f3c3172d490>
----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=2)

doctest

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

  • Для того, чтобы проверить актуальность строк документации, убедившись, что все интерактивные примеры работают именно так, как задокументировано.
  • Чтобы организовать регрессионное тестирование, проверяя, что интерактивные примеры из тестового файла или тестового объекта работают как ожидается.
  • Чтобы написать руководство для пакета, иллюстрированное примерами ввода-вывода. В зависимости от того, на что обращается внимание - на примеры или на пояснительный текст, это можно назвать либо “литературным тестированием”, либо “исполняемой документацией”.
"""
Это модуль-пример.

Этот модуль предоставляет одну функцию - factorial().  Например,

>>> factorial(5)
120
"""

def factorial(n):
    """Возвращает факториал числа n, которое является числом >= 0.

 Если резульатат умещается в int, возвращается int.
 Иначе возвращается long.

 >>> [factorial(n) for n in range(6)]
 [1, 1, 2, 6, 24, 120]
 >>> [factorial(long(n)) for n in range(6)]
 [1, 1, 2, 6, 24, 120]
 >>> factorial(30)
 265252859812191058636308480000000L
 >>> factorial(30L)
 265252859812191058636308480000000L
 >>> factorial(-1)
 Traceback (most recent call last):
 ...
 ValueError: n must be >= 0

 Можно вычислять факториал числа с десятичной частью, если она
 равна 0:
 >>> factorial(30.1)
 Traceback (most recent call last):
 ...
 ValueError: n must be exact integer
 >>> factorial(30.0)
 265252859812191058636308480000000L

 Кроме того, число не должно быть слишком большим:
 >>> factorial(1e100)
 Traceback (most recent call last):
 ...
 OverflowError: n too large
 """

    import math
    if not n >= 0:
        raise ValueError("n must be >= 0")
    if math.floor(n) != n:
        raise ValueError("n must be exact integer")
    if n+1 == n:  # перехватываем значения типа 1e300
        raise OverflowError("n too large")
    result = 1
    factor = 2
    while factor <= n:
        result *= factor
        factor += 1
    return result

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Если запустить example.py прямо из командной строки, то, doctest выполнит своё волшебство:

python example.py

Тут нет никакого вывода. Это нормально и это означает, что все примеры работают. Если передать -v скрипту , то doctest выведет детальный лог того, что он делает и подведёт итог в конце:

python example.py -v
Trying:
    factorial(5)
Expecting:
    120
ok
Trying:
    [factorial(n) for n in range(6)]
Expecting:
    [1, 1, 2, 6, 24, 120]
ok
Trying:
    [factorial(long(n)) for n in range(6)]
Expecting:
    [1, 1, 2, 6, 24, 120]
ok

И так далее, вплоть до:

    factorial(1e100)
Expecting:
    Traceback (most recent call last):
        ...
    OverflowError: n too large
ok
2 items passed all tests:
   1 tests in __main__
   8 tests in __main__.factorial
9 tests in 2 items.
9 passed and 0 failed.
Test passed.
$

mock

Mock на английском значит «имитация», «подделка». Модуль с таким названием помогает сильно упростить тесты модулей на Python.

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

>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock
<Mock id='140395983149664'>

>>> mock.some_attribute
<Mock name='mock.some_attribute' id='4394778696'>
>>> mock.do_something()
<Mock name='mock.do_something()' id='4394778920'>

Какие преимущества имеет mock?

  • Высокая скорость
    Быстрые тесты бывают очень полезны. Например, если у вас есть ресурсоемкая функция, mock для этой функции сократит ненужное использование ресурсов во время тестирования, тем самым сократив время выполнения теста.

  • Избежание нежелательных побочных эффектов во время тестирования
    Если вы тестируете функцию, которая вызывает внешний API, скорее всего вам не особо хочется каждый раз вызывать API для того, чтобы запустить тест. Вам придется менять код каждый раз, когда изменяется API, или могут быть некоторые ограничения скорости, но mock помогает этого избежать.

Цепочки атрибутов:

>>> m = Mock()
>>> m
<Mock id='167387660'>

>>> m.any_attribute
<Mock name='mock.any_attribute' id='167387436'>

>>> m.any_attribute
<Mock name='mock.any_attribute' id='167387436'>

>>> m.another_attribute
<Mock name='mock.another_attribute' id='167185324'>>

Обращение к атрибуту выдаёт ещё один экземпляр класса Mock, а повторное обращение к тому же атрибуту — снова тот же экземпляр. Атрибут может быть чем угодно, в том числе и функцией. Наконец, любой макет можно вызвать (скажем, вместо класса):

>>> m()
<Mock name='mock()' id='167186284'>

>>> m() is m
False

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

Если мы назначим атрибуту значение, то никаких сюрпризов: при следующем обращении получим именно это значение:

>>> m.any_attribute
<Mock name='mock.any_attribute' id='167387436'>
>>> m.any_attribute = 5
>>> m.any_attribute
5

return_value и side_effect

Предположим вам нужно убедиться что ваш код в рабочие и в выходные дни ведёт себя по-разному, а код подразумевает использование строенной библиотеки datetime

from datetime import datetime

def is_weekday():
    today = datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return 0 <= today.weekday() < 5

# Test if today is a weekday
assert is_weekday()

Если запустить этот тест в воскресенье, то будет эксепшен, что же с этим делать? Замокать… Mock объект может возвращать по вызову любой функции необходимое нам значение, посредством заполнения return_value

import datetime
from unittest.mock import Mock

# Save a couple of test days
tuesday = datetime.datetime(year=2019, month=1, day=1)
saturday = datetime.datetime(year=2019, month=1, day=5)

# Mock datetime to control today's date
datetime = Mock()


def is_weekday():
    today = datetime.datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return 0 <= today.weekday() < 5


# Mock .today() to return Tuesday
datetime.datetime.today.return_value = tuesday
# Test Tuesday is a weekday
assert is_weekday()
# Mock .today() to return Saturday
datetime.datetime.today.return_value = saturday
# Test Saturday is not a weekday
assert not is_weekday()

Если необходимо, что бы после повторного вызова получить другие результаты то, в этом поможет side_effect, которыйработает так же как и return_value только принимает перебираемый объект и с каждым вызовом возвращает следующее значение.

>>> mock_poll = Mock(side_effect=[None, 'data'])
>>> mock_poll()
None
>>> mock_poll()
'data'

Или как на прошлом примере

import datetime
from unittest.mock import Mock

# Save a couple of test days
tuesday = datetime.datetime(year=2019, month=1, day=1)
saturday = datetime.datetime(year=2019, month=1, day=5)

# Mock datetime to control today's date
datetime = Mock()


def is_weekday():
    today = datetime.datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return 0 <= today.weekday() < 5


# Mock .today() to return Tuesday first time and Saturday second time
datetime.datetime.today.side_effect = [tuesday, saturday]
assert is_weekday()
assert not is_weekday()

Декоратор patch
Есть класс, который имитирует длительные вычисления:

import time

class Calculator:
    def sum(self, a, b):
        time.sleep(10)  # long running process
        return a + b

И тест к этой функции:

from unittest import TestCase
from main import Calculator


class TestCalculator(TestCase):
    def setUp(self):
        self.calc = Calculator()

    def test_sum(self):
        answer = self.calc.sum(2, 4)
        self.assertEqual(answer, 6)

Этот тест будет идти 10 секунд, имитирую длительный процесс, но можно симитировать выполнение этого метода.

from unittest import TestCase
from unittest.mock import patch


class TestCalculator(TestCase):
    @patch('main.Calculator.sum', return_value=9)
    def test_sum(self, sum):
        self.assertEqual(sum(2, 3), 9)

или

from unittest import TestCase
from unittest.mock import patch


class TestCalculator(TestCase):
    @patch('main.Calculator.sum')
    def test_sum(self, sum):
        sum.return_value = 9
        self.assertEqual(sum(2, 3), 9)

Пропатченные методы попадают в аргументы метода теста.

Более продвинутый пример использования

import requests

class Blog:
    def __init__(self, name):
        self.name = name

    def posts(self):
        response = requests.get("https://jsonplaceholder.typicode.com/posts")

        return response.json()

    def __repr__(self):
        return '<Blog: {}>'.format(self.name)

Этот код определяет класс Blog с методом posts. Запустив posts в Blog, произойдет инициирование вызова API. Ссылаясь на post в Blog, объект будет инициировать вызов API jsonplaceholder.

В данном тесте необходимо имитировать непредвиденный вызов API и проверить, что функция posts объекта Blog возвращает posts. Необходимо будет исправить все posts объекта Blog следующим образом.

from unittest import TestCase
from unittest.mock import patch, Mock


class TestBlog(TestCase):
    @patch('main.Blog')
    def test_blog_posts(self, MockBlog):
        blog = MockBlog()

        blog.posts.return_value = [
            {
                'userId': 1,
                'id': 1,
                'title': 'Test Title',
                'body': 'Far out in the uncharted backwaters of the unfashionable end of the western spiral arm of the Galaxy\ lies a small unregarded yellow sun.'
            }
        ]

        response = blog.posts()
        self.assertIsNotNone(response)
        self.assertIsInstance(response[0], dict)

Вы можете обратить внимание, что функция test_blog_posts украшена декоратором @patch. Когда функция оформлена через @patch, mock класса, метода или функции, переданная в качестве цели для @patch, возвращается и передается в качестве аргумента декорируемой функции.

В этом случае @patch вызывается с помощью main.Blog и возвращает mock, который передается функции теста как MockBlog. Важно отметить, что цель, перешедшая к @patch, должна быть импортирована в @patch, из которой она была вызвана. В нашем случае импорт формы from main import Blog должен быть разрешен без каких либо проблем.

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

Вызов blog.posts() на нашем ложном объекте блога возвращает предопределенный JSON.
Обратите внимание, что тестирование mock вместо фактического объекта блога, позволяет нам делать дополнительные утверждения о том, как mock использовался.

Например, mock позволяет проверить, сколько раз он вызывался, аргументы, с которыми он вызывался, и даже был ли mock вообще когда либо вызван.

Литература (Что почитать)

  1. Адаптированый перевод документации по unittest на devpractice.ru
  2. Mock, и на русском
  3. Знакомство с тестированием в Python на habr.com: 1 часть, 2 часть, 3 часть
  4. Введение в тестирование в Python на webdevblog.ru
  5. Лекция от Яндекс