【问题标题】:Pytest: Parameterize unit test using a fixture that uses another fixture as inputPytest:使用使用另一个夹具作为输入的夹具参数化单元测试
【发布时间】:2020-11-19 08:30:30
【问题描述】:

我是参数化和固定装置的新手,还在学习。我发现了一些使用间接参数化的帖子,但我很难根据我的代码中的内容来实现。如果有任何关于我如何实现这一目标的想法,我将不胜感激。

我的 conftest.py 中有几个固定装置,它们为我的测试文件中的函数“get_fus_output()”提供输入文件。该函数处理输入并生成两个数据帧以在我的测试中进行比较。此外,我将基于一个共同值('Fus_id')转租这两个 DF 来单独测试它们。所以这个函数的输出将是[(Truth_df1, test_df1),(Truth_df2, test_df2)...] 只是为了参数化每个测试和真值df的测试。不幸的是,我无法在我的测试函数“test_annotation_match”中使用它,因为这个函数需要一个夹具。

我无法将夹具作为输入提供给另一个夹具以进行参数化。是的,pytest 不支持它,但无法找到间接参数化的解决方法。

#fixtures from conftest.py

@pytest.fixture(scope="session")
def test_input_df(fixture_path):
    fus_bkpt_file = os.path.join(fixture_path, 'test_bkpt.tsv')
    test_input_df= pd.read_csv(fus_bkpt_file, sep='\t')
    return test_input_df


@pytest.fixture
def test_truth_df(fixture_path):
    test_fus_out_file = os.path.join(fixture_path, 'test_expected_output.tsv')
    test_truth_df = pd.read_csv(test_fus_out_file, sep='\t')
    return test_truth_df

@pytest.fixture
def res_path():
    return utils.get_res_path()
#test script

@pytest.fixture
def get_fus_output(test_input_df, test_truth_df, res_path):
    param_list = []
    # get output from script
    script_out = ex_annot.run(test_input_df, res_path)

    for index, row in test_input_df.iterrows():
        fus_id = row['Fus_id']
         param_list.append((get_frame(test_truth_df, fus_id), get_frame(script_out, fus_id)))
    
    # param_list eg : [(Truth_df1, test_df1),(Truth_df2, test_df2)...]
    print(param_list)
    return param_list


@pytest.mark.parametrize("get_fus_output", [test_input_df, test_truth_df, res_path], indirect=True)
def test_annotation_match(get_fus_output):
    test, expected = get_fusion_output
    assert_frame_equal(test, expected, check_dtype=False, check_like=True)

#OUTPUT
================================================================================ ERRORS ================================================================================
_______________________________________________________ ERROR collecting test_annotations.py
 _______________________________________________________
test_annotations.py:51: in <module>
    @pytest.mark.parametrize("get_fus_output", [test_input_df, test_truth_df, res_path], indirect=True)
E   NameError: name 'test_input_df' is not defined
======================================================================= short test summary info ========================================================================
ERROR test_annotations.py - NameError: name 'test_input_df' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
=========================================================================== 1 error in 1.46s ===========================================================================

【问题讨论】:

标签: python python-3.x pandas unit-testing pytest


【解决方案1】:

我不是 100% 确定我理解您在这里要做什么,但我认为您对参数化和夹具作用的理解是不正确的。似乎您正在尝试使用固定装置为您的测试创建参数列表,这并不是真正的正确方法(而且您正在这样做的方式肯定行不通,正如您所看到的) .

为了充分解释如何解决这个问题,首先,让我介绍一下如何使用参数化和固定装置的背景知识。

参数化

我不认为这里有什么新东西,只是为了确保我们在同一个页面上:

通常,在 Pytest 中,一个 test_* 函数就是一个测试用例:

def test_square():
    assert square(3) == 9

如果你想用不同的数据做同样的测试,你可以编写单独的测试:

def test_square_pos():
    assert square(3) == 9

def test_square_frac():
    assert square(0.5) == 0.25

def test_square_zero():
    assert square(0) == 0

def test_square_neg():
    assert square(-3) == 9

这不是很好,因为它违反了DRY 原则。参数化是解决这个问题的方法。您可以通过提供测试参数列表将一个测试用例变成多个:

@pytest.mark.parametrize('test_input,expected',
                         [(3, 9), (0.5, 0.25), (0, 0), (-3, 9)])
def test_square(test_input, expected):
    assert square(test_input) == expected

夹具

fixtures 也是关于DRY 代码,但方式不同。

假设您正在编写一个网络应用程序。您可能有几个测试需要连接到数据库。您可以将相同的代码添加到每个测试以打开和设置测试数据库,但这肯定是在重复您自己。例如,如果您切换数据库,则需要更新大量测试代码。

Fixtures 是允许您进行一些可用于多个测试的设置(以及可能的拆卸)的功能:

@pytest.fixture
def db_connection():
    # Open a temporary database in memory
    db = sqlite3.connect(':memory:')
    # Create a table of test orders to use
    db.execute('CREATE TABLE orders (id, customer, item)')
    db.executemany('INSERT INTO orders (id, customer, item) VALUES (?, ?, ?)',
                   [(1, 'Max', 'Pens'),
                    (2, 'Rachel', 'Binders'),
                    (3, 'Max', 'White out'),
                    (4, 'Alice', 'Highlighters')])
    return db      

def test_get_orders_by_name(db_connection):
    orders = get_orders_by_name(db_connection, 'Max')
    assert orders = [(1, 'Max', 'Pens'),
                     (3, 'Max', 'White out')]

def test_get_orders_by_name_nonexistent(db_connection):
    orders = get_orders_by_name(db_connection, 'John')
    assert orders = []

修复代码

好的,让我们深入了解您的代码。

第一个问题是你的 @pytest.mark.parametrize 装饰器:

@pytest.mark.parametrize("get_fus_output", [test_input_df, test_truth_df, res_path], indirect=True)

这不是使用indirect 的正确情况。就像测试可以参数化一样,fixtures can be parameterized 也是。从文档中不是很清楚(在我看来),但indirect 只是参数化夹具的另一种方法。这与using a fixture in another fixture完全不同,这正是你想要的。

事实上,对于get_fus_output 使用test_input_dftest_truth_dfres_path 固定装置,您根本不需要@pytest.mark.parametrize 行。一般来说,测试函数或夹具的任何参数如果未被使用(例如由@pytest.mark.parametrize 装饰器),则自动假定为夹具

因此,您现有的 @pytest.mark.parametrize 没有达到您的预期。那么你如何参数化你的测试呢?这涉及到更大的问题:您正在尝试使用get_fus_output 夹具来为test_annotation_match 创建参数。 这不是你可以用固定装置做的事情。

当 Pytest 运行时,首先它收集所有的测试用例,然后它一个一个地运行它们。测试参数必须在收集阶段准备好,但夹具直到测试阶段才会运行。夹具内的代码无法帮助参数化。您仍然可以通过编程方式生成参数,但固定装置不是这样做的方法。

你需要做一些事情:

首先,将get_fus_output 从固定装置转换为常规函数。这意味着删除 @pytest.fixture 装饰器,但您还必须更新它以不使用 test_input_df test_truth_dfres_path 固定装置。 (如果没有其他需要它们作为固定装置,您可以将它们全部转换为常规函数,在这种情况下,您可能希望将它们放在 conftest.py 之外的自己的模块中,或者只是将它们移动到同一个测试脚本中。)

那么,@pytest.mark.parametrize 需要使用该函数来获取参数列表:

@pytest.mark.parametrize("expected,test", get_fus_output())
def test_annotation_match(expected, test):
    assert_frame_equal(test, expected, check_dtype=False, check_like=True)

【讨论】:

  • 非常感谢您花时间非常清楚地解释这一点!
猜你喜欢
  • 1970-01-01
  • 2018-02-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-03
  • 2021-02-05
  • 1970-01-01
相关资源
最近更新 更多