在pytest的skip-if条件中使用命令行选项

18

长话短说,如果会话是针对我们的生产API运行的,我希望能够跳过一些测试。设置测试运行环境的命令行选项已经被设定。

我发现使用pytest_namespace来跟踪全局变量的想法,因此我在我的conftest.py文件中进行了设置。

def pytest_namespace():
    return {'global_env': ''}
我获取命令行选项并在conftest.py的fixture中设置各种API网址(来自config.ini文件)。

我接收命令行选项,并将不同的 API URL(从 config.ini 文件中)设置为 fixture 在 conftest.py 中。

@pytest.fixture(scope='session', autouse=True)
def configInfo(pytestconfig):
    global data
    environment = pytestconfig.getoption('--ENV')
    print(environment)
    environment = str.lower(environment)

    pytest.global_env = environment

    config = configparser.ConfigParser()
    config.read('config.ini') # local config file
    configData = config['QA-CONFIG']
    if environment == 'qa':
            configData = config['QA-CONFIG']
    if environment == 'prod':
            configData = config['PROD-CONFIG']

(...)

然后我有一项想跳过的测试,它被装饰成这样:

@pytest.mark.skipif(pytest.global_env in 'prod',
                reason="feature not in Prod yet")

然而,每当我对生产环境运行测试时,它们就不会被跳过。我进行了一些调试,并发现:

a)global_env变量可以通过另一个fixture访问。

@pytest.fixture(scope="session", autouse=True)
def mod_header(request):
    log.info('\n-----\n| '+pytest.global_env+' |\n-----\n')

我的日志正确显示。

b) global_env变量可以在测试中访问,正确记录环境。

c) pytest_namespace不建议使用。

因此,我假设这与skipif访问global_env的时间以及fixture在测试会话中执行的时间有关。我也认为使用已弃用的功能非常不理想。

我的问题是:

  • 我如何从pytest命令行选项中获取值并将其传递给skipif?
  • 还有比pytest_namespace更好的方式吗?
2个回答

18

看起来根据命令行选项控制跳过测试的真正方法是动态标记测试为skip

  1. 使用pytest_addoption钩子添加选项,如下所示:
def pytest_addoption(parser):
    parser.addoption(
        "--runslow", action="store_true", default=False, help="run slow tests"
    )

使用pytest_collection_modifyitems钩子添加如下标记:
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)
  1. 为你的测试添加标记:
@pytest.mark.slow
def test_func_slow():
    pass

如果你想在测试中使用CLI的数据,比如它的凭据,只需要在从pytestconfig检索时指定一个跳过选项即可:

  1. 像这样使用pytest_addoption钩子添加选项
def pytest_addoption(parser):
    parser.addoption(
        "--credentials",
        action="store",
        default=None,
        help="credentials to ..."
    )
  1. 在从pytestconfig获取时使用skip选项
@pytest.fixture(scope="session")
def super_secret_fixture(pytestconfig):
    credentials = pytestconfig.getoption('--credentials', skip=True)
    ...
  1. 在你的测试中像往常一样使用fixture:
def test_with_fixture(super_secret_fixture):
    ...

在这种情况下,如果您没有向CLI发送--credentials选项,则会得到类似于此的结果:
Skipped: no 'credentials' option found

使用_pytest.config.get_config而不是已弃用的pytest.config更好。如果您仍然想像这样使用pytest.mark.skipif

@pytest.mark.skipif(not _pytest.config.get_config().getoption('--credentials'), reason="--credentials was not specified")

2
对我来说,奇怪的是pytest官方的解决方法是让你自己实现一个--runslow标志。我觉得在测试过程中,你总是会有一些长时间运行的、类似于烟雾测试的测试,你可能希望默认情况下跳过这些测试,如果--runslow内置在其中就好了。 - andrew
5
让我感到困惑的是,根据pytest的历史注释,用户以前可以在一行代码中完成这项操作,正如答案末尾提到的那样,使用pytest.mark.skipif(pytest.config.getoption(...)),而现在他们必须使用modifyitems钩子来完成。当他们弃用了pytest.config时,应该为这个常见的使用情况提供一个类似简洁且官方支持(即不基于_pytest)的替代方法,让用户不必再绕这么多弯路。 - smheidrich

4

将全局代码放在fixture中的问题在于,标记在fixture之前被评估,因此当评估skipif时,configInfo尚未运行,pytest.global_env将为空。我建议将配置代码从fixture移动到pytest_configure钩子中:

# conftest.py
import configparser
import pytest


def pytest_addoption(parser):
    parser.addoption('--ENV')


def pytest_configure(config):
    environment = config.getoption('--ENV')
    pytest.global_env = environment
    ...

配置钩子保证在收集测试和评估标记之前执行。
“pytest_namespace”有更好的尝试方法吗?
我知道的一些方法:
  1. Simply assign a module variable in pytest_configure (pytest.foo = 'bar', like I did in the example above).
  2. Use the config object as it is shared throughout the test session:

    def pytest_configure(config):
        config.foo = 'bar'
    
    @pytest.fixture
    def somefixture(pytestconfig):
        assert pytestconfig.foo == 'bar'
    
    def test_foo(pytestconfig):
        assert pytestconfig.foo == 'bar'
    

    Outside of the fixtures/tests, you can access the config via pytest.config, for example:

    @pytest.mark.skipif(pytest.config.foo == 'bar', reason='foo is bar')
    def test_baz():
        ...
    
  3. Use caching; this has an additional feature of persisting data between the test runs:

    def pytest_configure(config):
        config.cache.set('foo', 'bar')
    
    @pytest.fixture
    def somefixture(pytestconfig):
        assert pytestconfig.cache.get('foo', None)
    
    def test_foo(pytestconfig):
        assert pytestconfig.cache.get('foo', None)
    
    @pytest.mark.skipif(pytest.config.cache.get('foo', None) == 'bar', reason='foo is bar')
    def test_baz():
        assert True
    

在使用1或2时,请确保不会无意中用自己的数据覆盖pytest的内容。给你自己的变量加上唯一的名称是个好主意。使用缓存时,您就不会遇到这个问题。


抱歉如果这是一个基础问题,但是有没有关于pytest执行顺序的好参考资料?我想到了这种情况,但我发现很难弄清楚哪个先执行。 - klreeher
1
我认为这不是一个基础问题;实际上,我从未看到过所有框架部分执行顺序的完整描述。如果有疑问,请编写一个打印语句并分析输出的小型测试项目,参见我的其他答案,在那里我检查了测试类夹具和方法的执行顺序。 - hoefling
1
太棒了。虽然您明确指出了这个警告,但应该重申,解决方案1和2基本上是不安全的。然而,第3种解决方案在py.test调用之间缓存数据,因此不适用于此用例。简而言之,在全局上下文(例如@pytest.mark.skipif)中似乎没有安全的解决方法来解决这个弃用问题。由于此弃用的官方文档无用,这个详尽的答案可能是任何人能做到的最好的。显然,这个功能永远不应该被弃用*。 - Cecil Curry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接