File: testing.rst

package info (click to toggle)
cloud-init 25.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 12,412 kB
  • sloc: python: 135,894; sh: 3,883; makefile: 141; javascript: 30; xml: 22
file content (148 lines) | stat: -rw-r--r-- 6,329 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
.. _testing:

Testing
*******

``Cloud-init`` has both unit tests and integration tests. Unit tests can
be found at :file:`tests/unittests`. Integration tests can be found at
:file:`tests/integration_tests`. Documentation specifically for integration
tests can be found on the :ref:`integration_tests` page, but
the guidelines specified below apply to both types of tests.

``Cloud-init`` uses `pytest`_ to write and run its tests.

.. note::
  While there are a subset of tests written as ``unittest.TestCase``
  sub-classes, this is due to historical reasons. Their use is discouraged and
  they are tracked to be removed in `#6427`_.

Guidelines
==========

The following guidelines should be followed.

Test layout
-----------

* For consistency, unit test files should have a matching name and
  directory location under :file:`tests/unittests`.

* E.g., the expected test file for code in :file:`cloudinit/path/to/file.py`
  is :file:`tests/unittests/path/to/test_file.py`.

``pytest`` guidelines
---------------------

* Use `pytest fixtures`_ to share functionality instead of inheritance.

* Use bare ``assert`` statements, to take advantage of ``pytest``'s
  `assertion introspection`_.

* Prefer ``pytest``'s
  `parametrized tests <https://docs.pytest.org/en/stable/example/parametrize.html>`__
  over test repetition.

In-house fixtures
-----------------

Before implementing your own fixture do search in :file:`*/conftest.py` files
as it could be already implemented. Another source to look for test helpers is
:file:`tests/*/helpers.py`.

Relevant fixtures:

* `disable_subp_usage`_ auto-disables call to subprocesses. See its
  documentation to disable it.

* `fake_filesystem`_ makes tests run on a temporary filesystem.

* `paths`_  provides an instance of `cloudinit.helper.Paths` pointing to a
  temporary filesystem.

Dependency versions
-------------------

Cloud-init supports a range of versions for each of its test dependencies, as
well as runtime dependencies. If you are unsure whether a specific feature is
supported for a particular dependency, check the ``lowest-supported``
environment in ``tox.ini``. This can be run using ``tox -e lowest-supported``.
This runs as a Github Actions job when a pull request is submitted or updated.

Mocking and assertions
----------------------

* Variables/parameter names for ``Mock`` or ``MagicMock`` instances
  should start with ``m_`` to clearly distinguish them from non-mock
  variables. For example, ``m_readurl`` (which would be a mock for
  ``readurl``).

* The ``assert_*`` methods that are available on ``Mock`` and
  ``MagicMock`` objects should be avoided, as typos in these method
  names may not raise ``AttributeError`` (and so can cause tests to
  silently pass).

  * **An important exception:** if a ``Mock`` is `autospecced`_ then
    misspelled assertion methods *will* raise an ``AttributeError``, so these
    assertion methods may be used on autospecced ``Mock`` objects.

* For a non-autospecced ``Mock``, these substitutions can be used
  (``m`` is assumed to be a ``Mock``):

  * ``m.assert_any_call(*args, **kwargs)`` => ``assert
    mock.call(*args, **kwargs) in m.call_args_list``
  * ``m.assert_called()`` => ``assert 0 != m.call_count``
  * ``m.assert_called_once()`` => ``assert 1 == m.call_count``
  * ``m.assert_called_once_with(*args, **kwargs)`` => ``assert
    [mock.call(*args, **kwargs)] == m.call_args_list``
  * ``m.assert_called_with(*args, **kwargs)`` => ``assert
    mock.call(*args, **kwargs) == m.call_args_list[-1]``
  * ``m.assert_has_calls(call_list, any_order=True)`` => ``for call in
    call_list: assert call in m.call_args_list``

    * ``m.assert_has_calls(...)`` and ``m.assert_has_calls(...,
      any_order=False)`` are not easily replicated in a single
      statement, so their use when appropriate is acceptable.

  * ``m.assert_not_called()`` => ``assert 0 == m.call_count``

* When there are multiple patch calls in a test file for the module it
  is testing, it may be desirable to capture the shared string prefix
  for these patch calls in a module-level variable. If used, such
  variables should be named ``M_PATH`` or, for datasource tests, ``DS_PATH``.

Test argument ordering
----------------------

* Test arguments should be ordered as follows:

  * ``mock.patch`` arguments.  When used as a decorator, ``mock.patch``
    partially applies its generated ``Mock`` object as the first
    argument, so these arguments must go first.
  * ``pytest.mark.parametrize`` arguments, in the order specified to
    the ``parametrize`` decorator. These arguments are also provided
    by a decorator, so it's natural that they sit next to the
    ``mock.patch`` arguments.
  * Fixture arguments, alphabetically. These are not provided by a
    decorator, so they are last, and their order has no defined
    meaning, so we default to alphabetical.

* It follows from this ordering of test arguments (so that we retain
  the property that arguments left-to-right correspond to decorators
  bottom-to-top) that test decorators should be ordered as follows:

  * ``pytest.mark.parametrize``
  * ``mock.patch``

.. LINKS:
.. _pytest: https://docs.pytest.org/
.. _pytest fixtures: https://docs.pytest.org/en/latest/fixture.html
.. _TestGetPackageMirrorInfo: https://github.com/canonical/cloud-init/blob/42f69f410ab8850c02b1f53dd67c132aa8ef64f5/cloudinit/distros/tests/test_init.py\#L15
.. _TestPrependBaseCommands: https://github.com/canonical/cloud-init/blob/fbcb224bc12495ba200ab107246349d802c5d8e6/cloudinit/tests/test_subp.py#L20
.. _assertion introspection: https://docs.pytest.org/en/latest/assert.html
.. _pytest 3.0: https://docs.pytest.org/en/latest/changelog.html#id1093
.. _pytest.param: https://docs.pytest.org/en/6.2.x/reference.html#pytest-param
.. _autospecced: https://docs.python.org/3.8/library/unittest.mock.html#autospeccing
.. _#6427: https://github.com/canonical/cloud-init/issues/6427
.. _disable_subp_usage: https://github.com/canonical/cloud-init/blob/16f2039d0705ee9873ace98c967a34e6da6d0b87/conftest.py#L92
.. _fake_filesystem: https://github.com/canonical/cloud-init/blob/16f2039d0705ee9873ace98c967a34e6da6d0b87/tests/unittests/conftest.py#L114
.. _paths: https://github.com/canonical/cloud-init/blob/16f2039d0705ee9873ace98c967a34e6da6d0b87/tests/unittests/conftest.py#L224