mirror of
https://github.com/valitydev/salt.git
synced 2024-11-07 17:09:03 +00:00
215 lines
7.7 KiB
ReStructuredText
215 lines
7.7 KiB
ReStructuredText
==================
|
|
Writing Unit Tests
|
|
==================
|
|
|
|
Introduction
|
|
============
|
|
|
|
Like many software projects, Salt has two broad-based testing approaches --
|
|
integration testing and unit testing. While integration testing focuses on the
|
|
interaction between components in a sandboxed environment, unit testing focuses
|
|
on the singular implementation of individual functions.
|
|
|
|
Preparing to Write a Unit Test
|
|
==============================
|
|
|
|
Unit tests live in: `tests/unit/`__.
|
|
|
|
.. __: https://github.com/saltstack/salt/tree/develop/tests/unit
|
|
|
|
Most commonly, the following imports are necessary to create a unit test:
|
|
|
|
.. code-block:: python
|
|
|
|
# Import Salt Testing libs
|
|
from salttesting import skipIf, TestCase
|
|
from salttesting.helpers import ensure_in_syspath
|
|
|
|
|
|
If you need mock support to your tests, please also import:
|
|
|
|
.. code-block:: python
|
|
|
|
from salttesting.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch, call
|
|
|
|
|
|
A Simple Example
|
|
================
|
|
|
|
Let's assume that we're testing a very basic function in an imaginary Salt
|
|
execution module. Given a module called ``fib.py`` that has a function called
|
|
``calculate(num_of_results)``, which given a ``num_of_results``, produces a list of
|
|
sequential Fibonacci numbers of that length.
|
|
|
|
A unit test to test this function might be commonly placed in a file called
|
|
``tests/unit/modules/fib_test.py``. The convention is to place unit tests for
|
|
Salt execution modules in ``test/unit/modules/`` and to name the tests module
|
|
suffixed with ``_test.py``.
|
|
|
|
Tests are grouped around test cases, which are logically grouped sets of tests
|
|
against a piece of functionality in the tested software. Test cases are created
|
|
as Python classes in the unit test module. To return to our example, here's how
|
|
we might write the skeleton for testing ``fib.py``:
|
|
|
|
.. code-block:: python
|
|
|
|
# Import Salt Testing libs
|
|
from salttesting import TestCase
|
|
|
|
# Import Salt execution module to test
|
|
from salt.modules import fib
|
|
|
|
# Create test case class and inherit from Salt's customized TestCase
|
|
class FibTestCase(TestCase):
|
|
|
|
'''
|
|
If we want to set up variables common to all unit tests, we can do so
|
|
by defining a setUp method, which will be run automatically before
|
|
tests begin.
|
|
'''
|
|
def setUp(self):
|
|
# Declare a simple set of five Fibonacci numbers starting at zero that we know are correct.
|
|
self.fib_five = [0, 1, 1, 2, 3]
|
|
|
|
def test_fib(self):
|
|
'''
|
|
To create a unit test, we should prefix the name with `test_' so that it's recognized by the test runner.
|
|
'''
|
|
self.assertEqual(fib.calculate(5), self.fib_five)
|
|
|
|
|
|
At this point, the test can now be run, either individually or as a part of a
|
|
full run of the test runner. To ease development, a single test can be
|
|
executed:
|
|
|
|
.. code-block:: bash
|
|
|
|
tests/runtests.py -n unit.modules.fib_test
|
|
|
|
This will produce output indicating the success or failure of the tests in
|
|
given test case. For more detailed results, one can also include a flag to
|
|
increase verbosity:
|
|
|
|
.. code-block:: bash
|
|
|
|
tests/runtests.py -n unit.modules.fib_test -v
|
|
|
|
To review the results of a particular run, take a note of the log location
|
|
given in the output for each test:
|
|
|
|
.. code-block:: text
|
|
|
|
Logging tests on /var/folders/nl/d809xbq577l3qrbj3ymtpbq80000gn/T/salt-runtests.log
|
|
|
|
Evaluating Truth
|
|
================
|
|
|
|
A longer discussion on the types of assertions one can make can be found by
|
|
reading `Python's documentation on unit testing`__.
|
|
|
|
.. __: http://docs.python.org/2/library/unittest.html#unittest.TestCase
|
|
|
|
Tests Using Mock Objects
|
|
========================
|
|
|
|
In many cases, the very purpose of a Salt module is to interact with some
|
|
external system, whether it be to control a database, manipulate files on a
|
|
filesystem or many other examples. In these varied cases, it's necessary to
|
|
design a unit test which can test the function whilst replacing functions which
|
|
might actually call out to external systems. One might think of this as
|
|
"blocking the exits" for code under tests and redirecting the calls to external
|
|
systems with our own code which produces known results during the duration of
|
|
the test.
|
|
|
|
To achieve this behavior, Salt makes heavy use of the `MagicMock package`__.
|
|
|
|
To understand how one might integrate Mock into writing a unit test for Salt,
|
|
let's imagine a scenario in which we're testing an execution module that's
|
|
designed to operate on a database. Furthermore, let's imagine two separate
|
|
methods, here presented in pseduo-code in an imaginary execution module called
|
|
'db.py.
|
|
|
|
.. code-block:: python
|
|
|
|
def create_user(username):
|
|
qry = 'CREATE USER {0}'.format(username)
|
|
execute_query(qry)
|
|
|
|
def execute_query(qry):
|
|
# Connect to a database and actually do the query...
|
|
|
|
Here, let's imagine that we want to create a unit test for the `create_user`
|
|
function. In doing so, we want to avoid any calls out to an external system and
|
|
so while we are running our unit tests, we want to replace the actual
|
|
interaction with a database with a function that can capture the parameters
|
|
sent to it and return pre-defined values. Therefore, our task is clear -- to
|
|
write a unit test which tests the functionality of `create_user` while also
|
|
replacing 'execute_query' with a mocked function.
|
|
|
|
To begin, we set up the skeleton of our class much like we did before, but with
|
|
additional imports for MagicMock:
|
|
|
|
.. code-block:: python
|
|
|
|
# Import Salt Testing libs
|
|
from salttesting import TestCase
|
|
|
|
# Import Salt execution module to test
|
|
from salt.modules import db
|
|
|
|
# NEW! -- Import Mock libraries
|
|
from salttesting.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch, call
|
|
|
|
# Create test case class and inherit from Salt's customized TestCase
|
|
|
|
@skipIf(NO_MOCK, NO_MOCK_REASON) # Skip this test case if we don't have access to mock!
|
|
class DbTestCase(TestCase):
|
|
def test_create_user(self):
|
|
# First, we replace 'execute_query' with our own mock function
|
|
db.execute_query = MagicMock()
|
|
|
|
# Now that the exits are blocked, we can run the function under test.
|
|
|
|
db.create_user('testuser')
|
|
|
|
# We could now query our mock object to see which calls were made to it.
|
|
## print db.execute_query.mock_calls
|
|
|
|
'''
|
|
We want to test to ensure that the correct query was formed.
|
|
This is a contrived example, just designed to illustrate the concepts at hand.
|
|
|
|
We're going to first contruct a call() object that represents the way we expect
|
|
our mocked execute_query() function to have been called.
|
|
Then, we'll examine the list of calls that were actually made to to execute_function().
|
|
|
|
By comparing our expected call to execute_query() with create_user()'s call to
|
|
execute_query(), we can determine the success or failure of our unit test.
|
|
'''
|
|
|
|
expected_call = call('CREATE USER testuser')
|
|
|
|
# Do the comparison! Will assert False if execute_query() was not called with the given call
|
|
|
|
db.execute_query.assert_has_calls(expected_call)
|
|
|
|
|
|
.. __: http://www.voidspace.org.uk/python/mock/index.html
|
|
|
|
|
|
Modifying ``__salt__`` In Place
|
|
===============================
|
|
|
|
At times, it becomes necessary to make modifications to a module's view of
|
|
functions in its own ``__salt__`` dictionary. Luckily, this process is quite
|
|
easy.
|
|
|
|
Below is an example that uses MagicMock's ``patch`` functionality to insert a
|
|
function into ``__salt__`` that's actually a MagicMock instance.
|
|
|
|
.. code-block:: python
|
|
|
|
def show_patch(self):
|
|
with patch.dict(my_module.__salt__, {'function.to_replace': MagicMock()}:
|
|
# From this scope, carry on with testing, with a modified __salt__!
|