Mock-объекты • mock (англ.) – ложный, фиктивный, мнимый, фальшивый, поддельный Первый пример • Мы хотим протестировать функцию f: def f(x): return g(x) * g(x) НО: 1) Функция g еще не написана ИЛИ: 2) Функция g работает недетерминировано: def g(x): return randint(5 * x) ИЛИ: 3) Функция g слишком сложна, в ней тоже могут быть ошибки. Хотим протестировать f независимо ИЛИ: 4) Функция g имеет нежелательные при тестировании побочные эффекты: def g(x): file = open(“1.txt”, “w”) file.write(‘blablabla’) Простейшее решение • Пишем заглушку (stub): def g(x): return 5 ИДЕЯ: заменить настоящую функцию g на более удобную при тестировании Более сложный пример def f(x): if x > 0 and x < 10: return g(x) elif x <= 0 return g(x) * g(-x) else: return 0 Что хочется протестировать? Требуется убедиться, что функция f вызывает функцию g только в нужных случаях и нужное количество раз. ИДЕЯ: Наша фальшивая g не только возвращает фиксированное значение, но и запоминает информацию о своих вызовах. Что для этого нужно? 1) Использовать g как объект (хранить состояние) 2) Чтобы объект можно было вызвать – () – он должен быть callable Как это должно выглядеть? g = … f(5) g.assert_called_once_with(5) f(-5) g.assert_any_call(5) g.assert_any_call(-5) Стандартный модуль unittest.mock (начиная с python3.3) from unittest.mock import Mock g = Mock(return_value = 5) … “Мокаем” метод объекта def is_empty(group): return len(group.members()) == 0 group1 = Group() group1.members = Mock(return_value= [‘aaa’, ‘bbb’, ‘ccc’]) is_empty(group1) group1.members.assert_called_with() “Мокаем” объект group1 = Mock() group1.members.return_value = [‘aaa’, ‘bbb’, ‘ccc’] Все необходимые методы/поля создаются “на лету”: group1.count() НО: mock.__str__.return_value = ‘foo’ вызовет исключение magic-методы (__xxx__) Mock не создает (они часто используются неявно, например, __str__, __eq__ и их автоматическое переопределение не всегда желательно) MagicMock • Аналог Mock, дополнительно позволяющий создавать magic-методы from unittest.mock import MagicMock m = MagicMock() m.__str__.return_value = 'foo' print(m) Выведет foo Объекты из стандартных библиотек …тоже можно мокать: from unittest.mock import Mock import random random.randint = Mock(return_value = 100) print(random.randint(10)) Мокать можно всё from unittest.mock import Mock print = Mock() print('***') Mock-объект хранит историю вызовов >>> mock = Mock(return_value=None) >>> mock() >>> mock(3, 4) >>> mock(key='fish', next='w00t!') >>> mock.call_args_list [call(), call(3, 4), call(key='fish', next='w00t!')] …которую можно сравнивать с ожидаемыми значениями [call(), call(3, 4), call(key='fish', next='w00t!')] >>> expected = [(), ((3, 4),), ({'key':'fish', 'next':'w00t!'},)] >>> mock.call_args_list == expected True Если return_value не определено … то возвращается Mock-объект: >>> open = Mock() >>> f = open("1.txt") >>> print(f) <Mock name='mock()' id='40790112'> Для его методов также создаются новые Мockобъекты: >>> s = f.read() >>> print(s) <Mock name='mock().read()' id='40688664'> НО! from unittest.mock import Mock open = Mock() with open("1.txt") as f: s = f.read() вызовет исключение: AttributeError: __exit__ Замена на MagicMock лечит но проблемы остаются… Решение из коробки: from unittest.mock import mock_open open = mock_open(read_data='To be or...') with open("1.txt") as f: print(f.read()) print(open.mock_calls) Выведет To be or... [call('1.txt'), call().__enter__(), call().read(), call().__exit__(None, None, None)] Преимущества mock_open 1) Корректно работает и вместо отдельного open, и в конструкции с with 2) Необязательный параметр read_data (только для read!!!) 3) поддерживает только стандартный методы для файлов и выдает исключения для остальных: >>> f.aaa() AttributeError: Mock object has no attribute 'aaa' patch func.py: def f(x): return g(x) * g(x) def g(x): ... func_test.py import unittest from func import f class FTest(unittest.TestCase): def test_f(self): result = f(5) self.assertEqual(result, 9) if __name__ == '__main__': unittest.main() patch from unittest.mock import patch ... @patch('func.g', return_value = 3) def test_f(self, mock_g): result = f(5) self.assertEqual(result, 9) Преимущества Mock-объектов 1) Независимое тестирование одного метода или класса 2) Скорость тестирования (не выполняется лишний код) 3) Стимулирует к правильному проектированию 4) Избавление от недетерминированного поведения 5) Генерация сложно воспроизводимых в реальных условиях ошибок 6) Запись и анализ логов вызовов функций