Aller au contenu
Infrastructure as Code medium

Tests testinfra : assertions Python expressives pour rôles Ansible

10 min de lecture

Logo Ansible

verify.yml Ansible suffit pour 80 % des cas, mais quand vos assertions deviennent complexes (logique conditionnelle, agrégation, comparaison cross-host), testinfra offre une API Python beaucoup plus expressive. Cette page explique quand basculer vers testinfra, comment le configurer comme verifier Molecule, et comment écrire des tests Python idiomatiques.

  • Quand préférer testinfra à verify.yml.
  • Configurer verifier: testinfra dans molecule.yml.
  • L’API testinfra : host.package, host.service, host.socket, host.file, host.run.
  • Écrire des tests pytest lancés par Molecule.
  • Combiner verify.yml et testinfra dans le même rôle.
BesoinOutil recommandé
« Le service nginx est-il running ? »verify.yml (1 assertion)
« Le port 80 est-il ouvert ? »verify.yml
« La conf nginx contient-elle 5 directives spécifiques avec des conditions ? »testinfra (Python plus lisible)
« Vérifier la cohérence cross-host (master/replica) »testinfra
« Assertion conditionnelle complexe (si X alors Y sinon Z) »testinfra
« Test d’idempotence custom »testinfra

Règle : si votre verify.yml dépasse 50 lignes ou nécessite des block: imbriqués → bascule vers testinfra.

molecule/default/molecule.yml
verifier:
name: testinfra
options:
v: true

Une seule ligne suffit pour basculer du verifier ansible (verify.yml) à testinfra (Python). options.v: true active le mode verbose pour voir les tests détaillés.

Molecule cherche les tests dans molecule/<scenario>/tests/test_*.py.

molecule/
└── default/
├── molecule.yml
├── converge.yml
└── tests/ ← convention testinfra
├── test_webserver.py
└── test_security.py
def test_nginx_is_installed(host):
"""Vérifie que nginx est installé."""
assert host.package("nginx").is_installed
def test_nginx_is_running_and_enabled(host):
"""Service nginx démarré et activé au boot."""
nginx = host.service("nginx")
assert nginx.is_running
assert nginx.is_enabled
def test_nginx_listens_on_8080(host):
"""nginx écoute sur 8080."""
assert host.socket("tcp://0.0.0.0:8080").is_listening
def test_nginx_config_valid(host):
"""nginx -t renvoie OK."""
cmd = host.run("nginx -t")
assert cmd.rc == 0
def test_index_html_content(host):
"""Page d'accueil contient le custom message."""
f = host.file("/usr/share/nginx/html/index.html")
assert f.exists
assert f.user == "root"
assert "testinfra-tested" in f.content_string

Fixture host automatique — Molecule l’injecte. Pas besoin de configurer pytest.

ModuleExempleCas d’usage
host.package(name).is_installed, .versionVérifier paquet
host.service(name).is_running, .is_enabledVérifier service systemd
host.socket(uri).is_listeningVérifier port en écoute
host.file(path).exists, .mode, .user, .content_stringVérifier fichier
host.run(cmd).rc, .stdout, .stderrExécuter commande arbitraire
host.user(name).exists, .uid, .gid, .shellVérifier utilisateur Linux
host.process.filter(comm='nginx')Vérifier process en cours
host.iptablesrulesInspecter iptables
host.systemdunit-level checkssystemd avancé
verify.yml
- name: Vérifier nginx running
ansible.builtin.systemd:
name: nginx
state: started
check_mode: true
register: r
failed_when: r is changed
# testinfra (équivalent)
def test_nginx_running(host):
assert host.service("nginx").is_running

testinfra plus court, plus lisible. Pour des cas simples, c’est juste plus propre.

def test_nginx_config_consistency(host):
"""Test complexe : 5 directives à vérifier avec logique."""
config = host.file("/etc/nginx/nginx.conf").content_string
# Worker processes auto si CPU > 4 cœurs
cpus = int(host.run("nproc").stdout.strip())
if cpus > 4:
assert "worker_processes auto" in config
else:
assert "worker_processes 4" in config
# gzip activé en HTTPS, désactivé en HTTP
if "ssl on" in config:
assert "gzip on" in config
else:
# En HTTP-only, gzip désactivé pour les attaques BREACH
assert "gzip on" not in config or "gzip_disable" in config
# SSL TLS 1.2 minimum
if "listen 443" in config:
assert "ssl_protocols TLSv1.2 TLSv1.3" in config
assert "TLSv1 " not in config
assert "TLSv1.1" not in config

Faire ce genre de test en verify.yml serait inhumain — Python brille ici.

testinfra n’est pas couplé à Molecule. Sur un projet playbook-based (sans rôle, ou sur des machines déjà provisionnées), on peut le lancer directement avec pytest. Deux backends utiles selon le contexte.

Backend local:// — tester le control node lui-même

Section intitulée « Backend local:// — tester le control node lui-même »

Pour vérifier que la machine d’exécution (poste dev, CI runner, bastion) respecte une baseline :

tests/test_baseline.py
def test_python3_is_installed(host):
assert host.package("python3").is_installed
def test_passwd_permissions(host):
f = host.file("/etc/passwd")
assert f.user == "root"
assert f.mode == 0o644
Fenêtre de terminal
pytest -v --hosts=local:// tests/

testinfra exécute les commandes localement — utile pour valider un poste, un container CI, un environnement de build.

Backend ansible:// — tester via inventaire Ansible

Section intitulée « Backend ansible:// — tester via inventaire Ansible »

Pour tester des machines distantes déjà déclarées dans un inventaire Ansible (production, staging, lab Vagrant) :

inventory.ini
[webservers]
web1.lab ansible_user=ansible
web2.lab ansible_user=ansible
tests/test_webservers.py
def test_nginx_listens_on_80(host):
assert host.socket("tcp://0.0.0.0:80").is_listening
def test_inventory_facts_available(host):
facts = host.ansible("setup")["ansible_facts"]
assert facts["ansible_distribution"] in ("Ubuntu", "Rocky", "AlmaLinux")
Fenêtre de terminal
pytest -v --hosts="ansible://webservers" \
--ansible-inventory=inventory.ini \
--force-ansible tests/

host.ansible("setup") récupère les facts comme un play Ansible — pratique pour brancher la logique de test selon la distribution sans recoder la détection.

Un cas avancé : vérifier que la version installée correspond à la source de vérité (variables d’inventaire). Si un admin patche manuellement, le test échoue et signale la dérive.

def test_nginx_version_matches_inventory(host):
facts = host.ansible.get_variables()
expected = facts.get("nginx_version")
assert expected, "nginx_version manquant dans l'inventaire"
installed = host.package("nginx").version
assert installed.startswith(expected), (
f"Drift détecté : installé={installed} attendu={expected}"
)

À brancher dans une CI nocturne — toute dérive entre le code Ansible et les machines réelles est détectée et notifiée. Voir aussi Baselines et drift.

Cette page a un lab d’accompagnement : labs/tests/testinfra/ dans stephrobert/ansible-training.

Le lab configure testinfra comme verifier Molecule avec 6 tests Python sur le rôle webserver. 5 tests structure pour valider la config (verifier testinfra, dossier tests/, fixture host, ≥4 fonctions test).

Fenêtre de terminal
cd ~/Projets/ansible-training/labs/tests/testinfra/
cat molecule/default/tests/test_webserver.py
pytest -v challenge/tests/ # 5 tests structure
SymptômeCauseFix
host fixture not foundPas dans tests/ Molecule ou pytest standaloneLancer via molecule verify, pas pytest direct
host.run retourne rc != 0Permissions sudo manquantesTests testinfra héritent de become Molecule
host.file().content_string planteFichier binaireUtiliser .content (bytes) au lieu de .content_string
Tests passent sur RHEL, échouent sur DebianPath différentCharger host.system_info.distribution puis brancher
host.socket('tcp://0.0.0.0:80') faux négatifnginx écoute sur IPv6 uniquementTester aussi tcp://:::80
  • verify.yml = défaut 2026. testinfra complément pour assertions complexes.
  • verifier: testinfra + options.v: true dans molecule.yml.
  • API : host.package, host.service, host.socket, host.file, host.run.
  • Fixture host injectée automatiquement par Molecule.
  • Combinable : verify.yml pour le simple + testinfra pour le complexe dans le même scenario.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn