【问题标题】:How to mock a function of a class in an imported module but outside the scope of a function?如何模拟导入模块中但在函数范围之外的类的函数?
【发布时间】:2022-12-07 00:15:53
【问题描述】:

我想弄清楚如何使用 @patch.object 来模拟一个函数,这里是 log.write(),它被导入但没有在模块的函数内部使用。这样的教程,https://www.pythontutorial.net/python-unit-testing/python-patch/ , 指出修补需要在使用它的目标,而不是它来自的地方。但是,使用的每个示例都显示了要在另一个函数中模拟的目标。在下面提到的用例中,记录器被导入并用于在函数范围之外写入日志。有没有办法模拟main.pyrouters.py 中的行为?

源代码/apis/main.py

from apis.routers import routes
from fastapi import FastAPI, Request
from starlette.background import BackgroundTask
from utils.log import Logger

app = FastAPI()
app.include_router(routers.router)

log = Logger(name="logger-1")
log.write("logger started")

@app.middleware("http")
async def add_process_time_header(request: Request, call_next: Any):
    try:
        response = await call_next(request)
    except Exception as exc:     
        background = BackgroundTask(
            log.write, message=f"Reason: error")
    return JSONResponse(
        status_code=500,
        content={"reason": "error"},
        background=background
    )

    return response
    

在 src/apis/routers/routes.py

from utils.log import Logger
from fastapi import APIRouter, BackgroundTask
router = APIRouter()

log = Logger(name="logger-1")

@router.get("/test")
def test(background_tasks: BackgroundTasks):
    background_tasks.add_task(
    log.write, message=f"Logging done.")

在 utils/log/logging.py 中

import logging
Class Logger:
     def __init__(self, name):
          # creates a logger
     def write(self, message):
          # writes log

【问题讨论】:

  • 问题是代码在导入 main 时已经执行,如果您尝试模拟它也是如此。避免这种情况(以及其他问题)的通常做法是将该初始化代码置于 if __name__ == "__main__:" 条件下。

标签: python unit-testing python-unittest python-mock python-unittest.mock


【解决方案1】:

提问的时候,提供Minimal Reproducible Example非常方便。因此,删除不必要的 fastapi 和 starlette,并提供您尝试编写的测试代码。

这里是 :

# file: so74695297_main.py
from so74695297_log import Logger
from so74695297_routes import my_route

log = Logger(name="logger-1")
log.write("logger started")


def main():  # FAKE IMPL
    log.write(message=f"in main()")
    my_route()


if __name__ == "__main__":
    import logging
    logging.basicConfig(level=logging.INFO)  # for demo
    main()
# file: so74695297_routes.py
from so74695297_log import Logger

log = Logger(name="logger-1")


def my_route():  # FAKE IMPL
    log.write(message=f"route")
# file: so74695297_log.py
import logging


class Logger:
    def __init__(self, name):
        self._logger = logging.getLogger(name)  # FAKE IMPL

    def write(self, message):
        self._logger.info(message)  # FAKE IMPL

运行时(main.py 文件做了一些事情):

INFO:logger-1:in main()
INFO:logger-1:route

当记录器不喜欢任何格式化程序时,这是预期的输出。

然后添加一个测试:

# file: so74695297_test.py
import unittest
import unittest.mock as mock

from so74695297_routes import my_route


class TestMyRoute(unittest.TestCase):
    def test__my_route_write_a_log(self):
        spy_logger = mock.Mock()
        with mock.patch("so74695297_log.Logger", new=spy_logger):
            my_route()
        assert spy_logger.assert_called()


if __name__ == "__main__":
    unittest.main()  # for demo
Ran 1 test in 0.010s

FAILED (failures=1)

Failure
Traceback (most recent call last):
  File "/home/stack_overflow/so74695297_test.py", line 12, in test__my_route_write_a_log
    assert spy_logger.assert_called()
  File "/usr/lib/python3.8/unittest/mock.py", line 882, in assert_called
    raise AssertionError(msg)
AssertionError: Expected 'mock' to have been called.

现在我们有事情要做了!

正如@MrBeanBremen 指出的那样,您的记录器是在导入时配置的(即使不是“主”模块)这一事实使事情变得复杂。

问题是,当 mock.patch 行运行时,模块已经导入并创建了它们的 Logger。我们可以做的是模拟 Logger.write 方法:

    def test__my_route_writes_a_log(self):
        with mock.patch("so74695297_log.Logger.write") as spy__Logger_write:
            my_route()
        spy__Logger_write.assert_called_once_with(message="route")
Ran 1 test in 0.001s

OK

如果您更喜欢使用装饰器形式:

    @mock.patch("so74695297_log.Logger.write")
    def test__my_route_writes_a_log(self, spy__Logger_write):
        my_route()
        spy__Logger_write.assert_called_once_with(message="route")

因为我们模拟了类的方法,所以每个 Logger 实例都有一个 write 的模拟版本:

#                           vvvv
    @mock.patch("so74695297_main.Logger.write")
    def test__main_writes_a_log(self, spy__Logger_write):
        main()
        # assert False, spy__Logger_write.mock_calls
        spy__Logger_write.assert_any_call(message="in main()")

最后,main.Logger.writeroutes.Logger.writelog.Logger.write 本质上是相同的,只是对相同“方法”对象的引用。从一种方式嘲笑,也为所有其他方式嘲笑。

【讨论】:

    猜你喜欢
    • 2011-06-14
    • 2016-03-16
    • 2011-12-27
    • 2017-02-06
    • 2022-01-07
    • 2017-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多