unit.rst 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982
  1. .. _unit-tests:
  2. ==================
  3. Writing Unit Tests
  4. ==================
  5. Introduction
  6. ============
  7. Like many software projects, Salt has two broad-based testing approaches --
  8. integration testing and unit testing. While integration testing focuses on the
  9. interaction between components in a sandboxed environment, unit testing focuses
  10. on the singular implementation of individual functions.
  11. Unit tests should be used specifically to test a function's logic. Unit tests
  12. rely on mocking external resources.
  13. While unit tests are good for ensuring consistent results, they are most
  14. useful when they do not require more than a few mocks. Effort should be
  15. made to mock as many external resources as possible. This effort is encouraged,
  16. but not required. Sometimes the isolation provided by completely mocking the
  17. external dependencies is not worth the effort of mocking those dependencies.
  18. In these cases, requiring an external library to be installed on the
  19. system before running the test file is a useful way to strike this balance.
  20. For example, the unit tests for the MySQL execution module require the
  21. presence of the MySQL python bindings on the system running the test file
  22. before proceeding to run the tests.
  23. Overly detailed mocking can also result in decreased test readability and
  24. brittleness as the tests are more likely to fail when the code or its
  25. dependencies legitimately change. In these cases, it is better to add
  26. dependencies to the test runner dependency state.
  27. Preparing to Write a Unit Test
  28. ==============================
  29. This guide assumes that your Salt development environment is already configured
  30. and that you have a basic understanding of contributing to the Salt codebase.
  31. If you're unfamiliar with either of these topics, please refer to the
  32. :ref:`Installing Salt for Development<installing-for-development>` and the
  33. :ref:`Contributing<contributing>` pages, respectively.
  34. This documentation also assumes that you have an understanding of how to
  35. :ref:`run Salt's test suite<running-the-tests>`, including running the
  36. :ref:`unit test subsection<running-test-subsections>`, running the unit tests
  37. :ref:`without testing daemons<running-unit-tests-no-daemons>` to speed up
  38. development wait times, and running a unit test file, class, or individual test.
  39. Best Practices
  40. ==============
  41. Unit tests should be written to the following specifications.
  42. What to Test?
  43. -------------
  44. Since unit testing focuses on the singular implementation of individual functions,
  45. unit tests should be used specifically to test a function's logic. The following
  46. guidelines should be followed when writing unit tests for Salt's test suite:
  47. - Each ``raise`` and ``return`` statement needs to be independently tested.
  48. - Isolate testing functionality. Don't rely on the pass or failure of other,
  49. separate tests.
  50. - Test functions should contain only one assertion.
  51. - Many Salt execution modules are merely wrappers for distribution-specific
  52. functionality. If there isn't any logic present in a simple execution module,
  53. consider writing an :ref:`integration test<integration-tests>` instead of
  54. heavily mocking a call to an external dependency.
  55. Mocking Test Data
  56. -----------------
  57. A reasonable effort needs to be made to mock external resources used in the
  58. code being tested, such as APIs, function calls, external data either
  59. globally available or passed in through function arguments, file data, etc.
  60. - Test functions should contain only one assertion and all necessary mock code
  61. and data for that assertion.
  62. - External resources should be mocked in order to "block all of the exits". If a
  63. test function fails because something in an external library wasn't mocked
  64. properly (or at all), this test is not addressing all of the "exits" a function
  65. may experience. We want the Salt code and logic to be tested, specifically.
  66. - Consider the fragility and longevity of a test. If the test is so tightly coupled
  67. to the code being tested, this makes a test unnecessarily fragile.
  68. - Make sure you are not mocking the function to be tested so vigorously that the
  69. test return merely tests the mocked output. The test should always be testing
  70. a function's logic.
  71. Mocking Loader Modules
  72. ----------------------
  73. Salt loader modules use a series of globally available dunder variables,
  74. ``__salt__``, ``__opts__``, ``__pillar__``, etc. To facilitate testing these
  75. modules a mixin class was created, ``LoaderModuleMockMixin`` which can be found
  76. in ``tests/support/mixins.py``. The reason for the existence of this class is
  77. because historiclly and because it was easier, one would add these dunder
  78. variables directly on the imported module. This however, introduces unexpected
  79. behavior when running the full test suite since those attributes would not be
  80. removed once we were done testing the module and would therefore leak to other
  81. modules being tested with unpredictable results. This is the kind of work that
  82. should be deferred to mock, and that's exactly what this mixin class does.
  83. As an example, if one needs to specify some options which should be available
  84. to the module being tested one should do:
  85. .. code-block:: python
  86. import salt.modules.somemodule as somemodule
  87. class SomeModuleTest(TestCase, LoaderModuleMockMixin):
  88. def setup_loader_modules(self):
  89. return {
  90. somemodule: {
  91. '__opts__': {'test': True}
  92. }
  93. }
  94. Consider this more extensive example from
  95. ``tests/unit/modules/test_libcloud_dns.py``:
  96. .. code-block:: python
  97. # Import Python Libs
  98. from __future__ import absolute_import
  99. # Import Salt Testing Libs
  100. from tests.support.mixins import LoaderModuleMockMixin
  101. from tests.support.unit import TestCase, skipIf
  102. from tests.support.mock import (
  103. patch,
  104. MagicMock,
  105. NO_MOCK,
  106. NO_MOCK_REASON
  107. )
  108. import salt.modules.libcloud_dns as libcloud_dns
  109. class MockDNSDriver(object):
  110. def __init__(self):
  111. pass
  112. def get_mock_driver():
  113. return MockDNSDriver()
  114. @skipIf(NO_MOCK, NO_MOCK_REASON)
  115. @patch('salt.modules.libcloud_dns._get_driver',
  116. MagicMock(return_value=MockDNSDriver()))
  117. class LibcloudDnsModuleTestCase(TestCase, LoaderModuleMockMixin):
  118. def setup_loader_modules(self):
  119. module_globals = {
  120. '__salt__': {
  121. 'config.option': MagicMock(return_value={
  122. 'test': {
  123. 'driver': 'test',
  124. 'key': '2orgk34kgk34g'
  125. }
  126. })
  127. }
  128. }
  129. if libcloud_dns.HAS_LIBCLOUD is False:
  130. module_globals['sys.modules'] = {'libcloud': MagicMock()}
  131. return {libcloud_dns: module_globals}
  132. What happens in the above example is we mock a call to
  133. `__salt__['config.option']` to return the configuration needed for the
  134. execution of the tests. Additionally, if the ``libcloud`` library is not
  135. available, since that's not actually part of what's being tested, we mocked that
  136. import by patching ``sys.modules`` when tests are running.
  137. Mocking Filehandles
  138. -------------------
  139. .. note::
  140. This documentation applies to the 2018.3 release cycle and newer. The
  141. extended functionality for ``mock_open`` described below does not exist in
  142. the 2017.7 and older release branches.
  143. Opening files in Salt is done using ``salt.utils.files.fopen()``. When testing
  144. code that reads from files, the ``mock_open`` helper can be used to mock
  145. filehandles. Note that is not the same ``mock_open`` as
  146. :py:func:`unittest.mock.mock_open` from the Python standard library, but rather
  147. a separate implementation which has additional functionality.
  148. .. code-block:: python
  149. from tests.support.unit import TestCase, skipIf
  150. from tests.support.mock import (
  151. patch
  152. mock_open,
  153. NO_MOCK,
  154. NO_MOCK_REASON,
  155. )
  156. import salt.modules.mymod as mymod
  157. @skipIf(NO_MOCK, NO_MOCK_REASON)
  158. class MyAwesomeTestCase(TestCase):
  159. def test_something(self):
  160. fopen_mock = mock_open(read_data='foo\nbar\nbaz\n')
  161. with patch('salt.utils.files.fopen', fopen_mock):
  162. result = mymod.myfunc()
  163. assert result is True
  164. This will force any filehandle opened to mimic a filehandle which, when read,
  165. produces the specified contents.
  166. .. important::
  167. **String Types**
  168. When running tests on Python 2, ``mock_open`` will convert any ``unicode``
  169. types to ``str`` types to more closely reproduce Python 2 behavior (file
  170. reads are always ``str`` types in Python 2, irrespective of mode).
  171. However, when configuring your read_data, make sure that you are using
  172. bytestrings (e.g. ``b'foo\nbar\nbaz\n'``) when the code you are testing is
  173. opening a file for binary reading, otherwise the tests will fail on Python
  174. 3. The mocked filehandles produced by ``mock_open`` will raise a
  175. :py:obj:`TypeError` if you attempt to read a bytestring when opening for
  176. non-binary reading, and similarly will not let you read a string when
  177. opening a file for binary reading. They will also not permit bytestrings to
  178. be "written" if the mocked filehandle was opened for non-binary writing,
  179. and vice-versa when opened for non-binary writing. These enhancements force
  180. test writers to write more accurate tests.
  181. More Complex Scenarios
  182. **********************
  183. .. _unit-tests-multiple-file-paths:
  184. Multiple File Paths
  185. +++++++++++++++++++
  186. What happens when the code being tested reads from more than one file? For
  187. those cases, you can pass ``read_data`` as a dictionary:
  188. .. code-block:: python
  189. import textwrap
  190. from tests.support.unit import TestCase, skipIf
  191. from tests.support.mock import (
  192. patch
  193. mock_open,
  194. NO_MOCK,
  195. NO_MOCK_REASON,
  196. )
  197. import salt.modules.mymod as mymod
  198. @skipIf(NO_MOCK, NO_MOCK_REASON)
  199. class MyAwesomeTestCase(TestCase):
  200. def test_something(self):
  201. contents = {
  202. '/etc/foo.conf': textwrap.dedent('''\
  203. foo
  204. bar
  205. baz
  206. '''),
  207. '/etc/b*.conf': textwrap.dedent('''\
  208. one
  209. two
  210. three
  211. '''),
  212. }
  213. fopen_mock = mock_open(read_data=contents)
  214. with patch('salt.utils.files.fopen', fopen_mock):
  215. result = mymod.myfunc()
  216. assert result is True
  217. This would make ``salt.utils.files.fopen()`` produce filehandles with different
  218. contents depending on which file was being opened by the code being tested.
  219. ``/etc/foo.conf`` and any file matching the pattern ``/etc/b*.conf`` would
  220. work, while opening any other path would result in a
  221. :py:obj:`FileNotFoundError` being raised (in Python 2, an ``IOError``).
  222. Since file patterns are supported, it is possible to use a pattern of ``'*'``
  223. to define a fallback if no other patterns match the filename being opened. The
  224. below two ``mock_open`` calls would produce identical results:
  225. .. code-block:: python
  226. mock_open(read_data='foo\n')
  227. mock_open(read_data={'*': 'foo\n'})
  228. .. note::
  229. Take care when specifying the ``read_data`` as a dictionary, in cases where
  230. the patterns overlap (e.g. when both ``/etc/b*.conf`` and ``/etc/bar.conf``
  231. are in the ``read_data``). Dictionary iteration order will determine which
  232. pattern is attempted first, second, etc., with the exception of ``*`` which
  233. is used when no other pattern matches. If your test case calls for
  234. specifying overlapping patterns, and you are not running Python 3.6 or
  235. newer, then an ``OrderedDict`` can be used to ensure matching is handled in
  236. the desired way:
  237. .. code-block:: python
  238. contents = OrderedDict()
  239. contents['/etc/bar.conf'] = 'foo\nbar\nbaz\n'
  240. contents['/etc/b*.conf'] = IOError(errno.EACCES, 'Permission denied')
  241. contents['*'] = 'This is a fallback for files not beginning with "/etc/b"\n'
  242. fopen_mock = mock_open(read_data=contents)
  243. Raising Exceptions
  244. ++++++++++++++++++
  245. Instead of a string, an exception can also be used as the ``read_data``:
  246. .. code-block:: python
  247. import errno
  248. from tests.support.unit import TestCase, skipIf
  249. from tests.support.mock import (
  250. patch
  251. mock_open,
  252. NO_MOCK,
  253. NO_MOCK_REASON,
  254. )
  255. import salt.modules.mymod as mymod
  256. @skipIf(NO_MOCK, NO_MOCK_REASON)
  257. class MyAwesomeTestCase(TestCase):
  258. def test_something(self):
  259. exc = IOError(errno.EACCES, 'Permission denied')
  260. fopen_mock = mock_open(read_data=exc)
  261. with patch('salt.utils.files.fopen', fopen_mock):
  262. mymod.myfunc()
  263. The above example would raise the specified exception when any file is opened.
  264. The expectation would be that ``mymod.myfunc()`` would gracefully handle the
  265. IOError, so a failure to do that would result in it being raised and causing
  266. the test to fail.
  267. Multiple File Contents
  268. ++++++++++++++++++++++
  269. For cases in which a file is being read more than once, and it is necessary to
  270. test a function's behavior based on what the file looks like the second (or
  271. third, etc.) time it is read, just specify the the contents for that file as a
  272. list. Each time the file is opened, ``mock_open`` will cycle through the list
  273. and produce a mocked filehandle with the specified contents. For example:
  274. .. code-block:: python
  275. import errno
  276. import textwrap
  277. from tests.support.unit import TestCase, skipIf
  278. from tests.support.mock import (
  279. patch
  280. mock_open,
  281. NO_MOCK,
  282. NO_MOCK_REASON,
  283. )
  284. import salt.modules.mymod as mymod
  285. @skipIf(NO_MOCK, NO_MOCK_REASON)
  286. class MyAwesomeTestCase(TestCase):
  287. def test_something(self):
  288. contents = {
  289. '/etc/foo.conf': [
  290. textwrap.dedent('''\
  291. foo
  292. bar
  293. '''),
  294. textwrap.dedent('''\
  295. foo
  296. bar
  297. baz
  298. '''),
  299. ],
  300. '/etc/b*.conf': [
  301. IOError(errno.ENOENT, 'No such file or directory'),
  302. textwrap.dedent('''\
  303. one
  304. two
  305. three
  306. '''),
  307. ],
  308. }
  309. fopen_mock = mock_open(read_data=contents)
  310. with patch('salt.utils.files.fopen', fopen_mock):
  311. result = mymod.myfunc()
  312. assert result is True
  313. Using this example, the first time ``/etc/foo.conf`` is opened, it will
  314. simulate a file with the first string in the list as its contents, while the
  315. second time it is opened, the simulated file's contents will be the second
  316. string in the list.
  317. If no more items remain in the list, then attempting to open the file will
  318. raise a :py:obj:`RuntimeError`. In the example above, if ``/etc/foo.conf`` were
  319. to be opened a third time, a :py:obj:`RuntimeError` would be raised.
  320. Note that exceptions can also be mixed in with strings when using this
  321. technique. In the above example, if ``/etc/bar.conf`` were to be opened twice,
  322. the first time would simulate the file not existing, while the second time
  323. would simulate a file with string defined in the second element of the list.
  324. .. note::
  325. Notice that the second path in the ``contents`` dictionary above
  326. (``/etc/b*.conf``) contains an asterisk. The items in the list are cycled
  327. through for each match of a given pattern (*not* separately for each
  328. individual file path), so this means that only two files matching that
  329. pattern could be opened before the next one would raise a
  330. :py:obj:`RuntimeError`.
  331. Accessing the Mocked Filehandles in a Test
  332. ******************************************
  333. .. note::
  334. The code for the ``MockOpen``, ``MockCall``, and ``MockFH`` classes
  335. (referenced below) can be found in ``tests/support/mock.py``. There are
  336. extensive unit tests for them located in ``tests/unit/test_mock.py``.
  337. The above examples simply show how to mock ``salt.utils.files.fopen()`` to
  338. simulate files with the contents you desire, but you can also access the mocked
  339. filehandles (and more), and use them to craft assertions in your tests. To do
  340. so, just add an ``as`` clause to the end of the ``patch`` statement:
  341. .. code-block:: python
  342. fopen_mock = mock_open(read_data='foo\nbar\nbaz\n')
  343. with patch('salt.utils.files.fopen', fopen_mock) as m_open:
  344. # do testing here
  345. ...
  346. ...
  347. When doing this, ``m_open`` will be a ``MockOpen`` instance. It will contain
  348. several useful attributes:
  349. - **read_data** - A dictionary containing the ``read_data`` passed when
  350. ``mock_open`` was invoked. In the event that :ref:`multiple file paths
  351. <unit-tests-multiple-file-paths>` are not used, then this will be a
  352. dictionary mapping ``*`` to the ``read_data`` passed to ``mock_open``.
  353. - **call_count** - An integer representing how many times
  354. ``salt.utils.files.fopen()`` was called to open a file.
  355. - **calls** - A list of ``MockCall`` objects. A ``MockCall`` object is a simple
  356. class which stores the arguments passed to it, making the positional
  357. arguments available via its ``args`` attribute, and the keyword arguments
  358. available via its ``kwargs`` attribute.
  359. .. code-block:: python
  360. from tests.support.unit import TestCase, skipIf
  361. from tests.support.mock import (
  362. patch
  363. mock_open,
  364. MockCall,
  365. NO_MOCK,
  366. NO_MOCK_REASON,
  367. )
  368. import salt.modules.mymod as mymod
  369. @skipIf(NO_MOCK, NO_MOCK_REASON)
  370. class MyAwesomeTestCase(TestCase):
  371. def test_something(self):
  372. with patch('salt.utils.files.fopen', mock_open(read_data=b'foo\n')) as m_open:
  373. mymod.myfunc()
  374. # Assert that only two opens attempted
  375. assert m_open.call_count == 2
  376. # Assert that only /etc/foo.conf was opened
  377. assert all(call.args[0] == '/etc/foo.conf' for call in m_open.calls)
  378. # Asser that the first open was for binary read, and the
  379. # second was for binary write.
  380. assert m_open.calls == [
  381. MockCall('/etc/foo.conf', 'rb'),
  382. MockCall('/etc/foo.conf', 'wb'),
  383. ]
  384. Note that ``MockCall`` is imported from ``tests.support.mock`` in the above
  385. example. Also, the second assert above is redundant since it is covered in
  386. the final assert, but both are included simply as an example.
  387. - **filehandles** - A dictionary mapping the unique file paths opened, to lists
  388. of ``MockFH`` objects. Each open creates a unique ``MockFH`` object. Each
  389. ``MockFH`` object itself has a number of useful attributes:
  390. - **filename** - The path to the file which was opened using
  391. ``salt.utils.files.fopen()``
  392. - **call** - A ``MockCall`` object representing the arguments passed to
  393. ``salt.utils.files.fopen()``. Note that this ``MockCall`` is also available
  394. in the parent ``MockOpen`` instance's **calls** list.
  395. - The following methods are mocked using :py:class:`unittest.mock.Mock`
  396. objects, and Mock's built-in asserts (as well as the call data) can be used
  397. as you would with any other Mock object:
  398. - **.read()**
  399. - **.readlines()**
  400. - **.readline()**
  401. - **.close()**
  402. - **.write()**
  403. - **.writelines()**
  404. - **.seek()**
  405. - The read functions (**.read()**, **.readlines()**, **.readline()**) all
  406. work as expected, as does iterating through the file line by line (i.e.
  407. ``for line in fh:``).
  408. - The **.tell()** method is also implemented in such a way that it updates
  409. after each time the mocked filehandle is read, and will report the correct
  410. position. The one caveat here is that **.seek()** doesn't actually work
  411. (it's simply mocked), and will not change the position. Additionally,
  412. neither **.write()** or **.writelines()** will modify the mocked
  413. filehandle's contents.
  414. - The attributes **.write_calls** and **.writelines_calls** (no parenthesis)
  415. are available as shorthands and correspond to lists containing the contents
  416. passed for all calls to **.write()** and **.writelines()**, respectively.
  417. Examples
  418. ++++++++
  419. .. code-block:: python
  420. with patch('salt.utils.files.fopen', mock_open(read_data=contents)) as m_open:
  421. # Run the code you are unit testing
  422. mymod.myfunc()
  423. # Check that only the expected file was opened, and that it was opened
  424. # only once.
  425. assert m_open.call_count == 1
  426. assert list(m_open.filehandles) == ['/etc/foo.conf']
  427. # "opens" will be a list of all the mocked filehandles opened
  428. opens = m_open.filehandles['/etc/foo.conf']
  429. # Check that we wrote the expected lines ("expected" here is assumed to
  430. # be a list of strings)
  431. assert opens[0].write_calls == expected
  432. .. code-block:: python
  433. with patch('salt.utils.files.fopen', mock_open(read_data=contents)) as m_open:
  434. # Run the code you are unit testing
  435. mymod.myfunc()
  436. # Check that .readlines() was called (remember, it's a Mock)
  437. m_open.filehandles['/etc/foo.conf'][0].readlines.assert_called()
  438. .. code-block:: python
  439. with patch('salt.utils.files.fopen', mock_open(read_data=contents)) as m_open:
  440. # Run the code you are unit testing
  441. mymod.myfunc()
  442. # Check that we read the file and also wrote to it
  443. m_open.filehandles['/etc/foo.conf'][0].read.assert_called_once()
  444. m_open.filehandles['/etc/foo.conf'][1].writelines.assert_called_once()
  445. .. _`Mock()`: https://github.com/testing-cabal/mock
  446. Naming Conventions
  447. ------------------
  448. Test names and docstrings should indicate what functionality is being tested.
  449. Test functions are named ``test_<fcn>_<test-name>`` where ``<fcn>`` is the function
  450. being tested and ``<test-name>`` describes the ``raise`` or ``return`` being tested.
  451. Unit tests for ``salt/.../<module>.py`` are contained in a file called
  452. ``tests/unit/.../test_<module>.py``, e.g. the tests for ``salt/modules/fib.py``
  453. are in ``tests/unit/modules/test_fib.py``.
  454. In order for unit tests to get picked up during a run of the unit test suite, each
  455. unit test file must be prefixed with ``test_`` and each individual test must be
  456. prepended with the ``test_`` naming syntax, as described above.
  457. If a function does not start with ``test_``, then the function acts as a "normal"
  458. function and is not considered a testing function. It will not be included in the
  459. test run or testing output. The same principle applies to unit test files that
  460. do not have the ``test_*.py`` naming syntax. This test file naming convention
  461. is how the test runner recognizes that a test file contains unit tests.
  462. Imports
  463. -------
  464. Most commonly, the following imports are necessary to create a unit test:
  465. .. code-block:: python
  466. from tests.support.unit import TestCase, skipIf
  467. If you need mock support to your tests, please also import:
  468. .. code-block:: python
  469. from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch, call
  470. Evaluating Truth
  471. ================
  472. A longer discussion on the types of assertions one can make can be found by
  473. reading `Python's documentation on unit testing`__.
  474. .. __: http://docs.python.org/2/library/unittest.html#unittest.TestCase
  475. Tests Using Mock Objects
  476. ========================
  477. In many cases, the purpose of a Salt module is to interact with some external
  478. system, whether it be to control a database, manipulate files on a filesystem
  479. or something else. In these varied cases, it's necessary to design a unit test
  480. which can test the function whilst replacing functions which might actually
  481. call out to external systems. One might think of this as "blocking the exits"
  482. for code under tests and redirecting the calls to external systems with our own
  483. code which produces known results during the duration of the test.
  484. To achieve this behavior, Salt makes heavy use of the `MagicMock package`__.
  485. To understand how one might integrate Mock into writing a unit test for Salt,
  486. let's imagine a scenario in which we're testing an execution module that's
  487. designed to operate on a database. Furthermore, let's imagine two separate
  488. methods, here presented in pseduo-code in an imaginary execution module called
  489. 'db.py'.
  490. .. code-block:: python
  491. def create_user(username):
  492. qry = 'CREATE USER {0}'.format(username)
  493. execute_query(qry)
  494. def execute_query(qry):
  495. # Connect to a database and actually do the query...
  496. Here, let's imagine that we want to create a unit test for the `create_user`
  497. function. In doing so, we want to avoid any calls out to an external system and
  498. so while we are running our unit tests, we want to replace the actual
  499. interaction with a database with a function that can capture the parameters
  500. sent to it and return pre-defined values. Therefore, our task is clear -- to
  501. write a unit test which tests the functionality of `create_user` while also
  502. replacing 'execute_query' with a mocked function.
  503. To begin, we set up the skeleton of our class much like we did before, but with
  504. additional imports for MagicMock:
  505. .. code-block:: python
  506. # Import Salt Testing libs
  507. from tests.support.unit import TestCase
  508. # Import Salt execution module to test
  509. from salt.modules import db
  510. # Import Mock libraries
  511. from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch, call
  512. # Create test case class and inherit from Salt's customized TestCase
  513. # Skip this test case if we don't have access to mock!
  514. @skipIf(NO_MOCK, NO_MOCK_REASON)
  515. class DbTestCase(TestCase):
  516. def test_create_user(self):
  517. # First, we replace 'execute_query' with our own mock function
  518. with patch.object(db, 'execute_query', MagicMock()) as db_exq:
  519. # Now that the exits are blocked, we can run the function under test.
  520. db.create_user('testuser')
  521. # We could now query our mock object to see which calls were made
  522. # to it.
  523. ## print db_exq.mock_calls
  524. # Construct a call object that simulates the way we expected
  525. # execute_query to have been called.
  526. expected_call = call('CREATE USER testuser')
  527. # Compare the expected call with the list of actual calls. The
  528. # test will succeed or fail depending on the output of this
  529. # assertion.
  530. db_exq.assert_has_calls(expected_call)
  531. .. __: http://www.voidspace.org.uk/python/mock/index.html
  532. Modifying ``__salt__`` In Place
  533. ===============================
  534. At times, it becomes necessary to make modifications to a module's view of
  535. functions in its own ``__salt__`` dictionary. Luckily, this process is quite
  536. easy.
  537. Below is an example that uses MagicMock's ``patch`` functionality to insert a
  538. function into ``__salt__`` that's actually a MagicMock instance.
  539. .. code-block:: python
  540. def show_patch(self):
  541. with patch.dict(my_module.__salt__,
  542. {'function.to_replace': MagicMock()}):
  543. # From this scope, carry on with testing, with a modified __salt__!
  544. .. _simple-unit-example:
  545. A Simple Example
  546. ================
  547. Let's assume that we're testing a very basic function in an imaginary Salt
  548. execution module. Given a module called ``fib.py`` that has a function called
  549. ``calculate(num_of_results)``, which given a ``num_of_results``, produces a list of
  550. sequential Fibonacci numbers of that length.
  551. A unit test to test this function might be commonly placed in a file called
  552. ``tests/unit/modules/test_fib.py``. The convention is to place unit tests for
  553. Salt execution modules in ``test/unit/modules/`` and to name the tests module
  554. prefixed with ``test_*.py``.
  555. Tests are grouped around test cases, which are logically grouped sets of tests
  556. against a piece of functionality in the tested software. Test cases are created
  557. as Python classes in the unit test module. To return to our example, here's how
  558. we might write the skeleton for testing ``fib.py``:
  559. .. code-block:: python
  560. # Import Salt Testing libs
  561. from tests.support.unit import TestCase
  562. # Import Salt execution module to test
  563. import salt.modules.fib as fib
  564. # Create test case class and inherit from Salt's customized TestCase
  565. class FibTestCase(TestCase):
  566. '''
  567. This class contains a set of functions that test salt.modules.fib.
  568. '''
  569. def test_fib(self):
  570. '''
  571. To create a unit test, we should prefix the name with `test_' so
  572. that it's recognized by the test runner.
  573. '''
  574. fib_five = (0, 1, 1, 2, 3)
  575. self.assertEqual(fib.calculate(5), fib_five)
  576. At this point, the test can now be run, either individually or as a part of a
  577. full run of the test runner. To ease development, a single test can be
  578. executed:
  579. .. code-block:: bash
  580. tests/runtests.py -v -n unit.modules.test_fib
  581. This will report the status of the test: success, failure, or error. The
  582. ``-v`` flag increases output verbosity.
  583. .. code-block:: bash
  584. tests/runtests.py -n unit.modules.test_fib -v
  585. To review the results of a particular run, take a note of the log location
  586. given in the output for each test:
  587. .. code-block:: text
  588. Logging tests on /var/folders/nl/d809xbq577l3qrbj3ymtpbq80000gn/T/salt-runtests.log
  589. .. _complete-unit-example:
  590. A More Complete Example
  591. =======================
  592. Consider the following function from salt/modules/linux_sysctl.py.
  593. .. code-block:: python
  594. def get(name):
  595. '''
  596. Return a single sysctl parameter for this minion
  597. CLI Example:
  598. .. code-block:: bash
  599. salt '*' sysctl.get net.ipv4.ip_forward
  600. '''
  601. cmd = 'sysctl -n {0}'.format(name)
  602. out = __salt__['cmd.run'](cmd)
  603. return out
  604. This function is very simple, comprising only four source lines of code and
  605. having only one return statement, so we know only one test is needed. There
  606. are also two inputs to the function, the ``name`` function argument and the call
  607. to ``__salt__['cmd.run']()``, both of which need to be appropriately mocked.
  608. Mocking a function parameter is straightforward, whereas mocking a function
  609. call will require, in this case, the use of MagicMock. For added isolation, we
  610. will also redefine the ``__salt__`` dictionary such that it only contains
  611. ``'cmd.run'``.
  612. .. code-block:: python
  613. # Import Salt Libs
  614. import salt.modules.linux_sysictl as linux_sysctl
  615. # Import Salt Testing Libs
  616. from tests.support.mixins import LoaderModuleMockMixin
  617. from tests.support.unit import skipIf, TestCase
  618. from tests.support.mock import (
  619. MagicMock,
  620. patch,
  621. NO_MOCK,
  622. NO_MOCK_REASON
  623. )
  624. @skipIf(NO_MOCK, NO_MOCK_REASON)
  625. class LinuxSysctlTestCase(TestCase, LoaderModuleMockMixin):
  626. '''
  627. TestCase for salt.modules.linux_sysctl module
  628. '''
  629. def test_get(self):
  630. '''
  631. Tests the return of get function
  632. '''
  633. mock_cmd = MagicMock(return_value=1)
  634. with patch.dict(linux_sysctl.__salt__, {'cmd.run': mock_cmd}):
  635. self.assertEqual(linux_sysctl.get('net.ipv4.ip_forward'), 1)
  636. Since ``get()`` has only one raise or return statement and that statement is a
  637. success condition, the test function is simply named ``test_get()``. As
  638. described, the single function call parameter, ``name`` is mocked with
  639. ``net.ipv4.ip_forward`` and ``__salt__['cmd.run']`` is replaced by a MagicMock
  640. function object. We are only interested in the return value of
  641. ``__salt__['cmd.run']``, which MagicMock allows us by specifying via
  642. ``return_value=1``. Finally, the test itself tests for equality between the
  643. return value of ``get()`` and the expected return of ``1``. This assertion is
  644. expected to succeed because ``get()`` will determine its return value from
  645. ``__salt__['cmd.run']``, which we have mocked to return ``1``.
  646. .. _complex-unit-example:
  647. A Complex Example
  648. =================
  649. Now consider the ``assign()`` function from the same
  650. salt/modules/linux_sysctl.py source file.
  651. .. code-block:: python
  652. def assign(name, value):
  653. '''
  654. Assign a single sysctl parameter for this minion
  655. CLI Example:
  656. .. code-block:: bash
  657. salt '*' sysctl.assign net.ipv4.ip_forward 1
  658. '''
  659. value = str(value)
  660. sysctl_file = '/proc/sys/{0}'.format(name.replace('.', '/'))
  661. if not os.path.exists(sysctl_file):
  662. raise CommandExecutionError('sysctl {0} does not exist'.format(name))
  663. ret = {}
  664. cmd = 'sysctl -w {0}="{1}"'.format(name, value)
  665. data = __salt__['cmd.run_all'](cmd)
  666. out = data['stdout']
  667. err = data['stderr']
  668. # Example:
  669. # # sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
  670. # net.ipv4.tcp_rmem = 4096 87380 16777216
  671. regex = re.compile(r'^{0}\s+=\s+{1}$'.format(re.escape(name),
  672. re.escape(value)))
  673. if not regex.match(out) or 'Invalid argument' in str(err):
  674. if data['retcode'] != 0 and err:
  675. error = err
  676. else:
  677. error = out
  678. raise CommandExecutionError('sysctl -w failed: {0}'.format(error))
  679. new_name, new_value = out.split(' = ', 1)
  680. ret[new_name] = new_value
  681. return ret
  682. This function contains two raise statements and one return statement, so we
  683. know that we will need (at least) three tests. It has two function arguments
  684. and many references to non-builtin functions. In the tests below you will see
  685. that MagicMock's ``patch()`` method may be used as a context manager or as a
  686. decorator. When patching the salt dunders however, please use the context
  687. manager approach.
  688. There are three test functions, one for each raise and return statement in the
  689. source function. Each function is self-contained and contains all and only the
  690. mocks and data needed to test the raise or return statement it is concerned
  691. with.
  692. .. code-block:: python
  693. # Import Salt Libs
  694. import salt.modules.linux_sysctl as linux_sysctl
  695. from salt.exceptions import CommandExecutionError
  696. # Import Salt Testing Libs
  697. from tests.support.mixins import LoaderModuleMockMixin
  698. from tests.support.unit import skipIf, TestCase
  699. from tests.support.mock import (
  700. MagicMock,
  701. patch,
  702. NO_MOCK,
  703. NO_MOCK_REASON
  704. )
  705. @skipIf(NO_MOCK, NO_MOCK_REASON)
  706. class LinuxSysctlTestCase(TestCase, LoaderModuleMockMixin):
  707. '''
  708. TestCase for salt.modules.linux_sysctl module
  709. '''
  710. @patch('os.path.exists', MagicMock(return_value=False))
  711. def test_assign_proc_sys_failed(self):
  712. '''
  713. Tests if /proc/sys/<kernel-subsystem> exists or not
  714. '''
  715. cmd = {'pid': 1337, 'retcode': 0, 'stderr': '',
  716. 'stdout': 'net.ipv4.ip_forward = 1'}
  717. mock_cmd = MagicMock(return_value=cmd)
  718. with patch.dict(linux_sysctl.__salt__, {'cmd.run_all': mock_cmd}):
  719. self.assertRaises(CommandExecutionError,
  720. linux_sysctl.assign,
  721. 'net.ipv4.ip_forward', 1)
  722. @patch('os.path.exists', MagicMock(return_value=True))
  723. def test_assign_cmd_failed(self):
  724. '''
  725. Tests if the assignment was successful or not
  726. '''
  727. cmd = {'pid': 1337, 'retcode': 0, 'stderr':
  728. 'sysctl: setting key "net.ipv4.ip_forward": Invalid argument',
  729. 'stdout': 'net.ipv4.ip_forward = backward'}
  730. mock_cmd = MagicMock(return_value=cmd)
  731. with patch.dict(linux_sysctl.__salt__, {'cmd.run_all': mock_cmd}):
  732. self.assertRaises(CommandExecutionError,
  733. linux_sysctl.assign,
  734. 'net.ipv4.ip_forward', 'backward')
  735. @patch('os.path.exists', MagicMock(return_value=True))
  736. def test_assign_success(self):
  737. '''
  738. Tests the return of successful assign function
  739. '''
  740. cmd = {'pid': 1337, 'retcode': 0, 'stderr': '',
  741. 'stdout': 'net.ipv4.ip_forward = 1'}
  742. ret = {'net.ipv4.ip_forward': '1'}
  743. mock_cmd = MagicMock(return_value=cmd)
  744. with patch.dict(linux_sysctl.__salt__, {'cmd.run_all': mock_cmd}):
  745. self.assertEqual(linux_sysctl.assign(
  746. 'net.ipv4.ip_forward', 1), ret)