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.