【问题标题】:Execute python script, get its print and logging output and assert unittest执行 python 脚本,获取其打印和日志输出并断言 unittest
【发布时间】:2021-05-21 23:53:19
【问题描述】:

编辑:感谢@eemz 提出重新设计结构并使用from unittest.mock import patch 的想法,但问题仍然存在。

所以我最近偶然发现了 unittest,我有一个程序,我通常像这样 python run.py -config /path/to/config.file -y 开始。我想在单独的test.py 文件中编写一个简单的测试:执行脚本,传递提到的参数并获取其所有输出。我传递了一个缺少某些东西的准备好的配置文件,因此run.py 将中断并使用logging.error 准确记录此错误:“xyz 在配置文件中丢失!” (见下面的例子)。我会从print() 那里得到几句话,然后logging 实例会启动并从那里开始处理。我如何获得它的输出以便我可以检查它?请随意重写,因为我还在学习中,请多多包涵。

简化示例:

run.py

import logging

def run(args):
  < args.config = /path/to/config.file >
  cnfg = Config(args.config)

  cnfg.logger.info("Let's start with the rest of the code!") # This is NOT in 'output' of the unittest
  < code >

if __name__ == "__main__":
  print("Welcome! Starting execution.") # This is in 'output' of the unittest
  < code to parse arguments 'args' >
  run(args)

Config.py

import logging

class Config:
  def __init__(self):
    print("Creating logging instance, hold on ...") # This is in 'output' of the unittest
    logger = logging.getLogger(__name__)
    console_handler = logging.StreamHandler()
    logger.addHandler(console_handler)
    logger.info("Logging activated, let's go!") # This is NOT in 'output' of the unittest
    self.logger = logger

    if xyz not in config:
      self.logger.error("xyz was missing in Config file!") # This is NOT in 'output' of the unittest
      exit(1)

test.py

import unittest
from unittest.mock import patch

class TestConfigs(unittest.TestCase):
    def test_xyz(self):
        with patch('sys.stdout', new=StringIO()) as capture:
            with self.assertRaises(SystemExit) as cm:
                run("/p/to/f/missing/xyz/f", "", False, True)
        output = capture.getvalue().strip()

        self.assertEqual(cm.exception.code, 1)
        # Following is working, because the print messages are in output
        self.assertTrue("Welcome! Starting execution." in output)
        # Following is NOT working, because the logging messages are not in output
        self.assertTrue("xyz was missing in Config file!" in output)

if __name__ == "__main__":
    unittest.main()

【问题讨论】:

  • 不直接相关,但 pytest 可能值得研究,因为它是 unittest 的超集,并且在错误时提供更好的堆栈跟踪

标签: python unit-testing testing logging python-unittest


【解决方案1】:

我最终使用特定名称实例化了主程序的记录器,因此我可以再次在test.py 中获取记录器并断言记录器是使用特定文本调用的。我不知道我可以通过使用同名的 logging.getLogger("name") 来获取记录器。简化示例:

test.py

import unittest
from run import run
from unittest.mock import patch

main_logger = logging.getLogger("main_tool")

class TestConfigs(unittest.TestCase):
    def test_xyz(self):
        with patch('sys.stdout', new=StringIO()) as capture, \
             self.assertRaises(SystemExit) as cm, \
             patch.object(main_logger , "info") as mock_log1, \
             patch.object(main_logger , "error") as mock_log2:
                run("/path/to/file/missing/xyz.file")
        output = capture.getvalue().strip()

        self.assertTrue("Creating logging instance, hold on ..." in output)
        mock_log1.assert_called_once_with("Logging activated, let's go!")
        mock_log2.assert_called_once_with("xyz was missing in Config file!")
        self.assertEqual(cm.exception.code, 1)

if __name__ == "__main__":
    unittest.main()

run.py

def run(path: str):
  cnfg = Config(path)
  < code >

if __name__ == "__main__":
  < code to parse arguments 'args' >
  path = args.file_path
  run(path)

Config.py

import logging

class Config:
  def __init__(self, path: str):
    print("Creating logging instance, hold on ...")
    logger = logging.getLogger("main_tool")
    console_handler = logging.StreamHandler()
    logger.addHandler(console_handler)
    logger.info("Logging activated, let's go!")
    self.logger = logger

    # Load file, simplified
    config = load(path)

    if xyz not in config:
      self.logger.error("xyz was missing in Config file!")
      exit(1)

这种方法似乎很复杂,我通过阅读许多其他帖子和文档来达到这一点。也许有人知道实现这一目标的更好方法。

【讨论】:

    【解决方案2】:

    我会像这样重组 run.py:

    import logging
    
    def main():
      print("Welcome! Starting execution.")
      Etc etc
    
    if __name__ == "__main__":
      main()
    

    然后你可以在你的单元测试中调用函数 run.main() 而不是创建一个子进程。

    from io import StringIO
    from unittest.mock import patch
    import sys
    
    import run
    
    class etc etc
      def test_run etc etc:
        with patch('sys.stdout', new=StringIO()) as capture:
          sys.argv = [‘run.py’, ‘-flag’, ‘-flag’, ‘-flag’]
          run.main()
          output = capture.getvalue().strip()
        assert output == <whatever you expect it to be>
    

    如果您不熟悉单元测试,那么您之前可能没有见过模拟。实际上,我正在用假的标准输出替换标准输出,以捕获发送到那里的所有内容,以便稍后将其拉出到变量输出中。

    事实上,围绕 sys.argv 的第二个补丁会更好,因为我在这里所做的,分配给真正的 argv,实际上会改变它,这将影响同一文件中的后续测试。

    【讨论】:

    • 这确实很好,到目前为止效果很好。但是,我仍然无法获得日志实例的输出。 output 将包含 run.py 通过 print() 写入的内容,但不包含使用 logger.x() 写入的内容。我会用更多信息更新我的问题,也许某处会出现问题。
    猜你喜欢
    • 2021-03-14
    • 2021-07-08
    • 1970-01-01
    • 1970-01-01
    • 2022-11-24
    • 1970-01-01
    • 1970-01-01
    • 2021-07-13
    • 1970-01-01
    相关资源
    最近更新 更多