Automation
Automation

Scripting How-To: Unit test a Juniper PyEZ project with pytest

by Juniper Employee on ‎12-17-2015 08:30 AM - edited on ‎08-23-2017 11:33 AM by Administrator Administrator (4,849 Views)

Unit Testing and Mocking a Juniper PyEZ Project

 

Unit Testing does not replace functional testing, however, it does help to improve code quality and reduce time required to access real environments by testing against mocked (faked) environments. Mocking is the replacement of one or more function calls or objects with mock calls or objects.

Unit testing help to prevent regression bugs. Testing with mocked environments enables you to test code paths that are difficult to reproduce with a real environment, such as exception handling.

This project includes a few examples and a small pyez utility called routing_neighbors.py with corresponding unit tests in the directory tests.

 

Click here for the GitHub project  repository

 

The examples are based on the python test framework pytest and travis as continuous integration (CI) service.

 

You execute the tests with the command:

 

python -m pytest -v --durations=10 --cov="routing_neighbors" 

as shown in the file .travis.yml.

 

pytest auto discovery checks for all files in the directory tests starting with tests_ and executes all included functions beginning with the same prefix. 

 

Mock PyEZ Device and RPC Reply

 

The mock object for your PyEZ device connection (pyez_mock/device.py) allow you to test your PyEZ application against an emulated environment.

 

This mock object uses the root element tag of the rpc-request to return the corresponding rpc-reply stored in the rpc_reply_dict or as a file in the directory rpc-reply.

 

For the following rpc-request:

 

<get-route-information>
<detail/>
</get-route-information>

The mocked PyEZ device attempts to retrieve the rpc-reply from the rpc_reply_dict using get-route-information as the key; otherwise, it attempts to retrieve the reply from the file tests/rpc-reply/get-route-information.xml relative to working directory. The parameters of the rpc-request will be ignored (e.g. <detail/>).

 

You must format the rpc-reply as a string with a valid XML structure and rpc-reply as root element:

 

from pyez_mock.device import rpc_reply_dict

rpc_reply_dict['get-alarm-information'] = """
<rpc-reply>
<alarm-information>
<alarm-summary>
<no-active-alarms/>
</alarm-summary>
</alarm-information>
</rpc-reply>"""

 

The rpc_reply_dict allows you to generate a rpc-reply dynamically during test execution or to return exceptions (see tests/test_exceptions.py).

 

Example

 

from pyez_mock.device import device

dev = device(host="1.2.3.4", user="juniper")
result = device.rpc.get_route_information(detail=True)
assert result.findtext(".//destination-count") == "5"
WIth Pytest

 

pytest_device is a pre-defined pytest fixture with module level scope and default credentials. You can create custom device fixtures as shown in tests/test_device.py.

 

from pyez_mock.device import pytest_device as device

def test_default(device):
result = device.rpc.get_route_information(detail=True)
assert result.findtext(".//destination-count") == "5"

 

Mock Code

 

pyez_mock/device.py

 

from __future__ import unicode_literals
from mock import MagicMock, patch
from jnpr.junos import Device
from jnpr.junos.facts.swver import version_info
from ncclient.manager import Manager, make_device_handler
from ncclient.transport import SSHSession
from ncclient.xml_ import NCElement
import pytest
import os


# dynamic generated rpc-replys
rpc_reply_dict = {}


# junos device facts
device_facts = {
'2RE': True,
'HOME': '/var/home/juniper',
'RE0': {'last_reboot_reason': 'Router rebooted after a normal shutdown.',
'mastership_state': 'master',
'model': 'RE-S-1800x4',
'status': 'OK',
'up_time': '19 days, 16 hours, 57 minutes, 47 seconds'},
'RE1': {'last_reboot_reason': 'Router rebooted after a normal shutdown.',
'mastership_state': 'backup',
'model': 'RE-S-1800x4',
'status': 'OK',
'up_time': '19 days, 16 hours, 58 minutes, 11 seconds'},
'domain': 'mocked.juniper.domain',
'fqdn': 'mx960.mocked.juniper.domain',
'hostname': 'mx960',
'ifd_style': 'CLASSIC',
'master': 'RE0',
'model': 'mx960',
'personality': 'MX',
'serialnumber': 'JN1337NA1337',
'switch_style': 'BRIDGE_DOMAIN',
'vc_capable': False,
'version': '13.3R8',
'version_RE0': '13.3R8',
'version_RE1': '13.3R8',
'version_info': version_info('13.3R8')
}


@patch('ncclient.manager.connect')
def device(mock_connect, *args, **kwargs):
"""Juniper PyEZ Device Mock"""

def get_facts():
dev._facts = device_facts

def mock_manager(*args, **kwargs):
if 'device_params' in kwargs:
# open connection
device_params = kwargs['device_params']
  device_handler = make_device_handler(device_params)
  session = SSHSession(device_handler)
return Manager(session, device_handler)
elif args:
# rpc request
rpc_request = args[0].tag
if rpc_request == "command":
# CLI commands
rpc_request = "%s_%s" % (rpc_request, args[0].text.replace(" ", "_"))
if rpc_request in rpc_reply_dict:
# result for rpc request is in dict
reply = rpc_reply_dict[rpc_request]
if isinstance(reply, Exception):
raise reply
else:
xml = reply
else:
# get rpc result from file
fname = os.path.join(os.getcwd(), 'tests', 'rpc-reply', rpc_request + '.xml')
with open(fname, 'r') as f:
xml = f.read()
rpc_reply = NCElement(xml, dev._conn._device_handler.transform_reply())
return rpc_reply

mock_connect.side_effect = mock_manager
dev = Device(*args, **kwargs)
dev.facts_refresh = get_facts
dev.open()
dev._conn.rpc = MagicMock(side_effect=mock_manager)
# replace open/close with mock objects
dev.open = MagicMock()
dev.close = MagicMock()
return dev


# ------------------------------------------------------------------------------
# pytest fixtures
# ------------------------------------------------------------------------------

@pytest.fixture(scope="module", params=[{"host": "1.2.3.4", "user": "juniper"}])
def pytest_device(request):
return device(**request.param)