Pytest与行定义的JSON

时间:2019-04-15 08:05:13

标签: python json pytest tweets

我对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

这失败了,因为它测试了第一个tweet(json中的行)是否为空,而不是第二个。我认为这是因为固定装置中的return,但是如果我尝试使用yield而不是return,则会收到yield_fixture function has more than one 'yield'错误`(第二行仍然失败)。

我现在要解决此问题的方法是,使每行成为一个单独的JSON文件,然后为它们中的每一个创建一个单独的夹具。 (对于较短的示例,我使用StringIO来编写JSON内联)。 这确实有效,但感觉不佳。我觉得我应该为此使用@pytest.mark.parametrize,但是我无法理解。我想我也尝试过pytest_generate_tests来做到这一点,但是它将测试每个键。有可能做我想做的事情,还是当我对断言有不同的值时创建单独的夹具更好?

1 个答案:

答案 0 :(得分:1)

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

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'

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

$ 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夹具将行加载到字典中,使标记通过,因此测试中的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固定装置仅处理来自原始字符串的推文解析,将参数化移至测试本身。我正在使用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