Introduction
Network automation is great, but how do you verify your network is working correctly after changes? Automated network testing lets you define expected behavior and run tests continuously — like unit tests for your network.
Testing Tools Overview
| Tool | Purpose |
|---|---|
| Batfish | Network configuration analysis (offline, no traffic) |
| Nornir + pytest | Test actual device state |
| NAPALM | Vendor-neutral network API |
| netmiko | SSH to network devices |
| pyATS/Genie | Cisco's test framework |
Setting Up pytest for Network Testing
BASH
# Install dependencies
pip install pytest nornir nornir-netmiko napalm
# Project structure
network-tests/
├── inventory/
│ ├── hosts.yaml
│ └── groups.yaml
├── tests/
│ ├── conftest.py
│ ├── test_connectivity.py
│ ├── test_routing.py
│ └── test_interfaces.py
└── pytest.iniNornir Inventory
YAML
# inventory/hosts.yaml
router01:
hostname: 192.168.1.1
platform: ios
groups:
- cisco
switch01:
hostname: 192.168.1.10
platform: ios
groups:
- cisco
# inventory/groups.yaml
cisco:
username: admin
password: cisco123
connection_options:
netmiko:
extras:
device_type: cisco_iosconftest.py — Shared Fixtures
PYTHON
# tests/conftest.py
import pytest
from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command
@pytest.fixture(scope="session")
def nr():
'''Initialize Nornir for all tests.'''
return InitNornir(
runner={"plugin": "threaded", "options": {"num_workers": 10}},
inventory={
"plugin": "SimpleInventory",
"options": {
"host_file": "inventory/hosts.yaml",
"group_file": "inventory/groups.yaml",
},
},
)
def get_output(nr, host, command):
'''Helper: run command on single host.'''
result = nr.filter(name=host).run(
task=netmiko_send_command,
command_string=command
)
return result[host][0].resultWriting Network Tests
PYTHON
# tests/test_interfaces.py
import pytest
def test_interface_up(nr):
'''All expected interfaces should be up.'''
expected_up = {
"router01": ["GigabitEthernet0/0", "GigabitEthernet0/1"],
"switch01": ["GigabitEthernet1/0/1", "GigabitEthernet1/0/2"],
}
for host, interfaces in expected_up.items():
output = get_output(nr, host, "show interfaces status")
for iface in interfaces:
assert "connected" in output or iface in output, f"{host}: {iface} is not up!"
def test_no_err_disabled(nr):
'''No interfaces should be err-disabled.'''
result = nr.run(
task=netmiko_send_command,
command_string="show interfaces status err-disabled"
)
for host, task_result in result.items():
output = task_result[0].result
assert "err-disabled" not in output.lower(), f"{host} has err-disabled interfaces: {output}"PYTHON
# tests/test_routing.py
import pytest
def test_default_route_exists(nr):
'''All routers should have a default route.'''
result = nr.filter(F(groups__contains="routers")).run(
task=netmiko_send_command,
command_string="show ip route 0.0.0.0"
)
for host, task_result in result.items():
output = task_result[0].result
assert "0.0.0.0/0" in output, f"{host}: no default route!"
def test_bgp_neighbors_established(nr):
'''All BGP neighbors should be in Established state.'''
result = nr.filter(F(groups__contains="routers")).run(
task=netmiko_send_command,
command_string="show bgp summary"
)
for host, task_result in result.items():
output = task_result[0].result
# Check no neighbors in Idle/Active/Connect state
for line in output.split("
"):
if line and not line.startswith("BGP"):
assert "Idle" not in line and "Active" not in line, f"{host}: BGP neighbor not Established: {line}"Running Tests
BASH
# Run all tests
pytest tests/ -v
# Run specific test file
pytest tests/test_routing.py -v
# Run with HTML report
pytest tests/ -v --html=report.html
# Run only tests matching keyword
pytest tests/ -k "bgp" -v
# Parallel execution
pip install pytest-xdist
pytest tests/ -n 4 # Use 4 workersBatfish for Offline Analysis
BASH
# Run Batfish in Docker
docker run -d -p 9997:9997 -p 9996:9996 batfish/allinone
pip install pybatfishPYTHON
# Analyze network configs without touching devices
from pybatfish.client.session import Session
import pandas as pd
bf = Session(host="localhost")
bf.set_network("company-network")
bf.init_snapshot("configs/", name="snapshot1")
# Check for routing loops
loops = bf.q.detectLoops().answer().frame()
print("Routing loops:", len(loops))
# Verify reachability
reachability = bf.q.reachability(
pathConstraints=PathConstraints(startLocation="router01"),
headers=HeaderConstraints(dstIps="8.8.8.8")
).answer().frame()
print(reachability)CI/CD Integration
YAML
# .gitlab-ci.yml or GitHub Actions
network-tests:
stage: test
script:
- pip install -r requirements.txt
- pytest tests/ -v --junit-xml=test-results.xml
artifacts:
reports:
junit: test-results.xml
only:
- merge_requestsSummary
- Automated network tests catch regressions before users do
- Use Nornir + pytest for testing live device state
- Use Batfish for offline analysis of configuration changes
- Run tests in CI/CD pipelines after every infrastructure change
- Start with basic tests (interface up, routing tables) and add more over time
