在 pytest 测试中使用 runpy 时如何防止缓存模块/变量?

问题描述 投票:0回答:2

鉴于这些文件:

# bar.py
barvar = []

def barfun():
    barvar.append(1)


# foo.py
import bar

foovar = []
def foofun():
    foovar.append(1)

if __name__ == '__main__':

    foofun()
    bar.barfun()
    foovar.append(2)
    bar.barvar.append(2)

    print(f'{foovar    =}')
    print(f'{bar.barvar=}')
    
# test_foo.py
import sys
import os
import pytest
import runpy

sys.path.insert(0,os.getcwd()) # so that "import bar" in foo.py works

@pytest.mark.parametrize('execution_number', range(5))
def test1(execution_number):
    print(f'\n{execution_number=}\n')
    sys.argv=[os.path.join(os.getcwd(),'foo.py')]
    runpy.run_path('foo.py',run_name="__main__")

如果我现在运行

pytest test_foo.py -s
我会得到:

========================================================================
platform win32 -- Python 3.10.8, pytest-7.2.0, pluggy-1.0.0
rootdir: C:\Temp
plugins: anyio-3.6.2
collected 5 items

test_foo.py
execution_number=0

foovar    =[1, 2]
bar.barvar=[1, 2]
.
execution_number=1

foovar    =[1, 2]
bar.barvar=[1, 2, 1, 2]
.
execution_number=2

foovar    =[1, 2]
bar.barvar=[1, 2, 1, 2, 1, 2]
.
execution_number=3

foovar    =[1, 2]
bar.barvar=[1, 2, 1, 2, 1, 2, 1, 2]
.
execution_number=4

foovar    =[1, 2]
bar.barvar=[1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
.

========================================================================

所以

barvar
正在记住它之前的内容。这显然不利于测试。

还在用

runpy
就可以预防吗?

可以理解,python docs 警告

runpy
副作用:

请注意,这不是沙盒模块 - 所有代码都在当前进程中执行,任何副作用(例如其他模块的缓存导入)将在函数返回后保留。

如果这很棘手或太复杂而无法可靠地完成,是否有其他选择? 我正在寻找测试接受参数并生成内容(通常是文件)的脚本的便利性。我典型的

pytest
测试脚本通过
sys.argv
设置参数,然后通过
runpy
运行目标脚本(具有大量导入的非常大的程序),然后验证生成的文件(例如,与回归测试的基线进行比较)。一次测试运行中有许多调用;因此需要一个干净的石板。

subprocess.run(['python.exe', 'script.py', *arglist])
是我能想到的另一种选择。

谢谢。

python unit-testing testing pytest runpy
2个回答
0
投票

简单实用的解决方案,在测试设置中驱逐“缓存”栏模块(如果有):

@pytest.fixture(autouse=True)
def evict_bar():
    sys.modules.pop("bar", None)

0
投票

如果您无法重构您的代码,您不想从

sys.modules
中删除模块(两种解决方案都可以)。您可以只修补
barvar
变量设置初始状态,以便为每个测试执行一个空列表。

from unittest import mock

@pytest.mark.parametrize('execution_number', range(5))
def test1(execution_number):
    with mock.patch('bar.barvar', new_callable=list):
        print(f'\n{execution_number=}\n')
        sys.argv=[os.path.join(os.getcwd(),'foo.py')]
        runpy.run_path('foo.py',run_name="__main__", init_globals={'bar.barvar': []})
© www.soinside.com 2019 - 2024. All rights reserved.