How to write vmware pyvmomi mocked unit tests
I have recently been doing some vmware api development work using pyvmomi and one of the issues I had is how we can unit test without a vSphere Installation. The solution is to mock the pyvmomi api but it turned out that it was a much more complex task than I initially thought, so here are my findings.
I will not go into depth of the pyvmomi api or the python mock object library as I assume anyone who will need to mock it would have the sufficient knowledge of both.
The pyvmomi wrapper class I have developed is VmwareUtil and I am using the context manager VmwareOpen to manage the lifecycle of the pyVim.connect.SmartConnectNoSSL which provides a vim.ServiceInstance. This is the root object of the vCenter/ESXi inventory.
from pyVim.connect import SmartConnectNoSSL, Disconnect
from pyVmomi import vim, vmodl
class VmwareOpen(object):
def __init__(self, host, port, username, password):
self._host = host
self._port = port
self._username = username
self._password = password
def __enter__(self):
try:
self._service_instance = SmartConnectNoSSL(host=self._host, user=self._username, pwd=self._password, port=self._port)
return self._service_instance
except IOError as e:
raise e
def __exit__(self, type, value, traceback):
Disconnect(self._service_instance)
class VmwareUtil(object):
def __init__(self, vmwarehost, vmwareport, username, password):
self._host = vmwarehost
self._port = vmwareport
self._username = username
self._password = password
def _get_vm(self, identity, service_instance):
content = service_instance.RetrieveContent()
vmlist = []
for child in content.rootFolder.childEntity:
if hasattr(child, 'vmFolder'):
datacenter = child
vmfolder = datacenter.vmFolder
vmlist = vmfolder.childEntity
for vm in vmlist:
if vm.summary.config.name == identity or vm.summary.config.uuid == identity:
return vm
raise Exception(identity + " not found.")
def get_uuid(self, identity):
with VmwareOpen(self._host, self._port, self._username, self._password) as service_instance:
vm = self._get_vm(identity, service_instance)
return vm.summary.config.uuid
def tryme():
vmware_util = VmwareUtil("192.168.122.168", "443", "root", "password")
vmuuid = vmware_util.get_uuid("test_vm_00")
print("UUID : " + vmuuid)
if __name__ == "__main__":
tryme()
As the simplest example I will just implement a method which will return the UUID of a Virtual Machine by passing its name.
In order to do this we first need to access the Service Instance Content vim.ServiceInstanceContent using the RetrieveContent method of the vim.ServiceInstance.
This will give us access to a set of Managed Objects in the inventory. The rootFolder is the root of the inventory and we need to look for the datacenter object vim.Datacenter. This would be the child which has the attribute vmFolder. There should be a more efficient way of doing this such as using the instanceof function but I kept the lookup code as it was in the pyvmomi community samples. The last step is to loop the children of the vmFolder and find the vm we look for based on name or uuid.
Now the hard stuff and the mocking bit.
The main issue I had is that all the pyvmomi objects are server managed so we cannot just instantiate and populate them at client side. This is why we need to mock the whole lot.
import unittest
from unittest import mock
from unittest.mock import patch
from vmware_util import VmwareUtil
from pyVim.connect import SmartConnectNoSSL, Disconnect
from pyVmomi import vim, vmodl
class TestVMwareUtil(unittest.TestCase):
@mock.patch('vmware_util.VmwareOpen')
def test_uuid(self, vmware_mock):
# Initialize the Config Summary
test_vm_00_config_summary = mock.MagicMock(spec_set=vim.vm.Summary.ConfigSummary())
test_vm_00_config_summary.name = "test_vm_00"
test_vm_00_config_summary.uuid = "c7a5fdbd-cdaf-9455-926a-d65c16db1809"
# Initialize VM Summary
test_vm_00_summary = mock.MagicMock(spec_set=vim.vm.Summary())
test_vm_00_summary.config = test_vm_00_config_summary
# Initialize the Mock VM
test_vm_00 = mock.MagicMock(spec_set=vim.VirtualMachine("vm-41"))
test_vm_00.summary = test_vm_00_summary
# Initialize the Data Center and add the Mock VM
# Create the vmFolder
vm_folder_mock = mock.MagicMock(spec_set=vim.Folder("vm-folder"))
vm_folder_mock.childType = { "vim.Folder", "vim.Virtualmachine", "vim.VirtualApp" }
# Add it to the Data Center and add the VM in the children
ds_mock = mock.MagicMock(spec_set=vim.Datacenter("ds-00"))
ds_mock.vmFolder = vm_folder_mock
ds_mock.vmFolder.childEntity = [test_vm_00]
# Initialize the Service Instance Mock and add the Data Center
# Create the rootFolder
root_folder_mock = mock.MagicMock(spec_set=vim.Folder("root-folder"))
root_folder_mock.childEntity = [ds_mock]
# Create the Service Instance and add the rootFolder
si_mock = mock.MagicMock(spec_set=vim.ServiceInstance("si-00"))
si_content_mock = mock.MagicMock(spec_set=vim.ServiceInstanceContent())
si_content_mock.rootFolder = root_folder_mock
# Mock the RetrieveContent Methos in the service instance
si_mock.RetrieveContent = mock.MagicMock(return_value=si_content_mock)
#si_mock.RetrieveContent.side_effect = Exception('Boom!')
# Get the VmwareOpen reference
vm_mock = vmware_mock.return_value
# Mock the enter and exit methods
vm_mock.__enter__ = mock.MagicMock(return_value=si_mock)
vm_mock.__exit__ = mock.MagicMock(return_value=None)
# Call the VMWare Util as normal. Any vmware_util.VmwareOpen reference within the function call will be replaced by the mocked object.
vmware_util = VmwareUtil("", "", "", "")
returned_uuid = vmware_util.get_uuid("test_vm_00")
# Assert
self.assertEqual('c7a5fdbd-cdaf-9455-926a-d65c16db1809', returned_uuid)
if __name__ == '__main__':
unittest.main()
The hierarchy inside the ServiceInstanceContent is the following so we need to mock in reverve order.
.
+-- ServiceInstanceContent
+-- rootFolder
+-- dataCenter
+-- vmFolder
+-- childEntity
+-- VirtualMachine
The next step is to mock the RetrieveContent method and assign the mocked ServiceInstanceContent as its returned object and the last step is to mock the __enter__ method of the vmware_util.VmwareOpen class which is the context manager for the ServiceInstance. We use the mock.patch function decorator @mock.patch('vmware_util.VmwareOpen') in the test_uuid() function so every time we ran the unit test, any reference to the vmware_util.VmwareOpen will be replaced with the patched object so there will be no real call to the VMWare Hypervisor but the mocked object will be returned instead. The vmware_util.VmwareOpen reference is at with VmwareOpen(self._host, self._port, self._username, self._password) as service_instance:.
You can find the code listings in my Github
That’s it, I hope you find this useful and if you have a better way of doing it please leave a comment below.
Thank you,
Petros