Advanced Python Automation for Network Engineers
Testing Automation Code Without Real Devices
What you'll learn
- The difference between a unit test (a fake) and an integration test (the mock server)
- How to write and run real tests with stdlib unittest
- Why stdlib unittest, not pytest, is used in this module
- An honest, verified finding about testing code that includes retry logic
Listen to a guided explanation of this lesson.
Spoken narration isn't supported in this browser.
Two different kinds of test
Every verification in this curriculum so far has run against a real mock server — that's an integration test: real HTTP, a real (if fake) backend. A unit test goes one level further: no HTTP at all, just a fake object satisfying the same interface, injected directly.
- Real HTTP requestagainst the mock server
- Tests the whole chainConnection, network, backend
- Slower, needs a running server
- A fake object, no HTTP at all
- Tests check_device()'s logic in isolation
- Fast, needs nothing running
A fake connection
import unittest
from network_automation.automation import check_device
from network_automation.exceptions import AuthenticationError, DeviceUnreachableError
from network_automation.models import AutomationResult, Device, Status, Vlan
class FakeConnection:
"""Satisfies the same interface as Connection.get_state() -- no HTTP at all."""
def __init__(self, vlans=None, raises=None):
self._vlans = vlans or []
self._raises = raises
def get_state(self, hostname):
if self._raises is not None:
raise self._raises
return self._vlans
class CheckDeviceTests(unittest.TestCase):
def test_already_compliant(self):
device = Device(hostname="SW1", required_vlans=[Vlan(id=10, name="USERS")])
conn = FakeConnection(vlans=[{"id": 10, "name": "USERS"}])
result = check_device(conn, device)
self.assertEqual(
result,
AutomationResult(hostname="SW1", status=Status.ALREADY_COMPLIANT, detail="no change needed"),
)
def test_unreachable_device(self):
device = Device(hostname="SW4", required_vlans=[Vlan(id=10, name="USERS")])
conn = FakeConnection(raises=DeviceUnreachableError("SW4 timed out"))
result = check_device(conn, device)
self.assertEqual(result.status, Status.UNREACHABLE)FakeConnection only implements get_state() -- exactly the one method check_device() actually calls. Passing raises= lets one test simulate a device that's unreachable, no real timeout required.
Actually running them
python -m unittest discover -v -s testsRun for real, not just written and assumed to pass.
test_already_compliant (test_automation.CheckDeviceTests.test_already_compliant) ... ok
test_authentication_failure (test_automation.CheckDeviceTests.test_authentication_failure) ... ok
test_missing_vlan_is_validation_failed (test_automation.CheckDeviceTests.test_missing_vlan_is_validation_failed) ... ok
test_unreachable_device (test_automation.CheckDeviceTests.test_unreachable_device) ... ok
----------------------------------------------------------------------
Ran 4 tests in 1.002s
OKReal, verified output — all 4 tests genuinely executed and passed.
A real, honest surprise: 1 second, not milliseconds
These are unit tests with no HTTP involved — you'd expect them to run in milliseconds. They took 1.002s. Why: check_device() calls get_state_with_retry(), which carries Lesson 4's real @retry decorator. When the fake raises DeviceUnreachableError, the decorator genuinely retries — real time.sleep() calls, even though the connection itself is fake. This is a real, verified trade-off, not a flaw hidden from you: decorated code's real behavior (including real delays) still applies when it's unit tested, unless the test is specifically designed around it.
Why stdlib unittest, not pytest
A deliberate choice, not an oversight
pytest is a genuinely nicer tool for large real-world test suites. It's also this curriculum's fifth external Python dependency if added here — and stdlib unittest, verified directly above, fully covers what this module needs to teach: test structure, fakes, and failure-path testing. Every module since Netmiko Fundamentals has added exactly one new dependency only when the capability genuinely required it. Testing doesn't.
Key takeaways
- A unit test uses a fake object with no real HTTP; an integration test (every mock-server verification so far) talks to a real, if fake, backend.
- FakeConnection implements only get_state() — enough to satisfy check_device()'s actual needs, with a raises= option to simulate any failure without a real device.
- All 4 tests were actually run: 4 tests, 1.002s, OK — not assumed to pass.
- That 1.002s (not milliseconds) is real and explained: the @retry decorator's real delays apply even to a fake connection — an honest trade-off, not a hidden flaw.
- stdlib unittest, not pytest — a deliberate choice; this module's testing needs are fully covered without a fifth external dependency.