DEV Community

Cover image for Exploring Unit Testing in OpenStack Horizon
Abby Nduta
Abby Nduta

Posted on • Edited on

Exploring Unit Testing in OpenStack Horizon

Context: I was accepted into Outreachy a few weeks ago and I'm working on OpenStack Horizon.

I've been working with Cinder, which is Block Storage on OpenStack.

I've been spending my time learning how to write unit tests on the OpenStack Horizon project.

I hope to share my reflections and what I've learned about writing unit tests on OpenStack.

Why Start With Unit Tests?

Unit tests are a great way to understand how various parts of the code interact with each other, and it's a highly recommended approach by the OpenStack community.

The main idea around unit tests is testing the smallest "piece of code" possible, usually a function or method.

Writing Unit Tests on OpenStack Horizon

I would like to begin by sharing some pitfalls to avoid.

Polish up on your knowledge

I would suggest polishing up on your unit testing basics first before attempting to write any unit tests. I'll share resources later on.

A lot of Cinder tests use mocking and patching. Learn about that too.

Go through Existing Tests

You also need to check how the rest of the tests are written. This will give you a clue about what's expected.

Follow the Inheritance Trail

Cinder code is basically Django classes, which means lots of inheritance. Make sure to follow the inheritance trail, all the way to the parent class.

That said, an example is always great.

Prerequisites

  • You have a working version of Horizon either on your PC or virtual machine.
  • You have the latest code from master
  • You have created some volumes on the Horizon dashboard

A Unit Testing Example

I have horizon installed locally. I have created some volumes on my Horizon dashboard.

Image description

I need to write a test to determine whether the "AttachedTo" column on the Volumes table displays a dash [-], if the volume is not attached to an instance.

The first thing I need to do is find the code that generates the column on the volumes table.

You'll find it under

horizon/openstack_dashboard/dashboards/project/volumes/tables.py
Enter fullscreen mode Exit fullscreen mode

The specific class is AttachmentsColumn:

class AttachmentColumn(tables.WrappingColumn):
    """Customized column class.

    So it that does complex processing on the attachments
    for a volume instance.
    """

    instance_detail_url = "horizon:project:instances:detail"

    def get_raw_data(self, volume):
        request = self.table.request
        link = _('%(dev)s on %(instance)s')
        attachments = []
        # Filter out "empty" attachments which the client returns...
        for attachment in [att for att in volume.attachments if att]:
            # When a volume is attached it may return the server_id
            # without the server name...
            instance = get_attachment_name(request, attachment,
                                           self.instance_detail_url)
            vals = {"instance": instance,
                    "dev": html.escape(attachment.get("device", ""))}
            attachments.append(link % vals)
        if attachments:
            return safestring.mark_safe(", ".join(attachments))
Enter fullscreen mode Exit fullscreen mode

The test essentially tests this code:

if attachments:
            return safestring.mark_safe(", ".join(attachments))
Enter fullscreen mode Exit fullscreen mode

Collecting the ingredients we need for our test

  • Check your Python version (if older install mock from PyPI)
  • In newer versions, Python 3.3+ unitttest.mock is part of the library by default
  • Think about what you want to test for (testing for None, equality, existence, truthiness, falsiness)
  • In our case, we are testing for None, since the dash [-] translates to None
  • Think about the scope of what you want to test. Do you want to write a test for the entire class? Or just the method? I went with the latter.
  • That means that we need to create an instance of the AttachmentColumn class which inherits from tables.WrappingColumn.
  • Let's explore this class more.
  • The tables code is on this file path:
/home/nduta/horizon/horizon/tables
Enter fullscreen mode Exit fullscreen mode
  • Go to the imports in the init.py and find WrappingColumn
from horizon.tables.base import WrappingColumn
Enter fullscreen mode Exit fullscreen mode
  • The WrappingColumn is defined in base.py
class WrappingColumn(Column):
    """A column that wraps its contents. Useful for data like UUIDs or names"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.classes.append('word-break')
Enter fullscreen mode Exit fullscreen mode
  • It inherits from Column, which is also defined in base.py
  • In the init method of Column, we see that the only required argument is "transform"
    def __init__(self, transform, verbose_name=None, sortable=True,
                 link=None, allowed_data_types=None, hidden=False, attrs=None,
                 status=False, status_choices=None, display_choices=None,
                 empty_value=None, filters=None, classes=None, summation=None,
                 auto=None, truncate=None, link_classes=None, wrap_list=False,
                 form_field=None, form_field_attributes=None,
                 update_action=None, link_attrs=None, policy_rules=None,
                 cell_attributes_getter=None, help_text=None):
Enter fullscreen mode Exit fullscreen mode
  • The docstrings suggest that is can be a string or callable.
"""A class which represents a single column in a :class:`.DataTable`.

    .. attribute:: transform

        A string or callable. If ``transform`` is a string, it should be the
        name of the attribute on the underlying data class which
        should be displayed in this column. If it is a callable, it
        will be passed the current row's data at render-time and should
        return the contents of the cell. Required.
Enter fullscreen mode Exit fullscreen mode
  • We now have the attribute to use when creating an instance of AttachmentColumn.

Mocking

  • To imitate the functionality of the AttachmentColumn class, we need to create some mocks.Think about mocks as "mimics".
  • We could mimic a table which is where the column we intend to test lives, for example.
  • Mocks also come in handy because Horizon makes API calls to services like Cinder when to display volume information. We would need to "mimic" this API calls too.
  • To display a volume, for example, we would need to send a request to Cinder, asking for volume information.
  • We would also need to "mimic" a volume, in this case, with the attachments attribute being an empty list, since it has no attachments.

Writing the Test

  • We are going to use the aforementioned unittest.mock library which has a Mock() class to help us in "mimicking"
    def test_attachment_column(self):
        column = volume_tables.AttachmentColumn("attachments")
        column.table = mock.Mock()
        column.table.request = mock.Mock()
        volume = mock.Mock()
        volume.attachments = []
        result = column.get_raw_data(volume)
        self.assertIsNone(result, None)
Enter fullscreen mode Exit fullscreen mode
  • We defined a method called "test_attachment_column"
  • We then created an instance of AttachmentColumn. Since the class is contained in the tables module, we prefixed that. If you check the imports, the tables module is imported as volume_tables.
  • We then created a mock of our table, request, and volume, with the attachments attribute being an empty list.
  • In our code, we use mock.Mocks() because we did not explicitly import Mock.
from unittest import mock
Enter fullscreen mode Exit fullscreen mode
  • We then called our method, get_raw_data from the AttachmentColumn, passing in our volume as an argument.
  • Eventually, we created an assertion, assertIsNone, to confirm that our volume has no attachments, and that translates to 'None', our [-]
  • Note that we need to call the method we test (get_raw_data in our case) and use assert to compare the result with what we expect to get.

Checking the correctness of your test

  • We use tox within the OpenStack ecosystem to run tests. You can run this command in horizon's root directory:
tox -e py3.10 --  openstack_dashboard/dashboards/project/volumes/tests.py -r test_attachment_column
Enter fullscreen mode Exit fullscreen mode
  • If using a different python version, then your command should be
tox -e <python_version> --  openstack_dashboard/dashboards/project/volumes/tests.py -r test_attachment_column
Enter fullscreen mode Exit fullscreen mode
  • Your test should pass.
openstack_dashboard/dashboards/project/volumes/tests.py::VolumeIndexViewTests::test_attachment_column PASSED  
Enter fullscreen mode Exit fullscreen mode
  • You can also edit code in AttachmentColumn and rerun the test command to confirm that the code works.
#if attachments:
 return safestring.mark_safe(", ".join(attachments))
Enter fullscreen mode Exit fullscreen mode
  • The test should now fail.
openstack_dashboard/dashboards/project/volumes/tests.py::VolumeIndexViewTests::test_attachment_column FAILED
Enter fullscreen mode Exit fullscreen mode

A word on mocks

  • We could have used Mock() interchangeably with MagicMock(), but the latter is more powerful, with more "bells and whistles" which could break your tests, or cause some to pass/fail due to default MagicMock behaviour.

Forging Forward

There's still a lot to explore regarding unit tests within the OpenStack Horizon project. Some tests use pytest, for example, and others use the patch() decorator.

I hope that this blog would go a long way in helping you get started with unit testing in Horizon.

Resources

Top comments (0)