Mental model
Cisco objective 4.5 says “Construct a Python unit test”. pytest is the standard library the entire Python world uses. 6 lines of code and you have a working test.
# File: test_vlans.py
from vlans import is_valid_vlan
def test_vlan_1_is_valid():
assert is_valid_vlan(1) is True
def test_vlan_4095_is_invalid():
assert is_valid_vlan(4095) is False
Run with pytest -q:
.. [100%]
2 passed in 0.03s
Each dot is one passing test. An F is a fail; E is an error raising before the assertion.
The three things pytest does for you
- Discovers tests. Any file matching
test_*.pyor*_test.py; any function starting withtest_; any method starting withtest_in a class starting withTest. - Runs them. Isolated by default — one failure does not stop the others.
- Reports them with rich diff output when an assertion fails.
assert
Python’s built-in assert is all you need. pytest introspects it so a failure shows both sides:
def test_names():
assert ["sw1", "sw2"] == ["sw1", "sw3"]
Failure:
> assert ["sw1", "sw2"] == ["sw1", "sw3"]
E AssertionError: assert ['sw1', 'sw2'] == ['sw1', 'sw3']
E At index 1 diff: 'sw2' != 'sw3'
E Use -v to get more diff
Compare that to self.assertEqual from Python’s older unittest library. pytest is much less code.
Install and layout
pip install pytest
Typical project:
myproject/
├── vlans.py # production code
├── devices.py
└── tests/
├── test_vlans.py
└── test_devices.py
Run from the project root:
pytest # all tests, verbose list
pytest -q # quiet mode (dots)
pytest -v # verbose (test name + outcome)
pytest -k "vlan" # only tests whose names contain 'vlan'
pytest tests/test_vlans.py # one file
pytest tests/test_vlans.py::test_valid_vlan_1 # one test
pytest -x # stop at first failure
pytest --tb=short # shorter traceback on failure
parametrize (same test, many inputs)
Instead of repeating:
def test_vlan_1_valid(): assert is_valid_vlan(1)
def test_vlan_100_valid(): assert is_valid_vlan(100)
def test_vlan_4094_valid(): assert is_valid_vlan(4094)
Write once:
import pytest
@pytest.mark.parametrize("vid", [1, 100, 4094])
def test_valid_vlans(vid):
assert is_valid_vlan(vid) is True
pytest runs the test 3 times, naming each run test_valid_vlans[1], [100], [4094].
fixtures (shared setup)
A fixture is a function that returns something reusable (a mock device, a temp directory, a connected API client). pytest injects it by name.
import pytest
@pytest.fixture
def sample_devices():
return [{"name": "sw1", "ip": "10.0.0.1"}, {"name": "sw2", "ip": "10.0.0.2"}]
def test_two_devices(sample_devices):
assert len(sample_devices) == 2
def test_first_name(sample_devices):
assert sample_devices[0]["name"] == "sw1"
The sample_devices fixture runs once per test; both tests get a fresh list.
Built-in fixtures worth knowing:
tmp_path— a fresh temp directory for the test.capsys— capture stdout/stderr the code under test printed.monkeypatch— swap functions, env vars, or dict keys for the duration of a test.
Testing a function that calls a REST API
Do not make real HTTP calls from unit tests. Mock the requests library:
from unittest.mock import patch
from mymeraki import get_networks
def test_get_networks():
fake_response = [{"id": "N_1", "name": "HQ"}]
with patch("mymeraki.requests.get") as mock_get:
mock_get.return_value.json.return_value = fake_response
mock_get.return_value.status_code = 200
result = get_networks(api_key="x")
assert result == fake_response
mock_get.assert_called_once()
Fast, deterministic, no risk of hitting rate limits during a test run.
Reading pytest failures
========================= FAILURES =========================
_____________________ test_vlan_0_invalid _____________________
def test_vlan_0_invalid():
> assert is_valid_vlan(0) is False
E AssertionError: assert True is False
E + where True = is_valid_vlan(0)
tests/test_vlans.py:7: AssertionError
Read bottom-up:
- File + line (where the test failed).
- Expression (
is_valid_vlan(0) is False). - What was True vs expected False.
- The actual return value (
+ where True = is_valid_vlan(0)).
FAQ
Do I need to inherit from a TestClass? No. pytest supports plain functions. (It can run unittest.TestCase classes too, so legacy code just works.)
Where should the tests live? Convention: a top-level tests/ directory. pytest finds them from the project root.
How do I run tests in CI/CD? Add pytest -q as a step. Pipeline fails if any test fails. See CI/CD pipeline components.
Can pytest test asyncio code? Yes, with pytest-asyncio. Outside 200-901 scope but useful for aiohttp / httpx code.
