Application Deployment and Security · Topic 4 of 8

Writing a Python Unit Test

Write a Python unit test with the unittest module, using TestCase, assert methods, setUp and mock to fake API calls, and run it from the command line.

  • 4 min
In a nutshell
  • A test class inherits from unittest.TestCase.
  • Each method that starts with test is run as a test.
  • assertEqual, assertTrue, assertFalse and assertRaises are common checks.

What it is

A unit test is a small piece of code that calls one function with known input and checks that the result is what you expect.

Python includes the unittest module for this. You write a class that inherits from TestCase, and each method whose name starts with test is one test.

Unit tests run in a CI pipeline on every change, so a broken function is found before it is released.

Test methodcalls the functionwith known inputFunctioncode under testreturnsAssertioncompares with expectedresultPass or failreported

How it works

You arrange the input, run the code and assert the outcome.

The code under test

# nettools.py
def mask_to_prefix(mask):
    return sum(bin(int(octet)).count("1") for octet in mask.split("."))


def is_valid_vlan(vlan_id):
    return 1 <= vlan_id <= 4094

The test file

# test_nettools.py
import unittest

from nettools import mask_to_prefix, is_valid_vlan


class TestNettools(unittest.TestCase):
    def test_mask_24(self):
        self.assertEqual(mask_to_prefix("255.255.255.0"), 24)

    def test_mask_30(self):
        self.assertEqual(mask_to_prefix("255.255.255.252"), 30)

    def test_vlan_valid(self):
        self.assertTrue(is_valid_vlan(100))

    def test_vlan_invalid(self):
        self.assertFalse(is_valid_vlan(4095))


if __name__ == "__main__":
    unittest.main()

The last two lines let you run the file directly. Each test method is independent, and each should check one idea.

Running the tests

$ python -m unittest test_nettools.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

Each dot is a passing test. A failure shows F, and an unexpected exception shows E. The command python -m unittest discover finds every file named like test_something.py in the folder tree.

Common assert methods

MethodPasses when
assertEqual(a, b)a == b
assertNotEqual(a, b)a != b
assertTrue(x)x is true
assertFalse(x)x is false
assertIn(a, b)a is in b
assertIsNone(x)x is None
assertRaises(Error)The code raises that error

Testing an error looks like this.

def test_bad_mask(self):
    with self.assertRaises(ValueError):
        mask_to_prefix("not.a.mask")

Reading a failure

When an assertion fails, unittest prints the test name, the line and the two values that it compared.

FAIL: test_mask_24 (test_nettools.TestNettools.test_mask_24)
AssertionError: 25 != 24

The first value is what the code returned, and the second is what the test expected. Here the function returned 25 and the test expected 24, so you fix either the code or the test, whichever is wrong. In a pipeline, a failed test stops the build, so the faulty change does not reach production.

Setup and teardown

setUp runs before every test, and tearDown runs after every test. Use them to create and clean up what the tests need, so that they stay independent.

def setUp(self):
    self.devices = ["R1", "R2"]

Mocking an API call

A unit test should not call a real service. The unittest.mock module can replace a function with a stand in that returns a prepared answer.

from unittest.mock import patch, Mock
import device_api


class TestDeviceApi(unittest.TestCase):
    @patch("device_api.requests.get")
    def test_get_names(self, mock_get):
        mock_get.return_value = Mock(
            status_code=200,
            json=lambda: {"items": [{"name": "R1"}, {"name": "R2"}]},
        )
        self.assertEqual(device_api.get_names(), ["R1", "R2"])

The patch decorator swaps requests.get inside the module device_api for the mock while the test runs. The test needs no network and gives the same result every time.

Good tests

  • Test one behaviour per method.
  • Give each test a name that says what it checks.
  • Cover normal input, edge values and bad input.
  • Keep tests fast, and keep them independent of each other.

Where pytest fits

pytest is a popular third party runner. It can run unittest tests and also allows plain functions with the assert statement. The exam focuses on the standard unittest module.

Exam tip

Test method names must start with the word test. Otherwise unittest does not run them.

What to remember for the exam

Seven facts cover this topic.

  • A test class inherits from unittest.TestCase.
  • Each method that starts with test is run as a test.
  • assertEqual, assertTrue, assertFalse and assertRaises are common checks.
  • python -m unittest runs the tests, and discover finds them in a folder.
  • setUp runs before each test and tearDown runs after each one.
  • unittest.mock.patch replaces a dependency, such as an API call, during a test.
  • A failure shows F and an error shows E.

Common mistakes

Four mistakes are typical.

Naming a method without the test prefix. The method is silently skipped.

Calling the real API in a unit test. The test becomes slow and unreliable. Mock it.

Putting many checks in one test. When it fails, you do not know which idea broke.

Depending on the order of tests. Tests can run in any order, so each must set up its own data.

Watch out

A test that always passes is worse than no test. Make sure each one can fail when the code is wrong.

Check yourself

Work through these three questions.

  • How does unittest know which methods are tests?

    It runs the methods of a TestCase class whose names start with test.

  • Why mock requests.get in a unit test?

    The test then needs no network and returns the same prepared answer every time.

  • What is the job of setUp?

    It runs before each test to create what the test needs.

Topic 4 of 8 in Application Deployment and Security

Test yourself on Application Deployment and Security

The full Cisco CCNA 200-901 DEVASC question bank has 808 questions, with timed practice exams and an explanation for every answer.

This study guide is independent and is not affiliated with, endorsed by, or sponsored by Cisco. Vendor names, certification names and exam codes are trademarks of their respective owners and are used for identification only.