NetAuto Academy

Advanced Python Automation for Network Engineers

Testing Automation Code Without Real Devices

Advanced14 min read

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
🎓 Teach This Lesson

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.

Integration test
  1. Real HTTP requestagainst the mock server
  2. Tests the whole chainConnection, network, backend
  3. Slower, needs a running server
Unit test (this lesson)
  1. A fake object, no HTTP at all
  2. Tests check_device()'s logic in isolation
  3. Fast, needs nothing running
Quick checkConcept check
What makes a test a 'unit test' rather than an 'integration test' in this lesson's sense?

A fake connection

tests/test_automation.py
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.

Quick checkIdentify the correct code
Which line makes FakeConnection able to simulate SW4 being unreachable, without any real network call?

Actually running them

terminal
python -m unittest discover -v -s tests

Run for real, not just written and assumed to pass.

output
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

OK

Real, 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.

Quick checkScenario
This lesson's own test suite takes just over 1 second for 4 tests with no real HTTP calls. Why, specifically?

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.

Quick checkMyth check
Does testing automation code require installing pytest?

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.