执行 python 脚本,获取其打印和日志输出并断言 unittest

Execute python script, get its print and logging output and assert unittest

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

所以我最近偶然发现了单元测试,我有一个程序,我通常这样启动 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()

我会像这样重组 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 的赋值,实际上会改变它,这将影响同一文件中的后续测试。

我最终用特定名称实例化了主程序的记录器,因此我可以再次在 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)

这个方法好像很复杂,我是看了很多帖子和文档才弄到这个的。也许有人知道实现这一目标的更好方法。