Найти - Пользователи
Полная версия: mockito mock(os.remove) и проверка вызовов
Начало » Python для экспертов » mockito mock(os.remove) и проверка вызовов
1
Master_Sergius
Здравствуйте,
Я продолжаю играться с тестами, и вот есть у меня такая функция:

  
 # 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

Я хочу проверить, что os.remove будет вызываться с правильными full_filename. Обычно, я юзал mock.patch:

  
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 и я могу проверить как угодно

Но, в текущем проекте повсюду используется библиотека mockito, а не mock, и с ней у меня не получается такого сделать, то есть даже замокать os.remove не удается через эти его when.thenReturn, verify и т.д.
Можно ли это сделать с помощью mockito и как?
FishHook
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

Есть проблемы с тестированием функции get_full_name? Нету. Значит этот код лучше.
py.user.next
Master_Sergius
Обычно, я юзал mock.patch:
https://mockito-python.readthedocs.io/en/latest/the-functions.html#mockito.patch
Master_Sergius
FishHook
Master_SergiusЗнаете, почему лично я люблю юнит-тесты? Потому что они очень ярко маркируют плохой код. Если юнит-тест пишется трудно, если возникают вопросы, это значит, что код плохой.В вашем случае, код плохой, потому что нарушает принцип единой ответственности. Ваша функция удаляет ненужные файлы? Ок, это её ответственность. А вот получать полное имя на основе короткого, это ответственность другой функции.

Полностью с вами согласен, но хотелось бы сам цикл протестировать все равно
Master_Sergius
py.user.next
https://mockito-python.readthedocs.io/en/latest/the-functions.html#mockito.patch

Надо же…. совпадение? не думаю. Немного разница есть, тут надо verify использовать, но я разобрался как, большое спасибо всем.
FishHook
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

test.py
 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']

декоратор mock здесь условный, может быть сколько угодно реализаций, как замокать объект. Главное - смысл юнит тестирования. Тестируйте только ответственность юнита, и мокайте зависимости.
Master_Sergius
Мысль Вашу понял, но некоторые вещи хотелось бы уточнить. Двайте изменим чуть задачу, допустим получение списка полных имен я вынес в отдельную функцию, а os.remove я мокаю в любом случае. То есть, имеем такую функцию:

  
def remove_obsolete_files(file_list):
    for filename in file_list:
        try:
             os.remove(filename)
        except:
            # filename can be None
            pass

Какой тест (или тесты) мы можем написать к данной функции? Просто проверить, был ли вызов os.remove с таким-то аргументом, так ведь?
FishHook
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']

мы знаем, что у нас есть зависимость - некий os и у него обязательно должен быть метод remove. Мы создаем объект-заглушку для него, чтобы абстрагироваться от реальной файловой системы. Мы знаем состояние этого объекта до теста (потому что сами это состояние и создали.) Мы знаем его состояние после теста. Сравниваем - это результат теста.
Master_Sergius
Хм, интересный подход, у меня была другая задумка, но наверное сделаю по-вашему. Спасибо.
This is a "lo-fi" version of our main content. To view the full version with more information, formatting and images, please click here.
Powered by DjangoBB