Skip to main content
PacketMentor logo
Open menu
← All topics
Automation & Programmability Foundational

Python Unit Tests with pytest

The minimum pytest to pass CCNAAUTO 200-901 4.5 and write useful tests for network scripts. Six-line test, assert, parametrize, fixtures. Run with pytest -q and read the output.

Quick summary
  • pytest finds any function in any file starting with test_ and runs it. If an assert fails, the test fails. That is the whole model.
  • assert <expr> is the standard assertion; pytest rewrites it to show both sides of a failing comparison. No need for assertEquals.
  • pytest -q runs the whole suite in quiet mode. pytest -k substring runs just tests whose names match.

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

  1. Discovers tests. Any file matching test_*.py or *_test.py; any function starting with test_; any method starting with test_ in a class starting with Test.
  2. Runs them. Isolated by default — one failure does not stop the others.
  3. 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.

Master this on a real network

Want this drilled into reflex?

1:1 weekly sessions, live feedback on your labs, and US interview prep: built around the CCNA Automation® exam blueprint. Free first session. No card on file until you decide.

Claim my free session →

Get the free CCNA 12-week roadmap

You're already reading up on Python Unit Tests with pytest. The roadmap is the order I recommend studying every CCNA topic in: with what to lab each week and where Python Unit Tests with pytest fits. A written personal reply, not an autoresponder. Expect it within one business day.

Personal reply from a senior network engineer. No third-party tracking. Unsubscribe any time.