簡體   English   中英

如何在導入模塊中模擬 function 的 class 但在 function 的 scope 之外?

[英]How to mock a function of a class in an imported module but outside the scope of a function?

我想弄清楚如何使用@patch.object模擬__init__log.write()用於Logger class,它已導入但未在模塊的 function 內部使用。 像這樣的教程 https://www.pythontutorial.net/python-unit-testing/python-patch/指出補丁需要在使用它的目標,而不是它來自的地方。 但是,使用的每個示例都顯示了要在另一個 function 中模擬的目標。

在下面提到的用例中,記錄器被導入並用於在 function 的 scope 之外寫入日志。有沒有辦法在main.pyrouters.py中模擬行為?

源代碼/apis/main.py

from utils.log import Logger
from routes import route

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

def main():
    log = Logger(name="logger-1")
    log.write("inside main")
    route()

if __name__ == "__main__":
    import logging
    logging.basicConfig(level=logging.INFO)  # for demo
    main()

在 src/apis/routers/routes.py

from utils.log import Logger
log = Logger(name="logger-1")
log.write, message=f"Inside route")
def route():
    log.write, message=f"Logging done.")

在 utils/log/logging.py 中

import logging
Class Logger:
     def __init__(self, name):
          # needs to be mocked

     def write(self, message):
          # needs to be mocked to return None

提出問題時,提供一個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

當記錄器不喜歡任何格式化程序時,這是預期的 output。

然后添加測試:

# 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本質上是一樣的,只是對相同“方法”object 的引用。從一種方式模擬,也為所有其他方式模擬.

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM