|
| 1 | +Unit tests: Mock driver |
| 2 | +======================= |
| 3 | + |
| 4 | +A mock driver is a software that imitates the response pattern of another |
| 5 | +system. It is meant to do nothing but returns the same predictable result, |
| 6 | +usually of the cases in a testing environment. |
| 7 | + |
| 8 | +A driver `mock` can mock all actions done by a common napalm driver. It can be |
| 9 | +used for unit tests, either to test napalm itself or inside external projects |
| 10 | +making use of napalm. |
| 11 | + |
| 12 | + |
| 13 | +Overview |
| 14 | +-------- |
| 15 | + |
| 16 | +For any action, the ``mock`` driver will use a file matching a specific pattern |
| 17 | +to return its content as a result. |
| 18 | + |
| 19 | +Each of these files will be located inside a directory specified at the driver |
| 20 | +initialization. Their names depend on the entire call name made to the |
| 21 | +driver, and about their order in the call stack. |
| 22 | + |
| 23 | + |
| 24 | +Replacing a standard driver by a ``mock`` |
| 25 | +----------------------------------------- |
| 26 | + |
| 27 | +Get the driver in napalm:: |
| 28 | + |
| 29 | + >>> import napalm |
| 30 | + >>> driver = napalm.get_network_driver('mock') |
| 31 | + |
| 32 | +And instantiate it with any host and credentials:: |
| 33 | + |
| 34 | + device = driver( |
| 35 | + hostname='foo', username='user', password='pass', |
| 36 | + optional_args={'path': path_to_results} |
| 37 | + ) |
| 38 | + |
| 39 | +Like other drivers, ``mock`` takes optional arguments: |
| 40 | + |
| 41 | +- ``path`` - Required. Directory where results files are located |
| 42 | + |
| 43 | +Open the driver:: |
| 44 | + |
| 45 | + >>> device.open() |
| 46 | + |
| 47 | +A user should now be able to call any function of a standard driver:: |
| 48 | + |
| 49 | + >>> device.get_network_instances() |
| 50 | + |
| 51 | +But should get an error because no mocked data is yet written:: |
| 52 | + |
| 53 | + NotImplementedError: You can provide mocked data in get_network_instances.1 |
| 54 | + |
| 55 | + |
| 56 | +Mocked data |
| 57 | +----------- |
| 58 | + |
| 59 | +We will use ``/tmp/mock`` as an example of a directory that will contain |
| 60 | +our mocked data. Define a device using this path:: |
| 61 | + |
| 62 | + >>> with driver('foo', 'user', 'pass', optional_args={'path': '/tmp/mock'}) as device: |
| 63 | + |
| 64 | +Mock a single call |
| 65 | +~~~~~~~~~~~~~~~~~~ |
| 66 | + |
| 67 | +In order to be able to call, for example, ``device.get_interfaces()``, a mocked |
| 68 | +data is needed. |
| 69 | + |
| 70 | +To build the file name that the driver will look for, take the function name |
| 71 | +(``get_interfaces``) and suffix it with the place of this call in the device |
| 72 | +call stack. |
| 73 | + |
| 74 | +.. note:: |
| 75 | + ``device.open()`` counts as a command. Each following order of call will |
| 76 | + start at 1. |
| 77 | + |
| 78 | +Here, ``get_interfaces`` is the first call made to ``device`` after ``open()``, |
| 79 | +so the mocked data need to be put in ``/tmp/mock/get_interfaces.1``:: |
| 80 | + |
| 81 | + |
| 82 | + { |
| 83 | + "Ethernet1/1": { |
| 84 | + "is_up": true, "is_enabled": true, "description": "", |
| 85 | + "last_flapped": 1478175306.5162635, "speed": 10000, |
| 86 | + "mac_address": "FF:FF:FF:FF:FF:FF" |
| 87 | + }, |
| 88 | + "Ethernet1/2": { |
| 89 | + "is_up": true, "is_enabled": true, "description": "", |
| 90 | + "last_flapped": 1492172106.5163276, "speed": 10000, |
| 91 | + "mac_address": "FF:FF:FF:FF:FF:FF" |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | +The content is the wanted result of ``get_interfaces`` in JSON, exactly as |
| 96 | +another driver would return it. |
| 97 | + |
| 98 | +Mock multiple iterative calls |
| 99 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 100 | + |
| 101 | +If ``/tmp/mock/get_interfaces.1`` was defined and used, for any other call on |
| 102 | +the same device, the number of calls needs to be incremented. |
| 103 | + |
| 104 | +For example, to call ``device.get_interfaces_ip()`` after |
| 105 | +``device.get_interfaces()``, the file ``/tmp/mock/get_interfaces_ip.2`` needs |
| 106 | +to be defined:: |
| 107 | + |
| 108 | + { |
| 109 | + "Ethernet1/1": { |
| 110 | + "ipv6": {"2001:DB8::": {"prefix_length": 64}} |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | +Mock a CLI call |
| 115 | +~~~~~~~~~~~~~~~ |
| 116 | + |
| 117 | +``device.cli(commands)`` calls are a bit different to mock, as a suffix |
| 118 | +corresponding to the command applied to the device needs to be added. As |
| 119 | +before, the data mocked file will start by ``cli`` and the number of calls done |
| 120 | +before (here, ``cli.1``). Then, the same process needs to be applied to each |
| 121 | +command. |
| 122 | + |
| 123 | +Each command needs to be sanitized: any special character (`` -,./\``, etc.) |
| 124 | +needs to be replaced by ``_``. Add the index of this command as it is sent to |
| 125 | +``device.cli()``. Each file then will contain the raw wanted output of its |
| 126 | +associated command. |
| 127 | + |
| 128 | +Example |
| 129 | +^^^^^^^ |
| 130 | + |
| 131 | +Example with 2 commands, ``show interface Ethernet 1/1`` and ``show interface |
| 132 | +Ethernet 1/2``. |
| 133 | + |
| 134 | +To define the mocked data, create a file ``/tmp/mock/cli.1.show_interface_Ethernet_1_1.0``:: |
| 135 | + |
| 136 | + Ethernet1/1 is up |
| 137 | + admin state is up, Dedicated Interface |
| 138 | + |
| 139 | +And a file ``/tmp/mock/cli.1.show_interface_Ethernet_1_2.1``:: |
| 140 | + |
| 141 | + Ethernet1/2 is up |
| 142 | + admin state is up, Dedicated Interface |
| 143 | + |
| 144 | +And now they can be called:: |
| 145 | + |
| 146 | + >>> device.cli(["show interface Ethernet 1/1", "show interface Ethernet 1/2"]) |
| 147 | + |
| 148 | + |
| 149 | +Mock an error |
| 150 | +~~~~~~~~~~~~~ |
| 151 | + |
| 152 | +The `mock` driver can raise an exception during a call, to simulate an error. |
| 153 | +An error definition is actually a json composed of 3 keys: |
| 154 | + |
| 155 | + * `exception`: the exception type that will be raised |
| 156 | + * `args` and `kwargs`: parameters sent to the exception constructor |
| 157 | + |
| 158 | +For example, to raise the exception `ConnectionClosedException` when calling |
| 159 | +``device.get_interfaces()``, the file ``/tmp/mock/get_interfaces.1`` needs to |
| 160 | +be defined:: |
| 161 | + |
| 162 | + { |
| 163 | + "exception": "napalm.base.exceptions.ConnectionClosedException", |
| 164 | + "args": [ |
| 165 | + "Connection closed." |
| 166 | + ], |
| 167 | + "kwargs": {} |
| 168 | + } |
| 169 | + |
| 170 | +Now calling `get_interfaces()` for the 1st time will raise an exception:: |
| 171 | + |
| 172 | + >>> device.get_interfaces() |
| 173 | + ConnectionClosedException: Connection closed |
| 174 | + |
| 175 | +As before, mock will depend on the number of calls. If a second file |
| 176 | +``/tmp/mock/get_interfaces.2`` was defined and filled with some expected data |
| 177 | +(not an exception), retrying `get_interfaces()` will run correctly if the first |
| 178 | +exception was caught. |
0 commit comments