【问题标题】:How to create unit test for methods without return statement in Pytest?如何在 Pytest 中为没有返回语句的方法创建单元测试?
【发布时间】:2020-07-31 04:40:47
【问题描述】:

我正在尝试对类中没有返回值的函数进行 pytest:

# app.py
from utils import DBConnection


class App:
  def add_ticket_watcher(self, ticket_key, watcher_name):
    if ticket_key is None:
      raise ValueError('Ticket key is empty')

    instance = DBConnection()
    instance.save(ticket_key, watcher_name)

从上面的代码中,add_ticket_watcher() 是一个没有返回语句的方法。我从这个article 中了解到,我们可以使用模拟来模拟这种方法的预期行为。

对于pytest的模拟功能,我发现我们可以使用monkeypatch

所以,我的方法是为 add_ticket_watcher() 执行 2 个测试用例:

  • 测试有效票据密钥
  • 测试无效票证

我有类似的东西:

# test_app.py
import pytest
from src.app import App


@pytest.fixture
def app():
    app = App()
    return app


# Positive test case: Setup parameterize test for valid ticket key
add_valid_watcher_data = (
    ('func_name', 'ticket_key', 'watcher_name', 'expected_result', 'comment'),
    [
        ('add_ticket_watcher', 'TIC-13527', 'someone', None, 'add watcher at valid ticket key'),
    ]
)


@pytest.mark.parametrize(*add_valid_watcher_data)
def test_add_ticket_watcher(monkeypatch, app, func_name, ticket_key, watcher_name, expected_result, comment):

    def mock_add_ticket_watcher(ticket_key, watcher_name):
        # Since `add_ticket_watcher()` has no return statement, I'm mocking the result as None
        return None

    monkeypatch.setattr(app, "add_ticket_watcher", mock_add_ticket_watcher)

    # Run each input parameter from add_valid_watcher_data to `add_ticket_watcher()`
    watcher = getattr(app, func_name)(ticket_key, watcher_name)

    # If watcher has None value, then `add_ticket_watcher()` execution is successful.
    assert expected_result == watcher


# Negative test case: Setup parameterize test for invalid ticket key
add_invalid_watcher_data = (
    ('func_name', 'ticket_key', 'watcher_name', 'exception_message', 'comment'),
    [
        ('add_ticket_watcher', 'TIC-xx', 'someone', 'Ticket key is empty', 'add watcher at invalid ticket key'),
        ('add_ticket_watcher', None, 'someone', 'Ticket key is empty', 'ticket key has None value'),
    ]
)


@pytest.mark.parametrize(*add_invalid_watcher_data)
def test_exception_add_ticket_watcher(app, func_name, ticket_key, watcher_name, exception_message, comment):
    with pytest.raises(ValueError, match=exception_message):
      # Run each input parameter from add_invalid_watcher_data to `add_ticket_watcher()`
      getattr(app, func_name)(ticket_key, watcher_name)

test_add_ticket_watcher(),我不确定要断言什么。但是,由于App.add_ticket_watcher(ticket_key, watcher_name) 没有返回语句。我创建了一个模拟函数来返回None

有没有更好的方法来达到同样的目的?

如何在 Pytest 中为没有返回语句的方法创建单元测试?

【问题讨论】:

  • 没有具体的答案。理想情况下,您可以通过某种方式从外部验证 add_watcher 应该进行的任何更新实际上是否已完成。如果做不到这一点,你能做的最好的事情就是使用一个模拟来验证它是使用你期望的参数调用的。
  • 那个测试很难理解,我认为它没有意义。似乎ServerInterfaceServer 的包装器/外观。如果server_interface 是您正在测试的东西,那么您应该对其进行修补。对此进行测试的困难源于决定让ServerInterface 创建自己的Server 实例——阅读依赖倒置。如果您可以注入一个模拟 服务器 并检查与它的适当交互,那可能会更直接。
  • @chepner:感谢您的回复。我更新了上面的代码 sn-p 以显示 add_ticket_watcher() 里面的内容。从更新的代码中,add_ticket_watcher() 正在将ticket_key、watcher_name 添加到数据库中。您能否详细说明外部验证是什么意思?
  • @jonrsharpe:感谢您的意见。我更新了上面的代码 sn-p 并缩小了范围以更多地关注 Server instance 而不是 ServerInterface instance。我将 Server 类 合并到 App 类add_ticket_watcher() 行为是将ticket_key、watcher_name 添加到数据库中。我不确定要断言什么,因为该函数没有返回语句。我创建了一个模拟函数来返回None
  • 现在你不是在测试任何东西,你在调用猴子补丁。

标签: python python-3.x unit-testing pytest monkeypatching


【解决方案1】:

除了 chepner 提到的。您可以测试是否使用 mock 调用了 logger.info。 并且为了测试负面场景,你可以使用 mock side_effects 强制它引发异常,然后你可以测试 logger.exception 是否被调用。

【讨论】:

  • 感谢您的回复。您能否详细说明使用模拟副作用强制负面场景引发异常是什么意思?
【解决方案2】:

我不熟悉 pytest 但 unittest。但是,我会为您的函数编写这 3 个测试(对您现有的函数进行一些修改:))

注意:测试方法名称是描述性的,因此,我不会添加额外的 cmets。

app.py

from utils import DBConnection
import cx_Oracle

class App:

  def add_ticket_watcher(self, ticket_key, watcher_name):
    if ticket_key is None:
      raise ValueError('Ticket key is empty')

    instance = DBConnection()

    try:
        instance.save(ticket_key, watcher_name)
    except Exception as e:
        raise cx_Oracle.DatabaseError('Database save failed')

test_app.py

import unittest
import app
import cx_Oracle
from mock import patch

class TestApp(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.app = app.App()

    def test_if_exception_raised_when_ticket_value_is_none(self):
        with self.assertRaises(ValueError):
            self.app.add_ticket_watcher(None, 'dummyname')

    def test_if_dbconnection_save_is_called(self):
        with patch('app.DBConnection.save') as mock_dbconn_save:
            self.app.add_ticket_watcher(123, 'dummyname')
            self.assertTrue(mock_dbconn_save.called)

    def test_if_dbconnection_save_raises_error(self):
        with patch('app.DBConnection.save', side_effect = Exception) as mock_dbconn_save_exc:
            with self.assertRaises(cx_Oracle.DatabaseError):
                self.app.add_ticket_watcher(123, 'dummyname')

if __name__ == '__main__':
    unittest.main(verbosity=2)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-11-21
    • 2013-07-08
    • 2022-10-30
    • 2021-06-10
    • 1970-01-01
    • 2014-11-08
    • 2022-12-18
    • 2011-08-14
    相关资源
    最近更新 更多