【问题标题】:Python - Using pytest to skip test unless specifiedPython - 除非指定,否则使用 pytest 跳过测试
【发布时间】:2019-02-14 04:49:53
【问题描述】:

背景

我正在使用 pytest 来测试一个将数据推送到数据库的网络爬虫。该类仅提取 html 并将 html 推送到数据库以供稍后解析。我的大多数测试都使用虚拟数据来表示 html。

问题

我想做一个测试,从网站上抓取一个网页,但除非指定,否则我希望自动关闭测试。如果您不想总是运行昂贵或耗时的测试,可能会出现类似的情况。

预期的解决方案

我期待某种标记会抑制测试,除非我让 pytest 运行所有抑制的测试,但我在文档中没有看到。

我做了什么

  • 我目前正在使用跳过标记并将其注释掉。
  • 尝试使用skipif 标记,并使用命令提示符pytest test_file.py 1 中的此命令和测试文件中的以下代码为python 脚本提供参数。问题是当我尝试为我的 test_file 提供一个参数时,pytest 期望它是另一个文件名,所以我收到一个错误“0.00 秒内没有运行测试,错误:找不到文件:1”

    if len(sys.argv) == 1:
      RUN_ALL_TESTS = False
    else:
      RUN_ALL_TESTS = True
    ...
    # other tests
    ...
    
    @pytest.mark.skipif(RUN_ALL_TESTS)
    def test_scrape_website():
      ...
    
  • 我或许可以将测试视为一个夹具并使用 @pytest.fixture(autouse=False),但不确定如何覆盖 autouse 变量

  • How to skip a pytest using an external fixture? 中提出了类似的解决方案,但这个解决方案似乎比我需要的更复杂。

【问题讨论】:

    标签: python python-3.x testing pytest


    【解决方案1】:

    文档准确地描述了您的问题:https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tests-according-to-command-line-option。从那里复制:

    这是一个 conftest.py 文件,添加了 --runslow 命令行选项 控制 pytest.mark.slow 标记测试的跳过:

    # content of conftest.py
    
    import pytest
    
    
    def pytest_addoption(parser):
        parser.addoption(
            "--runslow", action="store_true", default=False, help="run slow tests"
        )
    
    
    def pytest_collection_modifyitems(config, items):
        if config.getoption("--runslow"):
            # --runslow given in cli: do not skip slow tests
            return
        skip_slow = pytest.mark.skip(reason="need --runslow option to run")
        for item in items:
            if "slow" in item.keywords:
                item.add_marker(skip_slow)
    

    我们现在可以像这样编写一个测试模块:

    # content of test_module.py
    import pytest
    
    
    def test_func_fast():
        pass
    
    
    @pytest.mark.slow
    def test_func_slow():
        pass
    

    【讨论】:

      【解决方案2】:

      有几种方法可以解决这个问题,但我将介绍我在 Python 基线中看到的两种常见方法。

      1) 通过将“可选”测试放在另一个目录中来分隔您的测试。

      不确定你的项目布局是什么样的,但你可以这样做(只有测试目录很重要,其余的只是一个玩具示例布局):

      README.md
      setup.py
      requirements.txt
      test/
          unit/
              test_something.py
              test_something_else.py
          integration/
              test_optional.py
      application/
          __init__.py
          some_module.py
      

      然后,当你调用 pytest 时,如果你想运行 just 单元测试(即只运行 test_something*.py 文件),你可以通过 pytest test/unit 调用它,或者如果你想运行 pytest test/integration运行just 集成测试(即仅test_optional.py),或者pytest test,如果你想运行所有 测试。因此,默认情况下,您可以运行 pytest test/unit

      我建议将这些调用包装在某种脚本中。我更喜欢make,因为它对于这种类型的包装非常强大。然后你可以说make test,它只会运行你的默认(快速)测试套件,或者make test_all,它会运行所有的测试(可能会也可能不会很慢)。

      您可以使用的 Makefile 示例:

      .PHONY: all clean install test test_int test_all uninstall
      
      all: install
      
      clean:
          rm -rf build
          rm -rf dist
          rm -rf *.egg-info
      
      install:
          python setup.py install
      
      test: install
          pytest -v -s test/unit
      
      test_int: install
          pytest -v -s test/integration
      
      test_all: install
          pytest -v -s test
      
      uninstall:
          pip uninstall app_name
      

      2) 使用 @pytest.mark.skipif 装饰器明智地标记您的测试,但使用环境变量作为触发器

      我不太喜欢这个解决方案,我觉得它有点随意(很难判断在pytest 运行时正在运行哪组测试)。但是,您可以做的是定义一个环境变量,然后将该环境变量绑定到模块中,以检测您是否要运行所有测试。环境变量依赖于 shell,但我假设你有一个 bash 环境,因为那是一个流行的 shell。

      您可以为快速单元测试使用export TEST_LEVEL="unit"(所以这将是您的默认设置),或者为所有测试使用export TEST_LEVEL="all"。然后在你的测试文件中,你可以像这样做你最初想做的事情:

      import os
      
      ...
      
      @pytest.mark.skipif(os.environ["TEST_LEVEL"] == "unit")
      def test_scrape_website():
        ...
      

      注意:将测试级别命名为“单元”和“集成”是无关紧要的。您可以随意命名它们。您还可以有许多级别(例如夜间测试或性能测试)。

      另外,我认为选项 1 是最好的方法,因为它不仅明确允许测试分离,而且还可以为测试的含义和代表增加语义和清晰度。但是软件中没有“一刀切”的做法,您必须根据自己的具体情况来决定自己喜欢哪种方法。

      HTH!

      【讨论】:

      • 非常感谢您的深入回答。选项 1 似乎很明确。看起来你有测试的经验或知识。是否有您推荐阅读的文档或书籍?这是我第一次构建测试脚本。谢谢!
      • @shanearmstrong 我希望我能推荐一本书或文档,但不幸的是我不能。我通过试验、错误和经验学习了如何测试 Python 代码。使用pytest,您有了一个很好的开始,您正在思考正确的事情并提出正确的问题。您始终可以做的一件事是查看开源 Python 项目(如 numpydjango),看看它们是如何进行测试的。组织/运行测试有很多很多其他方法,我只是列出了我作为专业 Python 开发人员个人使用过的两个最流行的变体。
      【解决方案3】:

      一个非常简单的解决方案是使用-k 参数。您可以使用-k 参数取消选择某些测试。 -k 尝试将其参数匹配到测试名称或标记的任何部分您可以使用not 反转匹配(您也可以使用布尔运算符andor)。因此-k 'not slow' 会跳过名称中包含“slow”、名称中带有“slow”标记或类/模块名称包含“slow”的测试。

      例如,给定这个文件:

      import pytest
      
      def test_true():
          assert True
      
      @pytest.mark.slow
      def test_long():
          assert False
      
      def test_slow():
          assert False
      

      当你运行时:

      pytest -k 'not slow'
      

      它输出如下内容:(请注意,两个失败的测试都被跳过,因为它们与过滤器匹配)

      ============================= test session starts =============================
      platform win32 -- Python 3.5.1, pytest-3.4.0, py-1.5.2, pluggy-0.6.0
      rootdir: c:\Users\User\Documents\python, inifile:
      collected 3 items
      
      test_thing.py .                                                          [100%]
      
      ============================= 2 tests deselected ==============================
      =================== 1 passed, 2 deselected in 0.02 seconds ====================
      

      由于急切的匹配,您可能想做一些事情,例如将所有单元测试放在一个名为 unittest 的目录中,然后将慢速的测试标记为 slow_unittest(以便意外匹配恰好发生的测试名字很慢)。然后你可以使用-k 'unittest and not slow_unittest' 来匹配你所有的快速单元测试。

      More pytest example marker usage

      【讨论】:

        【解决方案4】:

        形成一个小类,以便在多个标记/cli 选项上重用 @xverges 代码;

        @dataclass
        class TestsWithMarkSkipper:
            ''' Util to skip tests with mark, unless cli option provided. '''
        
            test_mark: str
            cli_option_name: str
            cli_option_help: str
        
            def pytest_addoption_hook(self, parser):
                parser.addoption(
                    self.cli_option_name,
                    action="store_true",
                    default=False,
                    help=self.cli_option_help,
                )
        
            def pytest_collection_modifyitems_hook(self, config, items):
                if not config.getoption(self.cli_option_name):
                    self._skip_items_with_mark(items)
        
            def _skip_items_with_mark(self, items):
                reason = "need {} option to run".format(self.cli_option_name)
                skip_marker = pytest.mark.skip(reason=reason)
                for item in items:
                    if self.test_mark in item.keywords:
                        item.add_marker(skip_marker)
        

        用法示例(必须输入conftest.py):

        slow_skipper = TestsWithMarkSkipper(
            test_mark='slow',
            cli_option_name="--runslow",
            cli_option_help="run slow tests",
        )
        pytest_addoption = slow_skipper.pytest_addoption_hook
        pytest_collection_modifyitems = slow_skipper.pytest_collection_modifyitems_hook
        

        【讨论】:

          猜你喜欢
          • 2018-05-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-01-22
          • 2013-03-28
          • 1970-01-01
          • 2021-07-02
          • 1970-01-01
          相关资源
          最近更新 更多