OpenStack Horizon Week 7: Exploring Unit Testing
Tags: openstack, outreachy, cloud, django
Published: 2024-07-10 06:54:30

Context: I was [accepted into Outreachy](https://dev.to/ndutared/my-outreachy-application-journey-28m1) 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](https://www.youtube.com/watch?v=1IsJHWFGxlQ) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/twwxqrg9w3wu7nl07mcx.png)

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 
```python
horizon/openstack_dashboard/dashboards/project/volumes/tables.py
```
The specific class is AttachmentsColumn:
```python
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))
```
The test essentially tests this code:
```python
if attachments:
            return safestring.mark_safe(", ".join(attachments))
```

### Collecting the ingredients we need for our test

- Check your Python version (if older install [mock from PyPI](https://pypi.org/project/mock/))
- 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: 

```python
/home/nduta/horizon/horizon/tables
```
- Go to the imports in the __init__.py and find WrappingColumn
```python
from horizon.tables.base import WrappingColumn
```
- The WrappingColumn is defined in base.py
```python
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')
```
- 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"
```python
    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):
```
- The docstrings suggest that is can be a string or callable.
```python
"""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.
```
- 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](https://docs.python.org/3/library/unittest.mock.html).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"
```python
    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)
```
- 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.
```python
from unittest import mock
```
- 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:
```python
tox -e py3.10 --  openstack_dashboard/dashboards/project/volumes/tests.py -r test_attachment_column
```
- If using a different python version, then your command should be 
```python
tox -e <python_version> --  openstack_dashboard/dashboards/project/volumes/tests.py -r test_attachment_column
```
- Your test should pass.
```python
openstack_dashboard/dashboards/project/volumes/tests.py::VolumeIndexViewTests::test_attachment_column PASSED  
```
- You can also edit code in AttachmentColumn and rerun the test command to confirm that the code works. 
```python
#if attachments:
 return safestring.mark_safe(", ".join(attachments))
```
- The test should now fail.
```python
openstack_dashboard/dashboards/project/volumes/tests.py::VolumeIndexViewTests::test_attachment_column FAILED
```

## 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()](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch) decorator. 

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

## Resources
- [Python's unittest: Writing Unit Tests for Your Code](https://realpython.com/python-unittest/)
- [unittest.mock — mock object library](https://docs.python.org/3/library/unittest.mock.html)
- [Understanding the Python Mock Object Library](https://realpython.com/python-mock-library/)
- [What I Learned at Work this Week: MagicMock](https://mike-diaz006.medium.com/what-i-learned-at-work-this-week-magicmock-61996506bc27)