Pytest:使用使用另一个夹具作为输入的夹具参数化单元测试 [英] Pytest: Parameterize unit test using a fixture that uses another fixture as input

查看:72
本文介绍了Pytest:使用使用另一个夹具作为输入的夹具参数化单元测试的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我是参数化和装置的新手,仍在学习.我发现了一些使用间接参数化的帖子,但根据我的代码中的内容,我很难实现.感谢您对我如何实现这一目标的任何想法.

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

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

#fixtures 来自 conftest.py@pytest.fixture(范围=会话")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')返回 test_input_df@pytest.fixturedef 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')返回 test_truth_df@pytest.fixturedef res_path():返回 utils.get_res_path()

#test 脚本@pytest.fixturedef get_fus_output(test_input_df, test_truth_df, res_path):参数列表 = []# 获取脚本输出script_out = ex_annot.run(test_input_df, res_path)对于索引,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 例如:[(Truth_df1, test_df1),(Truth_df2, test_df2)...]打印(参数列表)返回参数列表@pytest.mark.parametrize(get_fus_output",[test_input_df,test_truth_df,res_path],indirect=True)def test_annotation_match(get_fus_output):测试,预期 = get_fusion_outputassert_frame_equal(测试,预期,check_dtype=False,check_like=True)

#OUTPUT================================================================================ 错误 ==================================================================================_______________________________________________________ 收集 test_annotations.py 时出错_______________________________________________________test_annotations.py:51: 在 <module>@pytest.mark.parametrize(get_fus_output",[test_input_df,test_truth_df,res_path],indirect=True)E NameError: name 'test_input_df' 未定义====================================================================== 简短的测试摘要信息 ========================================================================错误 test_annotations.py - NameError:未定义名称test_input_df"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!中断:收集过程中出现 1 个错误 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!============================================================================ 1.46 秒内出现 1 个错误 ============================================================================

解决方案

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

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

参数化

我不认为这里的任何东西都应该是新的,只是为了确保我们在同一页面上:

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

def test_square():断言正方形(3)== 9

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

def test_square_pos():断言正方形(3)== 9def test_square_frac():断言平方(0.5)== 0.25def test_square_zero():断言正方形(0)== 0def test_square_neg():断言平方(-3)== 9

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

@pytest.mark.parametrize('test_input,expected',[(3, 9), (0.5, 0.25), (0, 0), (-3, 9)])def test_square(test_input, 预期):断言平方(test_input)==预期

固定装置

Fixtures 也是关于 DRY 代码,但方式不同.>

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

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

@pytest.fixturedef db_connection():# 在内存中打开一个临时数据库db = sqlite3.connect(':memory:')# 创建要使用的测试订单表db.execute('CREATE TABLE orders (id, customer, item)')db.executemany('INSERT INTO orders (id, customer, item) VALUES (?, ?, ?)',[(1, '最大', '钢笔'),(2, '瑞秋', '活页夹'),(3, 'Max', 'White out'),(4, '爱丽丝', '荧光笔')])返回数据库def test_get_orders_by_name(db_connection):订单 = get_orders_by_name(db_connection, 'Max')断言订单 = [(1, 'Max', 'Pens'),(3, 'Max', 'White out')]def test_get_orders_by_name_nonexistent(db_connection):订单 = get_orders_by_name(db_connection, 'John')断言订单 = []

修复您的代码

好的,让我们先了解一下您的代码.

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

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

这不是使用 indirect 的正确情况.就像测试可以参数化一样,夹具可以参数化,也.从文档中不是很清楚(在我看来),但是 indirect 只是一种参数化装置的替代方法.这与完全不同"nofollow noreferrer">在另一个装置中使用一个装置,这正是您想要的.

事实上,对于 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(预期,测试):assert_frame_equal(测试,预期,check_dtype=False,check_like=True)

I am new to parameterize and fixtures and still learning. I found a few post that uses indirect paramerization but it is difficult for me to implement based on what I have in my code. Would appreciate any ideas on how I could achieve this.

I have a couple of fixtures in my conftest.py that supply input files to a function "get_fus_output()" in my test file. That function process the input and generate two data-frames to compare in my testing. Further, I am subletting those two DF based on a common value ('Fus_id') to testthem individually. So the output of this function would be[(Truth_df1, test_df1),(Truth_df2, test_df2)...] just to parameterize the testing of each of these test and truth df. Unfortunately I am not able to use this in my test function "test_annotation_match" since this function needs a fixture.

I am not able to feed the fixture as input to another fixture to parameterize. Yes it is not supported in pytest but not able to figure out a workaround with indirect parameterization.

#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 ===========================================================================

解决方案

I'm not 100% sure I understand what you are trying to do here, but I think your understanding of parameterization and the role of fixtures is incorrect. It seems like you are trying to use the fixtures to create the parameter lists for your tests, which isn't really the right way to go about it (and the way you are doing it certainly won't work, as you are seeing).

To fully explain how to fix this, first, let me give a little background about how parameterization and fixtures are meant to be used.

Parameterization

I don't think anything here should be new, but just to make sure we are on the same page:

Normally, in Pytest, one test_* function is one test case:

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

If you want to do the same test but with different data, you can write separate tests:

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

This isn't great, because it violates the DRY principle. Parameterization is the solution to this. You turn one test case into several by providing a list of test parameters:

@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

Fixtures are also about DRY code, but in a different way.

Suppose you are writing a web app. You might have several tests that need a connection to the database. You can add the same code to each test to open and set up a test database, but that's definitely repeating yourself. If you, say, switch databases, that's a lot of test code to update.

Fixtures are functions that allow you to do some setup (and potentially teardown) that can be used for multiple tests:

@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 = []

Fixing Your Code

Ok, so with that background out of the way, let's dig into your code.

The first problem is with your @pytest.mark.parametrize decorator:

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

This isn't the right situation to use indirect. Just like tests can be parameterized, fixtures can be parameterized, too. It's not very clear from the docs (in my opinion), but indirect is just an alternative way to parameterize fixtures. That's totally different from using a fixture in another fixture, which is what you want.

In fact, for get_fus_output to use the test_input_df, test_truth_df, and res_path fixtures, you don't need the @pytest.mark.parametrize line at all. In general, any argument to a test function or fixture is automatically assumed to be a fixture if it's not otherwise used (e.g. by the @pytest.mark.parametrize decorator).

So, your existing @pytest.mark.parametrize isn't doing what you expect. How do you parameterize your test then? This is getting into the bigger problem: you are trying to use the get_fus_output fixture to create the parameters for test_annotation_match. That isn't the sort of thing you can do with a fixture.

When Pytest runs, first it collects all the test cases, then it runs them one by one. Test parameters have to be ready during the collection stage, but fixtures don't run until the testing stage. There is no way for code inside a fixture to help with parameterization. You can still generate your parameters programmatically, but fixtures aren't the way to do it.

You'll need to do a few things:

First, convert get_fus_output from a fixture to a regular function. That means removing the @pytest.fixture decorator, but you've also got to update it not to use the test_input_df test_truth_df, and res_path fixtures. (If nothing else needs them as fixtures, you can convert them all to regular functions, in which case, you probably want to put them in their own module outside of conftest.py or just move them into the same test script.)

Then, @pytest.mark.parametrize needs to use that function to get a list of parameters:

@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)

这篇关于Pytest:使用使用另一个夹具作为输入的夹具参数化单元测试的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆