Skip to content

Commit d2e4d2f

Browse files
committed
📝 Update testing
* Rearrange testing section * pytest vs unittest * pytest monkeypatching * Add behave * Mocking with Typer * Expand glossary
1 parent 84b90c9 commit d2e4d2f

File tree

9 files changed

+264
-47
lines changed

9 files changed

+264
-47
lines changed

docs/appendix/glossary.rst

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Glossary
44
.. glossary::
55
:sorted:
66

7+
Acceptance test
8+
User Acceptance Test
9+
Verification that software functions as intended from the user’s
10+
perspective and that users accept the software. Acceptance tests are
11+
primarily used in :term:` Extreme Programming`.
12+
713
Argument
814
A value that is passed to a function. There are two types of arguments:
915

@@ -80,6 +86,14 @@ Glossary
8086
* :ref:`pytest_fail`
8187
* :class:`python3:Exception`
8288

89+
Extreme Programming
90+
XP
91+
Software development methodology that aims to improve software quality
92+
and responsiveness to changing customer requirements. As a form of agile
93+
software development, it advocates frequent releases in short development
94+
cycles to increase productivity and introduce control points where new
95+
requirements can be taken into account.
96+
8397
F-string
8498
:doc:`String </types/strings/built-in-modules/string>` literal preceded
8599
by an ``f`` or ``F``.
@@ -712,8 +726,27 @@ Glossary
712726

713727
Test-driven development
714728
TDD
715-
A software development strategy in which the tests are written before the
716-
code.
729+
A technique for creating software that guides software development by
730+
writing tests. It was developed in the late 1990s by Kent Beck as part of
731+
Extreme Programming. Essentially, it involves repeating three simple
732+
steps:
733+
734+
* Write a test for the next feature to be added.
735+
* Write the function code until the test passes.
736+
* Refactor both the new and old code to make it well structured.
737+
738+
Although these three steps, often summarised as *‘red – green –
739+
refactor’*, form the core of the process, there is also an important
740+
first step, in which a list of test cases is created. One of these tests
741+
is then selected, *‘Red – Green – Refactor’* is applied to it, and the
742+
next test is selected. During the process, further tests are added to
743+
this list.
744+
745+
.. seealso::
746+
* `Canon TDD <https://tidyfirst.substack.com/p/canon-tdd>`_ by Kent
747+
Beck
748+
* `Test-driven development by example
749+
<https://archive.org/details/est-driven-development-by-example/test-driven-development-by-example/>`_ by Kent Beck
717750

718751
``try``
719752
A keyword that protects a part of the code that can throw an

docs/test/behave/example.rst

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
Example
2+
=======
3+
4+
#. After installing behave, we can create a file called :file:`install.feature`
5+
in the :file:`features` directory with the following content:
6+
7+
.. code-block:: gherkin
8+
9+
Feature: showing off behave
10+
11+
Scenario: run a simple test
12+
Given we have behave installed
13+
When we implement a test
14+
Then behave will test it for us!
15+
16+
.. seealso::
17+
`Features <https://behave.readthedocs.io/en/latest/tutorial/#features>`_
18+
19+
#. Next, we create the file :file:`install.py` in the directory
20+
:file:`features/steps`:
21+
22+
.. code-block:: python
23+
24+
from behave import *
25+
26+
27+
@given("we have behave installed")
28+
def step_impl(context):
29+
pass
30+
31+
32+
@when("we implement a test")
33+
def step_impl(context):
34+
assert True is not False
35+
36+
37+
@then("behave will test it for us")
38+
def step_impl(context):
39+
assert context.failed is False
40+
41+
.. seealso::
42+
`Python Step Implementations
43+
<https://behave.readthedocs.io/en/latest/tutorial/#python-step-implementations>`_
44+
45+
#. Call ``behave``
46+
47+
.. code-block:: console
48+
49+
$ behave
50+
USING RUNNER: behave.runner:Runner
51+
Feature: showing off behave # features/install.feature:1
52+
53+
Scenario: run a simple test # features/install.feature:3
54+
Given we have behave installed # features/steps/install.py:3 0.000s
55+
When we implement a test # features/steps/install.py:7 0.000s
56+
Then behave will test it for us # features/steps/install.py:11 0.000s
57+
58+
1 feature passed, 0 failed, 0 skipped
59+
1 scenario passed, 0 failed, 0 skipped
60+
3 steps passed, 0 failed, 0 skipped
61+
Took 0min 0.000s

docs/test/behave/index.rst

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
behave
2+
======
3+
4+
`behave <https://behave.readthedocs.io/en/latest/>`_ implements behaviour-driven
5+
development (:abbr:`BDD (Behavior-Driven Development)`). BDD is an agile
6+
software development technique that promotes collaboration between development,
7+
quality assurance and non-technical or business staff on a software project. The
8+
term was originally coined in 2003 by `Daniel Terhorst-North
9+
<https://dannorth.net/introducing-bdd>`_ in response to :term:`test-driven
10+
development <Test-driven development>` and encompasses :term:`acceptance testing
11+
<Acceptance test>` or customer-test-driven development practices as found in
12+
:term:`extreme programming <Extreme programming>`. In `Selling BDD to the
13+
Business <https://speakerdeck.com/tastapod/selling-bdd-to-the-business>`_, he
14+
gave the following definition:
15+
16+
*‚BDD is a second-generation, outside–in, pull-based, multiple-stakeholder,
17+
multiple-scale, high-automation, agile methodology. It describes a cycle of
18+
interactions with well-defined outputs, resulting in the delivery of
19+
working, tested software that matters.‘*
20+
21+
Gherkin
22+
-------
23+
24+
Description language based on natural written language for the textual
25+
specification of software requirements. Only certain keywords are predefined.
26+
27+
.. code-block:: gherkin
28+
29+
Scenario: Add an item to the database
30+
Given an empty database
31+
When an item with a summary is added
32+
Then the number of items should be 1
33+
and the queried item from the db should correspond to the added object.
34+
35+
Each scenario is an example intended to illustrate a specific aspect of the
36+
application’s behaviour.
37+
38+
Installation
39+
------------
40+
41+
You can install behave in your :ref:`virtual environments <venv>` with:
42+
43+
.. tab:: Linux/macOS
44+
45+
.. code-block:: console
46+
47+
$ python -m pip install behave
48+
49+
.. tab:: Windows
50+
51+
.. code-block:: ps1con
52+
53+
C:> python -m pip install behave
54+
55+
.. toctree::
56+
:titlesonly:
57+
:hidden:
58+
59+
example

docs/test/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ Basically, a distinction is made between static and dynamic test procedures.
1919
:titlesonly:
2020
:hidden:
2121

22-
pytest/index
2322
unittest
23+
pytest/index
24+
behave/index
2425
mock
2526
hypothesis
2627
tox

docs/test/mock.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ We can then simply use this fixture to test the version in
123123
def test_version(items_cli):
124124
assert items_cli("version") == items.__version__
125125
126+
.. seealso::
127+
`Typer Learn Testing <https://typer.tiangolo.com/tutorial/testing/>`_
128+
126129
Mocking of attributes
127130
---------------------
128131

docs/test/pytest/builtin-fixtures.rst

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -270,38 +270,53 @@ code is restored and everything that was changed by the patch is undone.
270270

271271
The ``monkeypatch`` fixture offers the following functions:
272272

273-
+-------------------------------------------------------+-----------------------+
274-
| Function | Description |
275-
+=======================================================+=======================+
276-
| :samp:`setattr(TARGET, NAME, VALUE, raising=True)` | sets an attribute |
277-
| [1]_ | |
278-
+-------------------------------------------------------+-----------------------+
279-
| :samp:`delattr(TARGET, NAME, raising=True)` [1]_ | deletes an attribute |
280-
+-------------------------------------------------------+-----------------------+
281-
| :samp:`setitem(DICT, NAME, VALUE)` | sets a dict entry |
282-
| | |
283-
+-------------------------------------------------------+-----------------------+
284-
| :samp:`delitem(DICT, NAME, raising=True)` [1]_ | deletes a dict entry |
285-
+-------------------------------------------------------+-----------------------+
286-
| :samp:`setenv(NAME, VALUE, prepend=None)` [2]_ | sets an environment |
287-
| | variable |
288-
+-------------------------------------------------------+-----------------------+
289-
| :samp:`delenv(NAME, raising=True)` [1]_ | deletes an environment|
290-
| | variable |
291-
+-------------------------------------------------------+-----------------------+
292-
| :samp:`syspath_prepend(PATH)` | expands the path |
293-
| | ``sys.path`` |
294-
+-------------------------------------------------------+-----------------------+
295-
| :samp:`chdir(PATH)` | changes the current |
296-
| | working directory |
297-
+-------------------------------------------------------+-----------------------+
273+
+-----------------------------------------------+-----------------------+
274+
| Function | Description |
275+
+===============================================+=======================+
276+
| :meth:`monkeypatch.setattr(obj, name, value, | sets an attribute |
277+
| raising=True) | |
278+
| <pytest.MonkeyPatch.setattr>` | |
279+
| [1]_ | |
280+
+-----------------------------------------------+-----------------------+
281+
| :meth:`monkeypatch.delattr(obj, name, | deletes an attribute |
282+
| raising=True) | |
283+
| <pytest.MonkeyPatch.delattr>` | |
284+
| [1]_ | |
285+
+-----------------------------------------------+-----------------------+
286+
| :meth:`monkeypatch.setitem(mapping, name, | sets a dict entry |
287+
| value) <pytest.MonkeyPatch.setitem>` | |
288+
+-----------------------------------------------+-----------------------+
289+
| :meth:`monkeypatch.delitem(obj, name, | deletes a dict entry |
290+
| raising=True) <pytest.MonkeyPatch.delitem>` | |
291+
| [1]_ | |
292+
+-----------------------------------------------+-----------------------+
293+
| :meth:`monkeypatch.setenv(name, value, | sets an environment |
294+
| prepend=None) <pytest.MonkeyPatch.setenv>` | variable |
295+
| [2]_ | |
296+
+-----------------------------------------------+-----------------------+
297+
| :meth:`monkeypatch.delenv(name, raising=True) | deletes an environment|
298+
| <pytest.MonkeyPatch.delenv>` | variable |
299+
| [1]_ | |
300+
+-----------------------------------------------+-----------------------+
301+
| :meth:`monkeypatch.syspath_prepend(path) | expands the path |
302+
| <pytest.MonkeyPatch.syspath_prepend>` | :py:data:`sys.path` |
303+
+-----------------------------------------------+-----------------------+
304+
| :meth:`monkeypatch.chdir(path) | changes the current |
305+
| <pytest.MonkeyPatch.chdir>` | working directory |
306+
+-----------------------------------------------+-----------------------+
307+
| :meth:`monkeypatch.context() | changes the current |
308+
| <pytest.MonkeyPatch.context>` | context |
309+
+-----------------------------------------------+-----------------------+
298310

299311
.. [1] The ``raising`` :term:`parameter` tells pytest whether an exception
300312
should be thrown if the element is not (yet) present.
301313
.. [2] The ``prepend`` :term:`parameter` of ``setenv()`` can be a character. If
302314
it is set, the value of the environment variable is changed to
303315
:samp:`{VALUE} + prepend + {OLD_VALUE}`
304316
317+
RMonkey patching of environment variables
318+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
319+
305320
We can use ``monkeypatch`` to redirect the :abbr:`CLI (Command Line Interface)`
306321
to a temporary directory for the database in two ways. Both methods require
307322
knowledge of the application code. Let’s take a look at the method
@@ -392,6 +407,44 @@ environment variable :envvar:`ITEMS_DB_DIR` that can be easily patched:
392407
monkeypatch.setenv("ITEMS_DB_DIR", str(tmp_path))
393408
assert run_items_cli("config") == str(tmp_path)
394409
410+
Monkey patching dictionaries
411+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
412+
413+
The path could also have been specified in a dictionary, for example:
414+
415+
.. code-block:: python
416+
:caption: conf.py
417+
418+
DEFAULT_CONFIG = {"database": "items_db"}
419+
420+
421+
def create_connection(config=None):
422+
"""Create a connection string from input or defaults."""
423+
config = config or DEFAULT_CONFIG
424+
return f"Location={config['database']};"
425+
426+
For testing purposes, we can change the values in the ``DEFAULT_CONFIG``
427+
dictionary:
428+
429+
.. code-block:: python
430+
:caption: tests/test_conf.py
431+
432+
from items import conf
433+
434+
435+
def test_connection(monkeypatch):
436+
monkeypatch.setitem(conf.DEFAULT_CONFIG, "database", "test_db")
437+
438+
Alternatively, you could have defined a fixture with:
439+
440+
.. code-block:: python
441+
:caption: tests/conftest.py
442+
443+
@pytest.fixture
444+
def mock_test_database(monkeypatch):
445+
"""Set the DEFAULT_CONFIG database to test_db."""
446+
monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
447+
395448
Remaining built-in fixtures
396449
---------------------------
397450

docs/test/pytest/fixtures.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ But before you familiarise yourself with fixtures and use them to test Items,
2020
let’s take a look at a small example fixture and learn how fixtures and test
2121
functions are connected.
2222

23+
.. seealso::
24+
* `pytest fixtures <https://docs.pytest.org/en/latest/explanation/fixtures.html>`_
25+
* `pytest fixtures reference
26+
<https://docs.pytest.org/en/latest/reference/fixtures.html>`_
27+
* `How to use fixtures
28+
<https://docs.pytest.org/en/latest/how-to/fixtures.html#how-to-fixtures>`_
29+
2330
First steps with fixtures
2431
-------------------------
2532

docs/test/pytest/index.rst

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,25 @@ pytest
44
:doc:`pytest <pytest:index>` is an alternative to Python’s :doc:`../unittest`
55
module that simplifies testing even further.
66

7-
Features
8-
--------
9-
10-
* More detailed information about failed ``assert`` statements
11-
* Automatic detection of test modules and functions
12-
* Modular fixtures for the management of small or :term:`parameterised
13-
<Parameter>`, long-lived test resources
14-
* Can also execute unit tests without presets
15-
* Extensive plug-in architecture, with over 800 external plug-ins
7+
* pytest automatically recognises tests based on filenames and functions that
8+
start with ``test_``, while unittest derives test classes and methods from
9+
:class:`unittest.TestCase`. This results in simpler, more readable syntax with
10+
less boilerplate code.
11+
* unittest provides a set of assertion methods (for example,
12+
:func:`assertEqual`, :func:`assertTrue`, :func:`assertRaises`). With pytest,
13+
the same assertions can be defined, but using Python’s standard :func:`assert`
14+
statement. This often results in more meaningful error messages and better
15+
introspection.
16+
* unittest only provides :func:`setUp` and :func:`tearDown` methods for
17+
fixtures. In pytest, on the other hand, :doc:`fixtures <fixtures>` are defined
18+
as functions, which promotes reusability and simplifies the management of
19+
test dependencies.
20+
* Parametrised tests are possible in unittest, but require additional effort.
21+
pytest, however, includes the :doc:`decorator <../../functions/decorators>`
22+
``@pytest.mark.parametrize``, which makes it easy to run test functions with
23+
different inputs and expected outputs.
24+
* pytest has an extensive ecosystem with over 800 :doc:`plugins` for advanced
25+
testing requirements; unittest is more limited in its extensibility.
1626

1727
Installation
1828
------------

docs/test/unittest.rst

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,6 @@ It provides the following test concepts:
1515
Test Fixture
1616
is a consistent test environment.
1717

18-
.. seealso::
19-
* `pytest fixtures
20-
<https://docs.pytest.org/en/latest/explanation/fixtures.html>`_
21-
* `About fixtures
22-
<https://docs.pytest.org/en/latest/explanation/fixtures.html#about-fixtures>`_
23-
* `Fixtures reference
24-
<https://docs.pytest.org/en/latest/reference/fixtures.html>`_
25-
* `How to use fixtures
26-
<https://docs.pytest.org/en/latest/how-to/fixtures.html#how-to-fixtures>`_
27-
2818
Test Suite
2919
is a collection of several :term:`test cases <Test Case>`.
3020

0 commit comments

Comments
 (0)