Форум сайта python.su
Здравствуйте,
Я продолжаю играться с тестами, и вот есть у меня такая функция:
# file src/secret_service.py def remove_obsolete_files(file_list): for filename in file_list: try: # кусок кода вырезан: валидация, добавление правильного пути и т.п. os.remove(full_filename) except: # filename can be None pass
import mock from src.secret_service import remove_obsolete_files def test_remove_obsolete_files(): mock_remove = mock.MagicMock() with mock.patch('src.secret_service.os.remove', mock_remove): remove_obsolete_files(['file1', None, 'file2']) # тогда mock_remove имеет атрибут mock_calls и я могу проверить как угодно
Отредактировано Master_Sergius (Июнь 1, 2020 20:26:53)
Офлайн
Master_Sergius
Знаете, почему лично я люблю юнит-тесты? Потому что они очень ярко маркируют плохой код. Если юнит-тест пишется трудно, если возникают вопросы, это значит, что код плохой.
В вашем случае, код плохой, потому что нарушает принцип единой ответственности. Ваша функция удаляет ненужные файлы? Ок, это её ответственность. А вот получать полное имя на основе короткого, это ответственность другой функции.
def get_full_name(name): pass def remove_obsolete_files(file_list): for filename in file_list: try: os.remove(get_full_name(filename )) except: # filename can be None pass
Офлайн
Master_Sergiushttps://mockito-python.readthedocs.io/en/latest/the-functions.html#mockito.patch
Обычно, я юзал mock.patch:
Офлайн
FishHook
Master_SergiusЗнаете, почему лично я люблю юнит-тесты? Потому что они очень ярко маркируют плохой код. Если юнит-тест пишется трудно, если возникают вопросы, это значит, что код плохой.В вашем случае, код плохой, потому что нарушает принцип единой ответственности. Ваша функция удаляет ненужные файлы? Ок, это её ответственность. А вот получать полное имя на основе короткого, это ответственность другой функции.
Офлайн
py.user.next
https://mockito-python.readthedocs.io/en/latest/the-functions.html#mockito.patch
Офлайн
Master_Sergius
смысл юнит тестов не в тестировании циклов, а в том, что у каждого юнита есть ответственность и вы тестируете эту ответственность. Функция может либо возвращать некие данные, либо изменять состояние какого-то объекта. В вашем случае функция получает список строк и изменяет состояние объекта os. Этой функции плевать, что именно происходит с файловой системой, это не её ответственность. Вам надо сравнить состояние изменяемого объекта до и после применения функции. Если объект изменился ожидаемо, значит функция работает правильно.
my_module.py
import os def get_full_name(name): return 'bala-bla' + name def remove_obsolete_files(file_list): for filename in file_list: try: os.remove(get_full_name(filename )) except: # filename can be None pass
class _os: def __init__(self): self.filenames = ['a', 'b', 'd', 'e'] def remove(self, name): self.filenames = [x for x if x != name] def _get_full_name(name): return name @mock(os=_os, get_full_name=_get_full_name) def test_remove_obsolete_files(file_list): filenames_to_remove = ['a', 'b', 'c'] remove_obsolete_files(filenames_to_remove ) assert os.filenames == ['d', 'e']
Отредактировано FishHook (Июнь 2, 2020 14:19:38)
Офлайн
Мысль Вашу понял, но некоторые вещи хотелось бы уточнить. Двайте изменим чуть задачу, допустим получение списка полных имен я вынес в отдельную функцию, а os.remove я мокаю в любом случае. То есть, имеем такую функцию:
def remove_obsolete_files(file_list): for filename in file_list: try: os.remove(filename) except: # filename can be None pass
Офлайн
Master_SergiusЯ же вам написал выше - ваша функция изменяет что-то в файловой системе. Задача теста проверить, то ли она изменяет что нужно. Сама файловая система нам не важна, что имеено происходит по вызову os.remove - не имеет значения. У нас есть некий набор файлов в файловой системе. После применения функции набор файлов должен измениться. Что такое набор файлов - не важно. Что такое файл - плевать. Важно вот это самое изменение, за которое отвечает функция. Соответственно, все что нам не важно мы заменяем на затычки - объекты, которые ведут себя предсказуемо в рамках теста.
Какой тест (или тесты) мы можем написать к данной функции? Просто проверить, был ли вызов os.remove с таким-то аргументом, так ведь?
class _os: def __init__(self): self.filenames = ['a', 'b', 'd', 'e'] def remove(self, name): self.filenames = [x for x if x != name] @mock(os=_os()) def test_remove_obsolete_files(file_list): filenames_to_remove = ['a', 'b', 'c'] remove_obsolete_files(filenames_to_remove) assert os.filenames == ['d', 'e']
Офлайн
Хм, интересный подход, у меня была другая задумка, но наверное сделаю по-вашему. Спасибо.
Офлайн