Как проверить с помощью pytest, была ли вызвана одна функция внутри метода сохранения формы Django?

#django #pytest

#django #pytest

Вопрос:

Что у меня есть

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

 class UserAdminCreationForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ()
 
    def save(self, commit: bool = True) -> U:
        user = super(UserAdminCreationForm, self).save(commit=False)
        data = self.cleaned_data
        # Set random password for every new user
        random_password = generate_random_password()
        user.set_password(random_password)
        # Send email confirmation with credentials to login
        email = data.get("email")
        html_message = render_to_string(
            template_name="mails/user_creation_notification.html",
            context={"email": email, "password": random_password},
        )
        # Strip the html tag. So people can see the pure text at least.
        plain_message = strip_tags(html_message)
        send_mail(
            subject="Bienvenido a TodoTránsito",
            message=plain_message,
            recipient_list=[email],
            html_message=html_message,
        )
        # Save into the DB the new user
        if commit:
            user.save()
        return user
  

Проблема

Я использую pytest для тестирования своего кода Django, но я не знаю, как утверждать, были ли вызваны эти функции.

Комментарии:

1. Разве вы не можете просто добавить print("in function_name") оператор внутри каждой функции?

2. @GAEfan Я говорю о тестировании, мне нужно подтвердить, была ли вызвана одна функция внутри моего метода сохранения формы. проверьте The problem раздел.

3. Вы хотите издеваться над функциями или вы хотите, чтобы они работали как обычно и просто проверяли, были ли они вызваны?

4. В первом случае вы можете использовать @mock.patch , во втором случае вы могли бы использовать mocker.spy from pytest-mock .

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

Ответ №1:

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

 from unittest import mock

@mock.patch('some_module.generate_random_password', return_value='swordfish')
@mock.patch('some_module.send_mail')
def test_save_user(mocked_send_mail, mocked_generate_password):
    user_form = create_user_form()  # whatever you do to create the form in the test
    user_from.save()
    mocked_generate_password.assert_called_once()
    mocked_send_mail.assert_called_once()
    # or mocked_send_mail.assert_called_once_with(...) if you want to check the parameters it was called with
  

Обратите внимание, что вы должны убедиться в том, что вы смоделировали правильный модуль, например, тот, который использовался в тестируемом коде (см. Где нужно исправить).
В этом случае generate_random_password заменяется макетом, который всегда возвращает один и тот же пароль, и send_mail заменяется макетом, который ничего не делает, кроме записи вызовов. Доступ к обоим макетам можно получить через аргументы в тесте, которые вводятся patch декораторами (сначала последним декоратором, имена аргументов произвольны).

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

 def test_save_user(mocker):
    mocked_generate_password = mocker.patch('some_module.generate_random_password', return_value='swordfish')
    mocked_send_mail = mocker.patch('some_module.send_mail')
    user_form = create_user_form()  # whatever you do to create the form in the test
    user_from.save()
    mocked_generate_password.assert_called_once()
    mocked_send_mail.assert_called_once()
  

Если вы теперь хотите использовать real generate_random_password , но все еще хотите посмотреть, была ли она вызвана, вы можете использовать mocker.spy вместо этого:

 def test_save_user(mocker):
    mocked_generate_password = mocker.spy(some_module, 'generate_random_password')
    mocked_send_mail = mocker.patch('some_module.send_mail')
    user_form = create_user_form()  # whatever you do to create the form in the test
    user_from.save()
    mocked_generate_password.assert_called_once()
    mocked_send_mail.assert_called_once()
  

Обратите внимание, что вы можете добиться того же с помощью unittest.mock.patch.object , но, на мой взгляд, менее удобно.

Комментарии:

1. Когда вы используете mocked_generate_password , что вы имеете в виду?

2. Это макет для generate_random_password — я адаптировал ответ, чтобы сделать это более понятным. В примере он настроен на то, чтобы всегда возвращать «swordfish».

3. Во втором примере с pyest-mock , который вы используете mocked_generate_password.assert_called_once() , мне нужно сохранить mocker.patch в переменной like mocked_send_mail = mocker.patch("app_todotransito_co.users.forms.send_mail") , чтобы иметь возможность утверждать этот метод like mocked_send_mail.assert_called_once() , потому что, когда я запускаю тест, это выдается NameError: name 'generate_random_password' is not defined .

4. Ах, извините, забыл это адаптировать. Не на ПК, поэтому я допускаю ошибки…

5. Вызывающий код должен отображаться как протестированный, сами функции, замененные на макет (like send_mail ), нет.