summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/tools/pytest/doc/en/example/simple.rst
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/tools/pytest/doc/en/example/simple.rst')
-rw-r--r--testing/web-platform/tests/tools/pytest/doc/en/example/simple.rst751
1 files changed, 751 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/pytest/doc/en/example/simple.rst b/testing/web-platform/tests/tools/pytest/doc/en/example/simple.rst
new file mode 100644
index 000000000..be12d2afe
--- /dev/null
+++ b/testing/web-platform/tests/tools/pytest/doc/en/example/simple.rst
@@ -0,0 +1,751 @@
+
+.. highlightlang:: python
+
+Basic patterns and examples
+==========================================================
+
+Pass different values to a test function, depending on command line options
+----------------------------------------------------------------------------
+
+.. regendoc:wipe
+
+Suppose we want to write a test that depends on a command line option.
+Here is a basic pattern to achieve this::
+
+ # content of test_sample.py
+ def test_answer(cmdopt):
+ if cmdopt == "type1":
+ print ("first")
+ elif cmdopt == "type2":
+ print ("second")
+ assert 0 # to see what was printed
+
+
+For this to work we need to add a command line option and
+provide the ``cmdopt`` through a :ref:`fixture function <fixture function>`::
+
+ # content of conftest.py
+ import pytest
+
+ def pytest_addoption(parser):
+ parser.addoption("--cmdopt", action="store", default="type1",
+ help="my option: type1 or type2")
+
+ @pytest.fixture
+ def cmdopt(request):
+ return request.config.getoption("--cmdopt")
+
+Let's run this without supplying our new option::
+
+ $ py.test -q test_sample.py
+ F
+ ======= FAILURES ========
+ _______ test_answer ________
+
+ cmdopt = 'type1'
+
+ def test_answer(cmdopt):
+ if cmdopt == "type1":
+ print ("first")
+ elif cmdopt == "type2":
+ print ("second")
+ > assert 0 # to see what was printed
+ E assert 0
+
+ test_sample.py:6: AssertionError
+ --------------------------- Captured stdout call ---------------------------
+ first
+ 1 failed in 0.12 seconds
+
+And now with supplying a command line option::
+
+ $ py.test -q --cmdopt=type2
+ F
+ ======= FAILURES ========
+ _______ test_answer ________
+
+ cmdopt = 'type2'
+
+ def test_answer(cmdopt):
+ if cmdopt == "type1":
+ print ("first")
+ elif cmdopt == "type2":
+ print ("second")
+ > assert 0 # to see what was printed
+ E assert 0
+
+ test_sample.py:6: AssertionError
+ --------------------------- Captured stdout call ---------------------------
+ second
+ 1 failed in 0.12 seconds
+
+You can see that the command line option arrived in our test. This
+completes the basic pattern. However, one often rather wants to process
+command line options outside of the test and rather pass in different or
+more complex objects.
+
+Dynamically adding command line options
+--------------------------------------------------------------
+
+.. regendoc:wipe
+
+Through :confval:`addopts` you can statically add command line
+options for your project. You can also dynamically modify
+the command line arguments before they get processed::
+
+ # content of conftest.py
+ import sys
+ def pytest_cmdline_preparse(args):
+ if 'xdist' in sys.modules: # pytest-xdist plugin
+ import multiprocessing
+ num = max(multiprocessing.cpu_count() / 2, 1)
+ args[:] = ["-n", str(num)] + args
+
+If you have the :ref:`xdist plugin <xdist>` installed
+you will now always perform test runs using a number
+of subprocesses close to your CPU. Running in an empty
+directory with the above conftest.py::
+
+ $ py.test
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 0 items
+
+ ======= no tests ran in 0.12 seconds ========
+
+.. _`excontrolskip`:
+
+Control skipping of tests according to command line option
+--------------------------------------------------------------
+
+.. regendoc:wipe
+
+Here is a ``conftest.py`` file adding a ``--runslow`` command
+line option to control skipping of ``slow`` marked tests::
+
+ # content of conftest.py
+
+ import pytest
+ def pytest_addoption(parser):
+ parser.addoption("--runslow", action="store_true",
+ help="run slow tests")
+
+We can now write a test module like this::
+
+ # content of test_module.py
+
+ import pytest
+
+
+ slow = pytest.mark.skipif(
+ not pytest.config.getoption("--runslow"),
+ reason="need --runslow option to run"
+ )
+
+
+ def test_func_fast():
+ pass
+
+
+ @slow
+ def test_func_slow():
+ pass
+
+and when running it will see a skipped "slow" test::
+
+ $ py.test -rs # "-rs" means report details on the little 's'
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 2 items
+
+ test_module.py .s
+ ======= short test summary info ========
+ SKIP [1] test_module.py:14: need --runslow option to run
+
+ ======= 1 passed, 1 skipped in 0.12 seconds ========
+
+Or run it including the ``slow`` marked test::
+
+ $ py.test --runslow
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 2 items
+
+ test_module.py ..
+
+ ======= 2 passed in 0.12 seconds ========
+
+Writing well integrated assertion helpers
+--------------------------------------------------
+
+.. regendoc:wipe
+
+If you have a test helper function called from a test you can
+use the ``pytest.fail`` marker to fail a test with a certain message.
+The test support function will not show up in the traceback if you
+set the ``__tracebackhide__`` option somewhere in the helper function.
+Example::
+
+ # content of test_checkconfig.py
+ import pytest
+ def checkconfig(x):
+ __tracebackhide__ = True
+ if not hasattr(x, "config"):
+ pytest.fail("not configured: %s" %(x,))
+
+ def test_something():
+ checkconfig(42)
+
+The ``__tracebackhide__`` setting influences ``pytest`` showing
+of tracebacks: the ``checkconfig`` function will not be shown
+unless the ``--fulltrace`` command line option is specified.
+Let's run our little function::
+
+ $ py.test -q test_checkconfig.py
+ F
+ ======= FAILURES ========
+ _______ test_something ________
+
+ def test_something():
+ > checkconfig(42)
+ E Failed: not configured: 42
+
+ test_checkconfig.py:8: Failed
+ 1 failed in 0.12 seconds
+
+Detect if running from within a pytest run
+--------------------------------------------------------------
+
+.. regendoc:wipe
+
+Usually it is a bad idea to make application code
+behave differently if called from a test. But if you
+absolutely must find out if your application code is
+running from a test you can do something like this::
+
+ # content of conftest.py
+
+ def pytest_configure(config):
+ import sys
+ sys._called_from_test = True
+
+ def pytest_unconfigure(config):
+ del sys._called_from_test
+
+and then check for the ``sys._called_from_test`` flag::
+
+ if hasattr(sys, '_called_from_test'):
+ # called from within a test run
+ else:
+ # called "normally"
+
+accordingly in your application. It's also a good idea
+to use your own application module rather than ``sys``
+for handling flag.
+
+Adding info to test report header
+--------------------------------------------------------------
+
+.. regendoc:wipe
+
+It's easy to present extra information in a ``pytest`` run::
+
+ # content of conftest.py
+
+ def pytest_report_header(config):
+ return "project deps: mylib-1.1"
+
+which will add the string to the test header accordingly::
+
+ $ py.test
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ project deps: mylib-1.1
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 0 items
+
+ ======= no tests ran in 0.12 seconds ========
+
+.. regendoc:wipe
+
+You can also return a list of strings which will be considered as several
+lines of information. You can of course also make the amount of reporting
+information on e.g. the value of ``config.option.verbose`` so that
+you present more information appropriately::
+
+ # content of conftest.py
+
+ def pytest_report_header(config):
+ if config.option.verbose > 0:
+ return ["info1: did you know that ...", "did you?"]
+
+which will add info only when run with "--v"::
+
+ $ py.test -v
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 -- $PYTHON_PREFIX/bin/python3.4
+ cachedir: .cache
+ info1: did you know that ...
+ did you?
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collecting ... collected 0 items
+
+ ======= no tests ran in 0.12 seconds ========
+
+and nothing when run plainly::
+
+ $ py.test
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 0 items
+
+ ======= no tests ran in 0.12 seconds ========
+
+profiling test duration
+--------------------------
+
+.. regendoc:wipe
+
+.. versionadded: 2.2
+
+If you have a slow running large test suite you might want to find
+out which tests are the slowest. Let's make an artificial test suite::
+
+ # content of test_some_are_slow.py
+
+ import time
+
+ def test_funcfast():
+ pass
+
+ def test_funcslow1():
+ time.sleep(0.1)
+
+ def test_funcslow2():
+ time.sleep(0.2)
+
+Now we can profile which test functions execute the slowest::
+
+ $ py.test --durations=3
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 3 items
+
+ test_some_are_slow.py ...
+
+ ======= slowest 3 test durations ========
+ 0.20s call test_some_are_slow.py::test_funcslow2
+ 0.10s call test_some_are_slow.py::test_funcslow1
+ 0.00s setup test_some_are_slow.py::test_funcfast
+ ======= 3 passed in 0.12 seconds ========
+
+incremental testing - test steps
+---------------------------------------------------
+
+.. regendoc:wipe
+
+Sometimes you may have a testing situation which consists of a series
+of test steps. If one step fails it makes no sense to execute further
+steps as they are all expected to fail anyway and their tracebacks
+add no insight. Here is a simple ``conftest.py`` file which introduces
+an ``incremental`` marker which is to be used on classes::
+
+ # content of conftest.py
+
+ import pytest
+
+ def pytest_runtest_makereport(item, call):
+ if "incremental" in item.keywords:
+ if call.excinfo is not None:
+ parent = item.parent
+ parent._previousfailed = item
+
+ def pytest_runtest_setup(item):
+ if "incremental" in item.keywords:
+ previousfailed = getattr(item.parent, "_previousfailed", None)
+ if previousfailed is not None:
+ pytest.xfail("previous test failed (%s)" %previousfailed.name)
+
+These two hook implementations work together to abort incremental-marked
+tests in a class. Here is a test module example::
+
+ # content of test_step.py
+
+ import pytest
+
+ @pytest.mark.incremental
+ class TestUserHandling:
+ def test_login(self):
+ pass
+ def test_modification(self):
+ assert 0
+ def test_deletion(self):
+ pass
+
+ def test_normal():
+ pass
+
+If we run this::
+
+ $ py.test -rx
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 4 items
+
+ test_step.py .Fx.
+ ======= short test summary info ========
+ XFAIL test_step.py::TestUserHandling::()::test_deletion
+ reason: previous test failed (test_modification)
+
+ ======= FAILURES ========
+ _______ TestUserHandling.test_modification ________
+
+ self = <test_step.TestUserHandling object at 0xdeadbeef>
+
+ def test_modification(self):
+ > assert 0
+ E assert 0
+
+ test_step.py:9: AssertionError
+ ======= 1 failed, 2 passed, 1 xfailed in 0.12 seconds ========
+
+We'll see that ``test_deletion`` was not executed because ``test_modification``
+failed. It is reported as an "expected failure".
+
+
+Package/Directory-level fixtures (setups)
+-------------------------------------------------------
+
+If you have nested test directories, you can have per-directory fixture scopes
+by placing fixture functions in a ``conftest.py`` file in that directory
+You can use all types of fixtures including :ref:`autouse fixtures
+<autouse fixtures>` which are the equivalent of xUnit's setup/teardown
+concept. It's however recommended to have explicit fixture references in your
+tests or test classes rather than relying on implicitly executing
+setup/teardown functions, especially if they are far away from the actual tests.
+
+Here is a an example for making a ``db`` fixture available in a directory::
+
+ # content of a/conftest.py
+ import pytest
+
+ class DB:
+ pass
+
+ @pytest.fixture(scope="session")
+ def db():
+ return DB()
+
+and then a test module in that directory::
+
+ # content of a/test_db.py
+ def test_a1(db):
+ assert 0, db # to show value
+
+another test module::
+
+ # content of a/test_db2.py
+ def test_a2(db):
+ assert 0, db # to show value
+
+and then a module in a sister directory which will not see
+the ``db`` fixture::
+
+ # content of b/test_error.py
+ def test_root(db): # no db here, will error out
+ pass
+
+We can run this::
+
+ $ py.test
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 7 items
+
+ test_step.py .Fx.
+ a/test_db.py F
+ a/test_db2.py F
+ b/test_error.py E
+
+ ======= ERRORS ========
+ _______ ERROR at setup of test_root ________
+ file $REGENDOC_TMPDIR/b/test_error.py, line 1
+ def test_root(db): # no db here, will error out
+ fixture 'db' not found
+ available fixtures: record_xml_property, recwarn, cache, capsys, pytestconfig, tmpdir_factory, capfd, monkeypatch, tmpdir
+ use 'py.test --fixtures [testpath]' for help on them.
+
+ $REGENDOC_TMPDIR/b/test_error.py:1
+ ======= FAILURES ========
+ _______ TestUserHandling.test_modification ________
+
+ self = <test_step.TestUserHandling object at 0xdeadbeef>
+
+ def test_modification(self):
+ > assert 0
+ E assert 0
+
+ test_step.py:9: AssertionError
+ _______ test_a1 ________
+
+ db = <conftest.DB object at 0xdeadbeef>
+
+ def test_a1(db):
+ > assert 0, db # to show value
+ E AssertionError: <conftest.DB object at 0xdeadbeef>
+ E assert 0
+
+ a/test_db.py:2: AssertionError
+ _______ test_a2 ________
+
+ db = <conftest.DB object at 0xdeadbeef>
+
+ def test_a2(db):
+ > assert 0, db # to show value
+ E AssertionError: <conftest.DB object at 0xdeadbeef>
+ E assert 0
+
+ a/test_db2.py:2: AssertionError
+ ======= 3 failed, 2 passed, 1 xfailed, 1 error in 0.12 seconds ========
+
+The two test modules in the ``a`` directory see the same ``db`` fixture instance
+while the one test in the sister-directory ``b`` doesn't see it. We could of course
+also define a ``db`` fixture in that sister directory's ``conftest.py`` file.
+Note that each fixture is only instantiated if there is a test actually needing
+it (unless you use "autouse" fixture which are always executed ahead of the first test
+executing).
+
+
+post-process test reports / failures
+---------------------------------------
+
+If you want to postprocess test reports and need access to the executing
+environment you can implement a hook that gets called when the test
+"report" object is about to be created. Here we write out all failing
+test calls and also access a fixture (if it was used by the test) in
+case you want to query/look at it during your post processing. In our
+case we just write some informations out to a ``failures`` file::
+
+ # content of conftest.py
+
+ import pytest
+ import os.path
+
+ @pytest.hookimpl(tryfirst=True, hookwrapper=True)
+ def pytest_runtest_makereport(item, call):
+ # execute all other hooks to obtain the report object
+ outcome = yield
+ rep = outcome.get_result()
+
+ # we only look at actual failing test calls, not setup/teardown
+ if rep.when == "call" and rep.failed:
+ mode = "a" if os.path.exists("failures") else "w"
+ with open("failures", mode) as f:
+ # let's also access a fixture for the fun of it
+ if "tmpdir" in item.fixturenames:
+ extra = " (%s)" % item.funcargs["tmpdir"]
+ else:
+ extra = ""
+
+ f.write(rep.nodeid + extra + "\n")
+
+
+if you then have failing tests::
+
+ # content of test_module.py
+ def test_fail1(tmpdir):
+ assert 0
+ def test_fail2():
+ assert 0
+
+and run them::
+
+ $ py.test test_module.py
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 2 items
+
+ test_module.py FF
+
+ ======= FAILURES ========
+ _______ test_fail1 ________
+
+ tmpdir = local('PYTEST_TMPDIR/test_fail10')
+
+ def test_fail1(tmpdir):
+ > assert 0
+ E assert 0
+
+ test_module.py:2: AssertionError
+ _______ test_fail2 ________
+
+ def test_fail2():
+ > assert 0
+ E assert 0
+
+ test_module.py:4: AssertionError
+ ======= 2 failed in 0.12 seconds ========
+
+you will have a "failures" file which contains the failing test ids::
+
+ $ cat failures
+ test_module.py::test_fail1 (PYTEST_TMPDIR/test_fail10)
+ test_module.py::test_fail2
+
+Making test result information available in fixtures
+-----------------------------------------------------------
+
+.. regendoc:wipe
+
+If you want to make test result reports available in fixture finalizers
+here is a little example implemented via a local plugin::
+
+ # content of conftest.py
+
+ import pytest
+
+ @pytest.hookimpl(tryfirst=True, hookwrapper=True)
+ def pytest_runtest_makereport(item, call):
+ # execute all other hooks to obtain the report object
+ outcome = yield
+ rep = outcome.get_result()
+
+ # set an report attribute for each phase of a call, which can
+ # be "setup", "call", "teardown"
+
+ setattr(item, "rep_" + rep.when, rep)
+
+
+ @pytest.fixture
+ def something(request):
+ def fin():
+ # request.node is an "item" because we use the default
+ # "function" scope
+ if request.node.rep_setup.failed:
+ print ("setting up a test failed!", request.node.nodeid)
+ elif request.node.rep_setup.passed:
+ if request.node.rep_call.failed:
+ print ("executing test failed", request.node.nodeid)
+ request.addfinalizer(fin)
+
+
+if you then have failing tests::
+
+ # content of test_module.py
+
+ import pytest
+
+ @pytest.fixture
+ def other():
+ assert 0
+
+ def test_setup_fails(something, other):
+ pass
+
+ def test_call_fails(something):
+ assert 0
+
+ def test_fail2():
+ assert 0
+
+and run it::
+
+ $ py.test -s test_module.py
+ ======= test session starts ========
+ platform linux -- Python 3.4.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 3 items
+
+ test_module.py Esetting up a test failed! test_module.py::test_setup_fails
+ Fexecuting test failed test_module.py::test_call_fails
+ F
+
+ ======= ERRORS ========
+ _______ ERROR at setup of test_setup_fails ________
+
+ @pytest.fixture
+ def other():
+ > assert 0
+ E assert 0
+
+ test_module.py:6: AssertionError
+ ======= FAILURES ========
+ _______ test_call_fails ________
+
+ something = None
+
+ def test_call_fails(something):
+ > assert 0
+ E assert 0
+
+ test_module.py:12: AssertionError
+ _______ test_fail2 ________
+
+ def test_fail2():
+ > assert 0
+ E assert 0
+
+ test_module.py:15: AssertionError
+ ======= 2 failed, 1 error in 0.12 seconds ========
+
+You'll see that the fixture finalizers could use the precise reporting
+information.
+
+Integrating pytest runner and cx_freeze
+-----------------------------------------------------------
+
+If you freeze your application using a tool like
+`cx_freeze <http://cx-freeze.readthedocs.org>`_ in order to distribute it
+to your end-users, it is a good idea to also package your test runner and run
+your tests using the frozen application.
+
+This way packaging errors such as dependencies not being
+included into the executable can be detected early while also allowing you to
+send test files to users so they can run them in their machines, which can be
+invaluable to obtain more information about a hard to reproduce bug.
+
+Unfortunately ``cx_freeze`` can't discover them
+automatically because of ``pytest``'s use of dynamic module loading, so you
+must declare them explicitly by using ``pytest.freeze_includes()``::
+
+ # contents of setup.py
+ from cx_Freeze import setup, Executable
+ import pytest
+
+ setup(
+ name="app_main",
+ executables=[Executable("app_main.py")],
+ options={"build_exe":
+ {
+ 'includes': pytest.freeze_includes()}
+ },
+ # ... other options
+ )
+
+If you don't want to ship a different executable just in order to run your tests,
+you can make your program check for a certain flag and pass control
+over to ``pytest`` instead. For example::
+
+ # contents of app_main.py
+ import sys
+
+ if len(sys.argv) > 1 and sys.argv[1] == '--pytest':
+ import pytest
+ sys.exit(pytest.main(sys.argv[2:]))
+ else:
+ # normal application execution: at this point argv can be parsed
+ # by your argument-parsing library of choice as usual
+ ...
+
+This makes it convenient to execute your tests from within your frozen
+application, using standard ``py.test`` command-line options::
+
+ ./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/