Pytest 用线划定 json

Pytest with line-delineated json

我是 Python 的新手,也是 pytest 的新手。无论如何,我正在尝试编写一些测试来解析以行为单位的推文 json。这是一个简化的例子 test_cases.jsonl:

{"contributors":null,"coordinates":null,"created_at":"Sat Aug 20 01:00:12 +0000 2016","entities":{"hashtags":[{"indices":[97,116],"text":"StandWithLouisiana"}]}}
{"contributors":null,"coordinates":null,"created_at":"Sat Aug 20 01:01:35 +0000 2016","entities":{"hashtags":[]}}

我想做的是测试如下函数:

def hashtags(t):
    return ' '.join([h['text'] for h in t['entities']['hashtags']])

我可以按如下方式测试 JSON 的单行:

@pytest.fixture
def tweet(file='test_cases.jsonl'):
    with open(file, encoding='utf-8') as lines:
        for line in lines:
            return json.loads(line)


def test_hashtag(tweet):
    assert hashtags(tweet) == 'StandWithLouisiana'

(我只是将文件名作为此示例函数的参数)

这在测试通过的意义上是有效的,因为第一行通过了测试,但我基本上想做的是类似这样的事情,我不希望它像它写的那样工作。

def test_hashtag(tweet):
    assert hashtags(tweet) == 'StandWithLouisiana' # first tweet
    assert hashtags(tweet) == ''    # second tweet

这失败了,因为它测试第一条推文(json 中的行)是否为空,而不是第二条推文。我假设这是因为夹具中的 return,但如果我尝试 yield 而不是 return,我会得到一个 yield_fixture function has more than one 'yield' 错误(并且第二行仍然失败).

为了解决这个问题,我现在正在做的是让每一行成为一个单独的 JSON 文件,然后为每个文件创建一个单独的固定装置。 (对于较短的示例,我使用 StringIO 来编写 JSON 内联)。 这确实有效,但感觉不雅。我觉得我应该为此使用 @pytest.mark.parametrize ,但我无法理解它。我想我也尝试 pytest_generate_tests 这样做,但它会测试每个键。是否可以按照我的想法进行操作,或者当我对断言有不同的值时创建单独的装置是否更好?

我认为最适合您的方法是对夹具进行参数化:

import json
import pathlib
import pytest


lines = pathlib.Path('data.json').read_text().split('\n')

@pytest.fixture(params=lines)
def tweet(request):
    line = request.param
    return json.loads(line)


def hashtags(t):
    return ' '.join([h['text'] for h in t['entities']['hashtags']])


def test_hashtag(tweet):
    assert hashtags(tweet) == 'StandWithLouisiana'

这将对 tweet 的每个返回值调用一次 test_hashtag:

$ pytest -v
...
test_spam.py::test_hashtag[{"contributors":null,"coordinates":null,"created_at":"Sat Aug 20 01:00:12 +0000 2016","entities":{"hashtags":[{"indices":[97,116],"text":"StandWithLouisiana"}]}}]
test_spam.py::test_hashtag[{"contributors":null,"coordinates":null,"created_at":"Sat Aug 20 01:01:35 +0000 2016","entities":{"hashtags":[]}}]
...

编辑:扩展夹具以提供预期值

您可以将预期值包含到 tweet 夹具参数中,然后将其原封不动地传递给测试。在下面的示例中,预期的标签与文件行一起压缩以构建 (line, tag) 形式的对。 tweet fixture 将行加载到字典中,通过标记,因此测试中的 tweet 参数变成一对值。

import json
import pathlib
import pytest


lines = pathlib.Path('data.json').read_text().split('\n')
expected_tags = ['StandWithLouisiana', '']

@pytest.fixture(params=zip(lines, expected_tags),
                ids=tuple(repr(tag) for tag in expected_tags))
def tweet(request):
    line, tag = request.param
    return (json.loads(line), tag)


def hashtags(t):
    return ' '.join([h['text'] for h in t['entities']['hashtags']])


def test_hashtag(tweet):
    data, tag = tweet
    assert hashtags(data) == tag

测试 运行 像以前一样产生两个测试:

test_spam.py::test_hashtag['StandWithLouisiana'] PASSED
test_spam.py::test_hashtag[''] PASSED

编辑 2:使用间接参数化

另一种可能更简洁的方法是让 tweet fixture 仅处理从原始字符串解析推文,将参数化移动到测试本身。我在这里使用 indirect parametrization 将原始行传递给 tweet 固定装置:

import json
import pathlib
import pytest


lines = pathlib.Path('data.json').read_text().split('\n')
expected_tags = ['StandWithLouisiana', '']

@pytest.fixture
def tweet(request):
    line = request.param
    return json.loads(line)


def hashtags(t):
    return ' '.join([h['text'] for h in t['entities']['hashtags']])


@pytest.mark.parametrize('tweet, tag', 
                         zip(lines, expected_tags),
                         ids=tuple(repr(tag) for tag in expected_tags),
                         indirect=('tweet',))
def test_hashtag(tweet, tag):
    assert hashtags(tweet) == tag

测试 运行 现在还会产生两个测试:

test_spam.py::test_hashtag['StandWithLouisiana'] PASSED
test_spam.py::test_hashtag[''] PASSED