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

assert statements

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

Subscribe to keep reading

This content is free, but you must be subscribed to The Routing Intent by Leonardo Furtado to continue reading.

Already a subscriber?Sign in.Not now