@pytest.fixture.
decorator @pytest.fixture.
The function itself is executed at the moment when it is needed (before the test class, module or function) and when the value returned to it is available in the test itself. In this case, fixtures can use other fixtures, in addition, you can determine the lifetime of a specific fixture: in the current session, module, class, or function. They help us to contain tests in a modular form. And when testing integration, reuse them from neighboring test libraries. Flexibility and ease of use were among the main criteria for choosing pytest . To use the fixture, you need to specify its name as a parameter to the test.setUp/tearDown
;crashdump
;8081
and receiving GET
requests. The server takes a line from the text
parameter and in the response changes each word to its flipped word. It is given to json, if the client is able to receive it: import json from flask import Flask, request, make_response as response app = Flask(__name__) @app.route("/") def index(): text = request.args.get('text') json_type = 'application/json' json_accepted = json_type in request.headers.get('Accept', '') if text: words = text.split() reversed_words = [word[::-1] for word in words] if json_accepted: res = response(json.dumps({'text': reversed_words}), 200) else: res = response(' '.join(reversed_words), 200) else: res = response('text not found', 501) res.headers['Content-Type'] = json_type if json_accepted else 'text/plain' return res if __name__ == "__main__": app.run(host='0.0.0.0', port=8081)
import pytest import socket as s @pytest.fixture def socket(request): _socket = s.socket(s.AF_INET, s.SOCK_STREAM) def socket_teardown(): _socket.close() request.addfinalizer(socket_teardown) return _socket def test_server_connect(socket): socket.connect(('localhost', 8081)) assert socket
yield_fixture
, which represents a fixture in the form of a context manager that implements setUP/tearDown
and returns an object. @pytest.yield_fixture def socket(): _socket = s.socket(s.AF_INET, s.SOCK_STREAM) yield _socket _socket.close()
yield_fixture
looks more concise and clearer. It should be noted that the default fixtures have a lifetime scope=function
. This means that each test run with its parameters causes a new instance of the fixture.Server
fixture, describing where the web server under test is located. Since it returns an object that stores static information, and we don’t need to generate it every time, we set the scope=module
. The result that this fixture generates will be cached and will exist all the time the current module is launched: @pytest.fixture(scope='module') def Server(): class Dummy: host_port = 'localhost', 8081 uri = 'http://%s:%s/' % host_port return Dummy def test_server_connect(socket, Server): socket.connect(Server.host_port) assert socket
scope=session
and scope=class
- the lifetime of the fixture. And you can not use inside the fixture with a high level fixture with a lower value of scope=
.autouse
. They are dangerous because they can change the data unnoticed. For their flexible use, you can check the presence of the required fixture for the called test: @pytest.yield_fixture(scope='function', autouse=True) def collect_logs(request): if 'Server' in request.fixturenames: with some_logfile_collector(SERVER_LOCATION): yield else: yield
Service
fixture returns the object of the service being tested and has a set_time
method with which you can change the date and time: @pytest.yield_fixture def reset_shifted_time(Service): yield Service.set_time(datetime.datetime.now()) @pytest.mark.usefixtures("reset_shifted_time") class TestWithShiftedTime(): def test_shift_milesecond(self, Service): Service.set_time() assert ... def test_shift_time_far_far_away(self, Service): Service.set_time() assert ...
conftest.py
. After the fixture is described in this file, it becomes visible for all tests, and you do not need to import
.assert
. Assert is a standard Python instruction that verifies the statement described in it. We follow the rule “In one test - one assert
”. It allows you to test a specific functionality without affecting the steps of data preparation or bringing the service to the desired state. If the test uses data preparation steps that may cause an error, then it is better for them to write a separate test. Using this structure, we describe the expected behavior of the system.assert
. I advise you to use them until you need something more complicated. def test_dict(): assert dict(foo='bar', baz=None).items() == list({'foo': 'bar'}.iteritems())
E assert [('foo', 'bar...('baz', None)] == [('foo', 'bar')] E Left contains more items, first extra item: ('baz', None)
test_server_connect
method to more accurately determine that we do not expect a specific exception
. To do this, use the PyHamcrest framework: from hamcrest import * SOCKET_ERROR = s.error def test_server_connect(socket, Server): assert_that(calling(socket.connect).with_args(Server.host_port), is_not(raises(SOCKET_ERROR)))
has_property
and contains_string,
in this way contains_string,
we get easy-to-use simple matchmakers: def has_content(item): return has_property('text', item if isinstance(item, BaseMatcher) else contains_string(item)) def has_status(status): return has_property('status_code', equal_to(status))
BaseModifyMatcher
class, which forms such a matcher based on the class attributes: description
- the description of the matcher, modify
- the modifier function of the value being checked, instance
- the type of the class that is expected in the modifier: from hamcrest.core.base_matcher import BaseMatcher class BaseModifyMatcher(BaseMatcher): def __init__(self, item_matcher): self.item_matcher = item_matcher def _matches(self, item): if isinstance(item, self.instance) and item: self.new_item = self.modify(item) return self.item_matcher.matches(self.new_item) else: return False def describe_mismatch(self, item, mismatch_description): if isinstance(item, self.instance) and item: self.item_matcher.describe_mismatch(self.new_item, mismatch_description) else: mismatch_description.append_text('not %s, was: ' % self.instance) \ .append_text(repr(item)) def describe_to(self, description): description.append_text(self.description) \ .append_text(' ') \ .append_description_of(self.item_matcher)
text
parameter. Using BaseModifyMatcher
, we will write a matcher who will receive a list of ordinary words and will wait in the answer for a line of inverted words: rom hamcrest.core.helpers.wrap_matcher import wrap_matcher reverse_words = lambda words: [word[::-1] for word in words] def contains_reversed_words(item_match): """ Example: >>> from hamcrest import * >>> contains_reversed_words(contains_inanyorder('oof', 'rab')).matches("foo bar") True """ class IsStringOfReversedWords(BaseModifyMatcher): description = 'string of reversed words' modify = lambda _, item: reverse_words(item.split()) instance = basestring return IsStringOfReversedWords(wrap_matcher(item_match))
BaseModifyMatcher
, will check for a line containing json: import json as j def is_json(item_match): """ Example: >>> from hamcrest import * >>> is_json(has_entries('foo', contains('bar'))).matches('{"foo": ["bar"]}') True """ class AsJson(BaseModifyMatcher): description = 'json with' modify = lambda _, item: j.loads(item) instance = basestring return AsJson(wrap_matcher(item_match))
has_status
, has_content
and contains_reversed_words
has_content
described above: def test_server_response(Server): assert_that(requests.get(Server.uri), all_of(has_content('text not found'), has_status(501))) def test_server_request(Server): text = 'Hello word!' assert_that(requests.get(Server.uri, params={'text': text}), all_of( has_content(contains_reversed_words(text.split())), has_status(200) ))
@pytest.mark.parametrize
. You can specify several parameters in one parametrize
. If the parameters are divided into several parametrize
, then they are multiplied.test_server_request
method, describing the text
options: @pytest.mark.parametrize('text', ['Hello word!', ' 440 005 ', 'one_word']) def test_server_request(text, Server): assert_that(requests.get(Server.uri, params={'text': text}), all_of( has_content(contains_reversed_words(text.split())), has_status(200) ))
json
answer if the client supports it. Rewrite the test using objects instead of the usual parameters. Matcher will vary depending on the type of answer. I advise you to give more understandable names to the matcher: class DefaultCase: def __init__(self, text): self.req = dict( params={'text': text}, headers={}, ) self.match_string_of_reversed_words = all_of( has_content(contains_reversed_words(text.split())), has_status(200), ) class JSONCase(DefaultCase): def __init__(self, text): DefaultCase.__init__(self, text) self.req['headers'].update({'Accept': 'application/json'}) self.match_string_of_reversed_words = all_of( has_content(is_json(has_entries('text', contains(*reverse_words(text.split()))))), has_status(200), ) @pytest.mark.parametrize('case', [testclazz(text) for text in 'Hello word!', ' 440 005 ', 'one_word' for testclazz in JSONCase, DefaultCase]) def test_server_request(case, Server): assert_that(requests.get(Server.uri, **case.req), case.match_string_of_reversed_words)
py.test -v test_server.py
, we get the report: $ py.test -v test_server.py ============================= test session starts ============================= platform linux2 -- Python 2.7.3 -- py-1.4.20 -- pytest-2.5.2 -- /usr/bin/python plugins: timeout, allure-adaptor collected 8 items test_server.py:26: test_server_connect PASSED test_server.py:89: test_server_response PASSED test_server.py:109: test_server_request[case0] PASSED test_server.py:109: test_server_request[case1] PASSED test_server.py:109: test_server_request[case2] PASSED test_server.py:109: test_server_request[case3] PASSED test_server.py:109: test_server_request[case4] PASSED test_server.py:109: test_server_request[case5] PASSED ========================== 8 passed in 0.11 seconds ===========================
__repr__
method for the Case
class and write an auxiliary idparametrize
decorator, in which we use the additional parameter ids=
decorator pytest.mark.parametrize
: def idparametrize(name, values, fixture=False): return pytest.mark.parametrize(name, values, ids=map(repr, values), indirect=fixture) class DefaultCase: def __init__(self, text): self.text = text self.req = dict( params={'text': self.text}, headers={}, ) self.match_string_of_reversed_words = all_of( has_content(contains_reversed_words(self.text.split())), has_status(200), ) def __repr__(self): return 'text="{text}", {cls}'.format(cls=self.__class__.__name__, text=self.text) class JSONCase(DefaultCase): def __init__(self, text): DefaultCase.__init__(self, text) self.req['headers'].update({'Accept': 'application/json'}) self.match_string_of_reversed_words = all_of( has_content(is_json(has_entries('text', contains(*reverse_words(text.split()))))), has_status(200), ) @idparametrize('case', [testclazz(text) for text in 'Hello word!', ' 440 005 ', 'one_word' for testclazz in JSONCase, DefaultCase]) def test_server_request(case, Server): assert_that(requests.get(Server.uri, **case.req), case.match_string_of_reversed_words)
$ py.test -v test_server.py ============================= test session starts ============================= platform linux2 -- Python 2.7.3 -- py-1.4.20 -- pytest-2.5.2 -- /usr/bin/python plugins: ordering, timeout, allure-adaptor, qabs-yadt collected 8 items test_server.py:26: test_server_connect PASSED test_server.py:89: test_server_response PASSED test_server.py:117: test_server_request[text="Hello word!", JSONCase] PASSED test_server.py:117: test_server_request[text="Hello word!", DefaultCase] PASSED test_server.py:117: test_server_request[text=" 440 005 ", JSONCase] PASSED test_server.py:117: test_server_request[text=" 440 005 ", DefaultCase] PASSED test_server.py:117: test_server_request[text="one_word", JSONCase] PASSED test_server.py:117: test_server_request[text="one_word", DefaultCase] PASSED ========================== 8 passed in 0.12 seconds ===========================
idparametrize
decorator idparametrize
and pay attention to the fixture
parameter, you can see that you can parameterize the fixtures. In the following example, we check that the server is responding correctly, both on the real ip and on the local one. To do this, you need to tweak the Server
fixture a bit so that it can take parameters: from collections import namedtuple Srv = namedtuple('Server', 'host port') REAL_IP = s.gethostbyname(s.gethostname()) @pytest.fixture def Server(request): class Dummy: def __init__(self, srv): self.srv = srv @property def uri(self): return 'http://{host}:{port}/'.format(**self.srv._asdict()) return Dummy(request.param) @idparametrize('Server', [Srv('localhost', 8081), Srv(REAL_IP, 8081)], fixture=True) @idparametrize('case', [Case('Hello word!'), Case('Hello word!', json=True)]) def test_server_request(case, Server): assert_that(requests.get(Server.uri, **case.req), case.match_string_of_reversed_words)
@pytest.mark.MARK_NAME
. For example, each testpack can go for a few minutes, or even more. Therefore, I would like to get rid of the critical tests first and then the others: @pytest.mark.acceptance def test_server_connect(socket, Server): assert_that(calling(socket.connect).with_args(Server.host_port), is_not(raises(SOCKET_ERROR))) @pytest.mark.acceptance def test_server_response(Server): assert_that(requests.get(Server.uri), all_of(has_content('text not found'), has_status(501))) @pytest.mark.P1 def test_server_404(Server): assert_that(requests.get(Server.uri + 'not_found'), has_status(404)) @pytest.mark.P2 def test_server_simple_request(Server, SlowConnection): with SlowConnection(drop_packets=0.3): assert_that(requests.get(Server.uri + '?text=asdf'), has_content('fdsa'))
multi-configuration project
. For this task, in the Configuration Matrix
section, define the User-defined Axis
as a TESTPACK
containing ['acceptance', 'P1', 'P2', 'other']
. This task runs the tests in turn, and acceptance
tests will be started first, and their successful execution will be a condition for running other tests: #!/bin/bash PYTEST="py.test $WORKSPACE/tests/functional/ $TEST_PARAMS --junitxml=report.xml --alluredir=reports" if [ "$TESTPACK" = "other" ] then $PYTEST -m "not acceptance and not P1 and not P2" || true else $PYTEST -m $TESTPACK || true fi
xfail
. In addition to how to mark the entire test, you can mark the test parameters. So in the following example, when specifying the ipv6 address host='::1',
, the server does not respond. To solve this problem, you need to use in the server code instead of 0.0.0.0
. We will not fix it yet to see how our test responds to this situation. Additionally, you can describe the reason in the optional parameter reason
. This text will appear in the launch report: @pytest.yield_fixture def Server(request): class Dummy: def __init__(self, srv): self.srv = srv self.conn = None @property def uri(self): return 'http://{host}:{port}/'.format(**self.srv._asdict()) def connect(self): self.conn = s.create_connection((self.srv.host, self.srv.port)) self.conn.sendall('HEAD /404 HTTP/1.0\r\n\r\n') self.conn.recv(1024) def close(self): if self.conn: self.conn.close() res = Dummy(request.param) yield res res.close() SERVER_CASES = [ pytest.mark.xfail(Srv('::1', 8081), reason='ipv6 desn`t work, use `::` instead of `0.0.0.0`'), Srv(REAL_IP, 8081), ] @idparametrize('Server', SERVER_CASES, fixture=True) def test_server(Server): assert_that(calling(Server.connect), is_not(raises(SOCKET_ERROR)))
pytest.mark.skipif()
tag. It allows you to skip these tests using a specific condition.py.test
command, or as the python -m pytest
module python -m pytest
.norecursedirs
;test_*.py
or *_test.py
;Test
, that have no __init__
method;test_
;tox.ini
config in the root folder with the tests: [tox] envlist=py27 [testenv] deps= builders pytest pytest-allure-adaptor PyHamcrest commands= py.test tests/functional/ \ --junitxml=report.xml \ --alluredir=reports \ --verbose \ {posargs}
tox
. He will do his virtualenv
in the .tox
folder, .tox
up the dependencies needed to run the tests, and eventually run pytest with the parameters specified in the config.python setup.py test
. To do this, you need to arrange your setup.py
in accordance with the documentation .py.test --pep8 --doctest-modules -v --junit-xml report.xml self_tests/ ft_lib/
: py.test --pep8 --doctest-modules -v --junit-xml report.xml self_tests/ ft_lib/
ERROR
status. There are several approaches to this in pytest :pytest.set_trace()
anywhere in your test, you can immediately drop out to pdb
in the specified location;--pdb
parameter, which will launch the debugger when an error occurs;import pudb;pudb.set_trace()
in front of suspicious places (the main thing is to remember to add the -s
parameter to the test launch line).-k
when you need to run a separate test. It should be borne in mind that if you want to run two tests or use additional filters, then you need to follow the new syntax of this parameter. py.test -k "prepare or http and proxy" tests/functional/
;-x
when you need to stop executing tests at the first test or error that has fallen;--collect-only
when you need to check the correctness and the number of generated parameters to the tests and the list of tests that will be run (similar to the dry-run);--no-magic
hints at us that there is magic here :)assert
;depends
pytest.mark.skip[if]
;assert
;xfail
. , , , .type
. attachemnt' : txt
, html
, xml
, png
, jpg
, json
.error_if_wat
, , ERROR_CONDITION
. Server
. allure.step
. socket
. requests
. allure.attach
. docstring , , . import allure ERROR_CONDITION = None @pytest.fixture def error_if_wat(request): assert request.getfuncargvalue('Server').srv != ERROR_CONDITION SERVER_CASES = [ pytest.mark.xfail(Srv('::1', 8081), reason='ipv6 desn`t work, use `::` instead of `0.0.0.0`'), Srv('127.0.0.1', 8081), Srv('localhost', 80), ERROR_CONDITION, ] @idparametrize('Server', SERVER_CASES, fixture=True) def test_server(Server, error_if_wat): assert_that(calling(Server.connect), is_not(raises(SOCKET_ERROR))) """ Step 1: Try connect to host, port, and check for not raises SOCKET_ERROR. Step 2: Check for server response 'text not found' message. Response status should be equal to 501. """ with allure.step('Try connect'): assert_that(calling(Server.connect), is_not(raises(SOCKET_ERROR))) with allure.step('Check response'): response = requests.get(Server.uri) allure.attach('response_body', response.text, type='html') allure.attach('response_headers', j.dumps(dict(response.headers), indent=4), type='json') allure.attach('response_status', str(response.status_code)) assert_that(response, all_of(has_content('text not found'), has_status(501)))
py.test --alluredir=/var/tmp/allure/ test_server.py
. sudo add-apt-repository ppa:yandex-qatools/allure-framework sudo apt-get install yandex-allure-cli allure generate -o /var/tmp/allure/output/ -- /var/tmp/allure/
/var/tmp/allure/output
. index.html
.logging
.uri
, . pytest-localserver
Source: https://habr.com/ru/post/242795/
All Articles