- The Routing Intent by Leonardo Furtado
- Posts
- Chapter 21: From Scripts to Systems with Python OOP - Part 2
Chapter 21: From Scripts to Systems with Python OOP - Part 2

In case you're trying to read this through your email, be warned: This post is large and may get clipped by services like Gmail and others.
This isn't just an article or a typical post: it is a Python crash course delivered to your inbox. However, since services like Gmail often clip larger emails, I strongly recommend opening it in your browser instead.
Also, make sure to check “Part 1” before proceeding with this edition!
10. Testing and Validating OOP Code in Practice
By now, you've written Python classes like NetworkDevice
, Router
, Switch
, and Interface
. You've added logic, composition, and even special methods like __str__()
to improve readability. But how do you know if your code works?
In the real world, especially when building automation modules that touch real networks, validating object behavior through tests is crucial. This applies not just for correctness, but also to help future-proof your code as it grows or changes.
Let’s walk through how to set up a lightweight but effective test and validation setup for your OOP code.
Why Test OOP Code?
Without testing, you risk:
Devices being instantiated incorrectly
Methods returning unexpected results
Interface states being mishandled
Future refactors silently breaking things
Testing helps:
✅ Validate logic
✅ Prevent regressions
✅ Encourage modularity
✅ Make future maintenance easier
✅ Build trust in automation
Step 1: Create a Basic Test Environment
We’re going to simulate a small network and run behaviors on it.
Let’s assume you have the following classes already defined:
NetworkDevice
Router(NetworkDevice)
Switch(NetworkDevice)
Interface
If you don't already have these classes from previous exercises, let's create them from scratch and then manually set up test instances.
from pprint import pprint
class NetworkDevice:
def __init__(self, hostname, ip_address, vendor):
self.hostname = hostname
self.ip_address = ip_address
self.vendor = vendor
self.interfaces = []
def add_interface(self, interface):
self.interfaces.append(interface)
def __repr__(self):
return f"{self.vendor} {self.hostname} ({self.ip_address})"
class Router(NetworkDevice):
def generate_config_snippet(self):
return f"hostname {self.hostname}\ninterface Gig0/0\n ip address {self.ip_address}\n"
def check_bgp_peers(self):
# Simulate dummy peer state
return ["10.10.10.1 (Established)", "10.10.10.2 (Idle)"]
class Switch(NetworkDevice):
def generate_config_snippet(self):
return f"hostname {self.hostname}\nswitchport mode access\ninterface Eth1\n ip address {self.ip_address}\n"
class Interface:
def __init__(self, name, status, speed, is_uplink):
self.name = name
self.status = status
self.speed = speed
self.is_uplink = is_uplink
def __repr__(self):
return f"Interface {self.name}: {self.status}, {self.speed}, Uplink: {self.is_uplink}"
# Create devices
r1 = Router("R1", "10.0.0.1", "Cisco")
sw1 = Switch("SW1", "10.0.0.10", "Arista")
# Add interfaces
r1.add_interface(Interface("Gig0/0", status="up", speed="10G", is_uplink=True))
r1.add_interface(Interface("Gig0/1", status="down", speed="1G", is_uplink=False))
sw1.add_interface(Interface("Eth1", status="up", speed="10G", is_uplink=True))
sw1.add_interface(Interface("Eth2", status="up", speed="1G", is_uplink=False))
# Print device summaries
print(r1)
print(sw1)
# Show interface status
for iface in r1.interfaces:
print(iface)
Output:
Cisco R1 (10.0.0.1)
Arista SW1 (10.0.0.10)
Interface Gig0/0: up, 10G, Uplink: True
Interface Gig0/1: down, 1G, Uplink: False
At this stage, you can verify visually if everything looks correct. But let’s go deeper.
Step 2: Assert Expected Behaviors
Now let’s assert that things work as expected, which is the first step toward unit testing.
assert r1.hostname == "R1"
assert len(r1.interfaces) == 2
# Check uplink status
assert r1.interfaces[0].is_uplink is True
assert r1.interfaces[0].status == "up"
assert r1.interfaces[1].status == "down"
# Validate a method
assert r1.generate_config_snippet().startswith("hostname R1")
If anything fails, Python raises an AssertionError
. You don’t need a full testing framework yet; just validating assumptions is incredibly useful.
Step 3: Write a Simple Health Check Simulator
Let’s define a function to simulate a basic audit of devices:
def check_interface_health(device):
print(f"Checking {device.hostname} interfaces...")
for iface in device.interfaces:
if iface.is_uplink and iface.status != "up":
print(f"[ALERT] {device.hostname} - {iface.name} is a DOWN uplink!")
elif not iface.is_uplink and iface.status != "up":
print(f"[WARN] {device.hostname} - {iface.name} is down (non-uplink)")
else:
print(f"[OK] {device.hostname} - {iface.name} is {iface.status}")
# Run it
check_interface_health(r1)
check_interface_health(sw1)
Sample Output (focusing on the health check simulator code snippet above):
Checking SW1 interfaces...
[OK] SW1 - Eth1 is up
[OK] SW1 - Eth2 is up
This mimics real-world logic you might embed in a CI pipeline or an audit script.
Step 4: Validate Inheritance Behavior
Suppose your Router
class has a custom method (which should have, if you pasted it earlier):
class Router(NetworkDevice):
def check_bgp_peers(self):
# Simulate dummy peer state
return ["10.10.10.1 (Established)", "10.10.10.2 (Idle)"]
Let’s assert its output (make sure to add this to your code):
peers = r1.check_bgp_peers()
assert "10.10.10.1 (Established)" in peers
This confirms that class-specific behaviors are accessible and return expected results.
Step 5: Wrap It in a Reusable Validation Script
Here's a consolidated example. Ensure to add it to your code:
def test_device(device):
print(f"\nDevice Summary: {device}")
assert device.hostname is not None
assert isinstance(device.interfaces, list)
for iface in device.interfaces:
print(f"Interface: {iface}")
assert iface.name.startswith("Gig") or iface.name.startswith("Eth")
assert iface.status in ["up", "down"]
if isinstance(device, Router):
bgp_peers = device.check_bgp_peers()
print("BGP Peers:", bgp_peers)
assert len(bgp_peers) > 0
# Run against all test devices
test_device(r1)
test_device(sw1)
Output (focusing on the snippet above):
Device Summary: Cisco R1 (10.0.0.1)
Interface: Interface Gig0/0: up, 10G, Uplink: True
Interface: Interface Gig0/1: down, 1G, Uplink: False
BGP Peers: ['10.10.10.1 (Established)', '10.10.10.2 (Idle)']
Device Summary: Arista SW1 (10.0.0.10)
Interface: Interface Eth1: up, 10G, Uplink: True
Interface: Interface Eth2: up, 1G, Uplink: False
This helps catch malformed interfaces, bad states, or failed method overrides, without a test framework!
Bonus: Quick-and-Dirty Simulation of Link Traversal
Modify the classes NetworkDevice
and Interface
to reflect the following:
class NetworkDevice:
def __init__(self, hostname, ip_address, vendor):
self.hostname = hostname
self.ip_address = ip_address
self.vendor = vendor
self.interfaces = []
def add_interface(self, interface):
interface.device = self # Set back-reference to this device
self.interfaces.append(interface)
def __repr__(self):
return f"{self.vendor} {self.hostname} ({self.ip_address})"
class Interface:
def __init__(self, name, status, speed, is_uplink):
self.name = name
self.status = status
self.speed = speed
self.is_uplink = is_uplink
self.connected_to = None
self.device = None # Back-reference to parent device
def __repr__(self):
return f"Interface {self.name}: {self.status}, {self.speed}, Uplink: {self.is_uplink}"
Then, let's add this quick-and-dirty simulation to the code:
# Link interfaces manually
r1.interfaces[0].connected_to = sw1.interfaces[0]
sw1.interfaces[0].connected_to = r1.interfaces[0]
# Traverse the link
link_peer = r1.interfaces[0].connected_to
print(f"{r1.hostname}:{r1.interfaces[0].name} -> {link_peer.device.hostname}:{link_peer.name}")
Output (focusing on the quick-and-dirty simulation part of the code):
R1:Gig0/0 -> SW1:Eth1
Testing this kind of topology linkage is essential in automation systems that deal with multi-device path traversal or link validations.
Some Thoughts
Concept | Purpose |
---|---|
| Confirm code behavior manually |
Interface health checks | Simulate operational validation |
Inheritance testing | Validate subclass logic correctness |
Object graph traversal | Model and test topology navigation |
Manual simulation | Catch bugs before touching real networks |