Unit tests: Mock driver ======================= A mock driver is a software that imitates the response pattern of another system. It is meant to do nothing but returns the same predictable result, usually of the cases in a testing environment. A driver `mock` can mock all actions done by a common napalm driver. It can be used for unit tests, either to test napalm itself or inside external projects making use of napalm. Overview -------- For any action, the ``mock`` driver will use a file matching a specific pattern to return its content as a result. Each of these files will be located inside a directory specified at the driver initialization. Their names depend on the entire call name made to the driver, and about their order in the call stack. Replacing a standard driver by a ``mock`` ----------------------------------------- Get the driver in napalm:: >>> import napalm >>> driver = napalm.get_network_driver('mock') And instantiate it with any host and credentials:: device = driver( hostname='foo', username='user', password='pass', optional_args={'path': path_to_results} ) Like other drivers, ``mock`` takes optional arguments: - ``path`` - Optional directory where results files are located (defaults to the current directory). Open the driver:: >>> device.open() A user should now be able to call any function of a standard driver:: >>> device.get_network_instances() But should get an error because no mocked data is yet written:: NotImplementedError: You can provide mocked data in get_network_instances.1 Mocked data ----------- We will use ``/tmp/mock`` as an example of a directory that will contain our mocked data. Define a device using this path:: >>> with driver('foo', 'user', 'pass', optional_args={'path': '/tmp/mock'}) as device: Mock a single call ~~~~~~~~~~~~~~~~~~ In order to be able to call, for example, ``device.get_interfaces()``, a mocked data is needed. To build the file name that the driver will look for, take the function name (``get_interfaces``) and suffix it with the place of this call in the device call stack. .. note:: ``device.open()`` counts as a command. Each following order of call will start at 1. Here, ``get_interfaces`` is the first call made to ``device`` after ``open()``, so the mocked data need to be put in ``/tmp/mock/get_interfaces.1``:: { "Ethernet1/1": { "is_up": true, "is_enabled": true, "description": "", "last_flapped": 1478175306.5162635, "speed": 10000, "mac_address": "FF:FF:FF:FF:FF:FF" }, "Ethernet1/2": { "is_up": true, "is_enabled": true, "description": "", "last_flapped": 1492172106.5163276, "speed": 10000, "mac_address": "FF:FF:FF:FF:FF:FF" } } The content is the wanted result of ``get_interfaces`` in JSON, exactly as another driver would return it. Mock multiple iterative calls ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If ``/tmp/mock/get_interfaces.1`` was defined and used, for any other call on the same device, the number of calls needs to be incremented. For example, to call ``device.get_interfaces_ip()`` after ``device.get_interfaces()``, the file ``/tmp/mock/get_interfaces_ip.2`` needs to be defined:: { "Ethernet1/1": { "ipv6": {"2001:DB8::": {"prefix_length": 64}} } } Mock a CLI call ~~~~~~~~~~~~~~~ ``device.cli(commands)`` calls are a bit different to mock, as a suffix corresponding to the command applied to the device needs to be added. As before, the data mocked file will start by ``cli`` and the number of calls done before (here, ``cli.1``). Then, the same process needs to be applied to each command. Each command needs to be sanitized: any special character (`` -,./\``, etc.) needs to be replaced by ``_``. Add the index of this command as it is sent to ``device.cli()``. Each file then will contain the raw wanted output of its associated command. Example ^^^^^^^ Example with 2 commands, ``show interface Ethernet 1/1`` and ``show interface Ethernet 1/2``. To define the mocked data, create a file ``/tmp/mock/cli.1.show_interface_Ethernet_1_1.0``:: Ethernet1/1 is up admin state is up, Dedicated Interface And a file ``/tmp/mock/cli.1.show_interface_Ethernet_1_2.1``:: Ethernet1/2 is up admin state is up, Dedicated Interface And now they can be called:: >>> device.cli(["show interface Ethernet 1/1", "show interface Ethernet 1/2"]) Mock an error ~~~~~~~~~~~~~ The `mock` driver can raise an exception during a call, to simulate an error. An error definition is actually a json composed of 3 keys: * `exception`: the exception type that will be raised * `args` and `kwargs`: parameters sent to the exception constructor For example, to raise the exception `ConnectionClosedException` when calling ``device.get_interfaces()``, the file ``/tmp/mock/get_interfaces.1`` needs to be defined:: { "exception": "napalm.base.exceptions.ConnectionClosedException", "args": [ "Connection closed." ], "kwargs": {} } Now calling `get_interfaces()` for the 1st time will raise an exception:: >>> device.get_interfaces() ConnectionClosedException: Connection closed As before, mock will depend on the number of calls. If a second file ``/tmp/mock/get_interfaces.2`` was defined and filled with some expected data (not an exception), retrying `get_interfaces()` will run correctly if the first exception was caught.