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
- A test class inherits from
unittest.TestCase. - Each method that starts with
testis run as a test. assertEqual,assertTrue,assertFalseandassertRaisesare 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.
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
| Method | Passes 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.
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
testis run as a test. assertEqual,assertTrue,assertFalseandassertRaisesare common checks.python -m unittestruns the tests, anddiscoverfinds them in a folder.setUpruns before each test andtearDownruns after each one.unittest.mock.patchreplaces a dependency, such as an API call, during a test.- A failure shows
Fand an error showsE.
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.
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
unittestknow which methods are tests?It runs the methods of a
TestCaseclass whose names start withtest.Why mock
requests.getin 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.