Pytest in the Networking World

Like many of you, I come from a purely networking background. When I first started my network automation journey, I felt I was making great strides. However, I was, unknowingly, missing a very important component in my solutions—testing. I don’t mean testing what my code did in the network; I mean testing my code to ensure it acts as expected. That’s where pytest comes in. There are numerous examples of pytest in action on the internet but none showing those examples with networking data. As a somewhat visual learner myself, seeing an example of code with arbitrary data (e.g., Foo, Bar, i, j) versus seeing it with networking data (e.g., GigabiteEthernet1/1, Vlan100, etc.) makes a world of a difference to me. In this blog post I will give a quick overview on what pytest is, some of its important compenents, and show an example of how it can be used to test a function that normalizes MAC addresses.

What Is Pytest?

Pytest is a framework that helps you write unit and integration tests for your code. At its core, it’s pretty simple. However, that simplicity allows the pytest framework to be scaled very easily to accurately test large projects a well as lend itself to a test-driven development (TDD) approach to writing code. I’ll take this approach when I explain examples down below. Pytest has some cool features that you need to know about to understand the examples.

  • Any files following the conventions test_*.py or *_test.py are automatically discovered by pytest.
  • Any functions with test prefixed are automatically discovered.
  • Fixtures can be used to pass generated arguments to your tests.

You can read more in-depth about pytest here.

Pytest Example

Let’s say we are gathering MAC address info from our campus switches. After looking through what we’ve gathered, we notice we’re getting MAC addresses in two different formats:

  • aa:bb:cc:dd:ee:ff
  • aabb.ccdd.eeff

We need to normalize these to a single format. We decide that we want all MACs to be in the format aa:bb:cc:dd:ee:ff. Taking a TDD approach, we want to write a test before we actually develop any code. Our goal is to take a non-normalized MAC address, format it, and assert that the newly formated MAC address conforms to our defined standard. With that in mind, let’s create a file called test_mac_address.py and create the test:

>>> import pytest
>>>
>>>
>>> def test_normalize_mac_address(non_normalized_mac_address, expected_mac_address):
>>>     normalized_mac_address = normalize_mac_address(non_normalied_mac_address)
>>>     assert normalized_mac_address == expected_mac_address

Let’s break this down. The first line is defining our function, test_normalize_mac_address, which is expecting 2 arguments. Where are these arguments coming from? The arguments are coming from what pytest calls fixtures. These will be discussed in the next section. Our next line calls the function normalize_mac_address (which we have yet to define) passing in our non-normalized MAC address. Finally, the last line asserts that the MAC returned from our function is what we expect. Now that we’ve gone over what each line is doing, let’s quickly discuss fixtures.

Adding Fixtures

Fixtures are functions that are run before tests and are used to pass arguments into test functions. They are defined by the decorator @pytest.fixture. I mentioned before that we had two arguments we needed to pass into our test, so let’s make those two fixtures. In the same file where added our test function, let’s add these two fixtures:

>>> @pytest.fixture
>>> def non_normalized_mac_address():
>>>     return "aabb.ccdd.eeff"
>>> @pytest.fixture
>>> def expected_mac_address():
>>>     return "aa:bb:cc:dd:ee:ff"

You’ll notice the names of these fixtures match the names of the arguments in our test function. This must be done this way. When we run our test function, the two arguments are noticed and pytest looks for fixtures that have the same name. The fixture functions are then run and the returned value is used as the argument into the original test function.

Adding Our normalize_mac_address Function

Lastly, we need to add our normalize_mac_address function that we defined on the second line of our test function. Let’s create a file in the same directory we created our test file and name it mac_address.py. In this file let’s put our function:

def normalize_mac_address(non_normalized_mac_address):
    return non_normalized_mac_address

For now, lets return the non-normalized MAC address to see what results pytest displays. We need to do one last thing before we run our pytest, and that is telling our test_mac_address.py file where to find the normalize_mac_address function. We can do this by adding the line:

from mac_address import normalize_mac_address

Our test_mac_address.py file, in its complete form, should look like this.

>>> import pytest
>>> from mac_address import normalize_mac_address
>>>
>>> def test_normalize_mac_address(non_normalized_mac_address, expected_mac_address):
>>>     normalized_mac_address = normalize_mac_address(non_normalied_mac_address)
>>>     assert normalized_mac_address == expected_mac_address
>>>
>>> @pytest.fixture
>>> def non_normalized_mac_address():
>>>     return "aabb.ccdd.eeff"
>>>
>>> @pytest.fixture
>>> def expected_mac_address():
>>>     return "aa:bb:cc:dd:ee:ff"

Running pytest

Now, let’s run our test! In the same directory as our two files, let’s run pytest -v.

===================================================== test session starts =========================================
platform linux -- Python 3.8.2, pytest-6.1.2, py-1.9.0, pluggy-0.13.1 -- /home/adam/.virtualenvs/pytest/bin/python
cachedir: .pytest_cache
rootdir: /home/adam/repo/Sandbox/blog_pytest
collected 1 item

test_mac_address.py::test_normalize_mac_address FAILED                                                        [100%]

========================================================= FAILURES =================================================

non_normalized_mac_address = 'aabb.ccdd.eeff', expected_mac_address = 'aa:bb:cc:dd:ee:ff'

    def test_normalize_mac_address(non_normalized_mac_address, expected_mac_address):
        normalized_mac_address = normalize_mac_address(mac_address)
>       assert normalized_mac_address == expected_mac_address
E       AssertionError: assert 'aabb.ccdd.eeff' == 'aa:bb:cc:dd:ee:ff'
E         - aa:bb:cc:dd:ee:ff
E         ?   -  ^  -  ^  -
E         + aabb.ccdd.eeff
E         ?     ^    ^

test_mac_address.py:6: AssertionError
================================================= short test summary info ============================================
FAILED test_mac_address.py::test_normalize_mac_address - AssertionError: assert 'aabb.ccdd.eeff' == 'aa:bb:cc:dd:ee:ff'

The last line gives a clear picture of why the assertion failed. This is what we expected as we didn’t do anything to the MAC address in the normalize_mac_address function. Let’s update that function and run the test again. Here is the updated function:

>>> def normalize_mac_address(mac):
>>>     if mac.count(".") == 2:
>>>         mac = f"{mac[0:2]}:{mac[2:4]}:{mac[5:7]}:{mac[7:9]}:{mac[10:12]}:{mac[12:14]}"
>>>     return mac

Now we get this output when we run pytest.

======================================== test session starts =====================================================
platform linux -- Python 3.8.2, pytest-6.1.2, py-1.9.0, pluggy-0.13.1 -- /home/adam/.virtualenvs/pytest/bin/python
cachedir: .pytest_cache
rootdir: /home/adam/repo/Sandbox/blog_pytest
collected 1 item

test_mac_address.py::test_normalize_mac_address  PASSED                                                     [100%]

======================================== 1 passed in 0.01s =======================================================

Our test has passed!


Conclusion

Unit testing, and testing in general, is a very important part of the network automation journey. I hope this blog post has shed some light on what pytest is and the basic functionality of it. As the example is, there are only two types of MAC addresses. What would happen if we passed in a MAC address in the format aabbcc-ddeeff to our function? The test would fail.

In my next blog post I’ll dive into expanding on this example to use parameterization and other features of pytest. Thanks for reading!

-Adam



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Author