Skip to content
Back to Blog
Automation

Automated Network Testing with pytest and Nornir

Write automated network tests using pytest and Nornir: connectivity checks, configuration validation, and regression testing pipelines.

Nov 2025
12 min read

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

ToolPurpose
BatfishNetwork configuration analysis (offline, no traffic)
Nornir + pytestTest actual device state
NAPALMVendor-neutral network API
netmikoSSH to network devices
pyATS/GenieCisco'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.ini

Nornir 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_ios

conftest.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].result

Writing 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 workers

Batfish for Offline Analysis

BASH
# Run Batfish in Docker
docker run -d -p 9997:9997 -p 9996:9996 batfish/allinone

pip install pybatfish
PYTHON
# 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_requests

Summary

  • 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